控制流平坦化 ,Control-Flow-Flattening(FLA),的基本思想主要是通过一个主分发器来控制程序基本块的执行流程。该方法把函数内所有基本代码块放到控制流图的最底部,然后删除掉原来的基本块之间的跳转关系,添加混淆器的流程控制分发逻辑,通过新的复杂分发逻辑还原原来程序块之间的逻辑关系。即,通过一个主分发器,控制整个函数内代码块的运行流程。
-mllvm -fla : activates control flow flattening
-mllvm -split : activates basic block splitting. Improve the flattening when applied together.
-mllvm -split_num=3 : if the pass is activated, applies it 3 times on each basic block. Default:1
OLLVM控制流平坦化混淆源码位置:OLLVMCODE\lib\Transforms\Obfuscation\Flattening.cpp
0x00 getAnalysisUsage函数
这里我是用的LLVM12进行学习的。在新版LLVM中,若要调用其他Pass,不能够在Pass中直接Create了,必须override一个函数来描述Pass的依赖关系。比如在Flat的时候,OLLVM会将switch结构降级成if以扩大基本块的数量,以便于下一次再进行Flat的时候可以利用更多的基本块进行平坦化。 大家可能在网上其他博客中看到,在flatten函数开头有这一句,即create了一个LowerSwitchPass对象并用该对象对本函数进行处理
// Lower switch
FunctionPass * lower = createLowerSwitchPass ();
lower -> runOnFunction ( * f );
而在新版LLVM中,则是override了getAnalysisUsage函数,在函数中定义了依赖LowerSwitchPass,即要求在调用这个Pass之前要调用一次LowerSwitchPass进行预处理。可以说从结构上更加规范了。
void getAnalysisUsage ( AnalysisUsage & AU ) const override {
AU . addRequiredID ( LowerSwitchID );
FunctionPass :: getAnalysisUsage ( AU );
}
0x01 runOnFunction函数
bool Flattening :: runOnFunction ( Function & F ) {
Function * tmp = & F ;
// Do we obfuscate
if ( toObfuscate ( flag , tmp , "fla" )) {
if ( flatten ( tmp )) {
++ Flattened ;
}
}
return false ;
}
在runOnFunction函数中,同Bogus和Substitution,调用toObfuscate函数判断是否要进行混淆,如果为真,则调用flatten对函数进行混淆。
0x01 flatten函数
不同与BCF和Sub,在Pass收集基本块之前,会设置一个名为scrambling_key的变量。会调用llvm::cryptoutils->get_bytes获取一个随机种子,这个种子用于后面产生switch-case的随机case值
// SCRAMBLER
char scrambling_key [ 16 ];
llvm :: cryptoutils -> get_bytes ( scrambling_key , 16 );
然后,同之前的混淆,会将所有的基本块存在一个vector里,然后会有一个判断,如果当前处理的函数中有一个基本块以 Invoke 指令结尾,那么该函数无法混淆;如果整个函数只有一个基本块,也无法混淆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Save all original BB
for ( Function :: iterator i = f -> begin (); i != f -> end (); ++ i ) {
BasicBlock * tmp = &* i ;
origBB . push_back ( tmp );
// 如果当前处理的函数中有一个基本块以 Invoke 指令结尾,那么该函数无法混淆
BasicBlock * bb = &* i ;
if ( isa < InvokeInst > ( bb -> getTerminator ())) {
return false ;
}
}
// Nothing to flatten
if ( origBB . size () <= 1 ) {
return false ;
}
然后会从vector中删除第一个基本块,并做一些特殊判断处理。因为平坦化要求第一个基本块只能有一个后继基本块,如果第一个基本块末尾就是条件跳转(有两个或多个后继块)就无法进行后面的平坦化操作。因此,需先判断第一个基本块末尾是否是条件跳转,如果是,则要把这个条件跳转的语句切割出来,成为一个新的基本块,并插入到vector开头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Remove first BB
origBB . erase ( origBB . begin ());
// Get a pointer on the first BB
Function :: iterator tmp = f -> begin (); //++tmp;
BasicBlock * insert = &* tmp ;
// If main begin with an if
BranchInst * br = NULL ;
if ( isa < BranchInst > ( insert -> getTerminator ())) {
br = cast < BranchInst > ( insert -> getTerminator ());
}
if (( br != NULL && br -> isConditional ()) ||
insert -> getTerminator () -> getNumSuccessors () > 1 ) {
BasicBlock :: iterator i = insert -> end ();
-- i ;
if ( insert -> size () > 1 ) {
-- i ;
}
// 将条件跳转的语句切割出来,成为一个新的基本块,并插入到vector开头
BasicBlock * tmpBB = insert -> splitBasicBlock ( i , "first" );
origBB . insert ( origBB . begin (), tmpBB );
}
// 删除第一个基本块最后的末尾跳转
insert -> getTerminator () -> eraseFromParent ();
下面就是要构建主分发器了。OLLVM-FLA利用While-Switch结构重新组合分发原始基本块从而实现平坦化。Switch分发结构就是主分发器。而switch需要switch var,在OLLVM中,switch var是基本块的随机数编号,OLLVM为每一个基本块用随机数编号,并通过switch进行分发。其基本模式类似如下
while ( true ) {
switch ( var ) {
case 0x5166726F :
BB1 ; break ;
case 0x6F726651 :
BB2 ; break ;
.....
}
}
这部分代码就是创建switch var。AllocaInst意为在栈上创建变量,ConstantInt::get 获取指定类型的常量数据,用于给分配的变量赋初值。
// Create switch variable and set as it
switchVar =
new AllocaInst ( Type :: getInt32Ty ( f -> getContext ()), 0 , "switchVar" , insert );
// StoreInst::StoreInst(Value *val, Value *addr, BasicBlock *InsertAtEnd)
new StoreInst (
ConstantInt :: get ( Type :: getInt32Ty ( f -> getContext ()),
llvm :: cryptoutils -> scramble32 ( 0 , scrambling_key )),
switchVar , insert );
然后后面要构建两个基本块分别作为主分发器和子分发器。注意这里的insert依旧是第一个基本块的对象指针。
// Create main loop
// static BasicBlock *Create(LLVMContext &Context, const Twine &Name="", Function *Parent = nullptr, BasicBlock *InsertBefore = nullptr)
loopEntry = BasicBlock :: Create ( f -> getContext (), "loopEntry" , f , insert );
loopEnd = BasicBlock :: Create ( f -> getContext (), "loopEnd" , f , insert );
然后这里有个问题就是这三个基本块的关系。BasicBlock::Create 创建的基本块,是在第四个参数 BasicBlock InsertBefore 之前插入基本块(如果不指定 InsertBefore则会插入在函数末尾)。那么这样创建完后,三个基本块的关系是:loopEntry->loopEnd->insert
而我们知道,正确的调用关系应该是 insert-> loopEntry, loopEntry -> 基本块 -> loopEnd, loopEnd -> loopEntry 。因此,需要调整一下它们的位置,使loopEnd也指向loopEntry,第一个基本块到loopEntry的跳转在后面建立
// Move first BB on top
insert -> moveBefore ( loopEntry );
BranchInst :: Create ( loopEntry , loopEnd );
下面就是创建switch结构。这里有个点巨坑,就是对F.begin()删除末尾跳转。如果大家前面认真看的话,会发现前面对insert块做了删除末尾跳转的操作,然后又建立了第一个基本块到loopEntry的条件跳转,然后这里又对F.begin()做了删除末尾跳转的操作,最后在后面还会建立F.begin()到loopEntry的跳转。可能是因为我太菜了,没看懂这是什么魔幻操作,就把这部分代码删了,只留一句 *BranchInst::Create(loopEntry, &f->begin()); 即建立第一个基本块到loopEntry的条件跳转。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// load switchVar 变量
load = new LoadInst ( switchVar -> getType () -> getElementType (), switchVar , "switchVar" , loopEntry );
// 创建 switch default 基本块, default -> loopEnd
BasicBlock * swDefault =
BasicBlock :: Create ( f -> getContext (), "switchDefault" , f , loopEnd );
BranchInst :: Create ( loopEnd , swDefault );
// 创建 SwitchInst 指令
switchI = SwitchInst :: Create ( &* f -> begin (), swDefault , 0 , loopEntry );
// 设置 SwitchVar 变量
switchI -> setCondition ( load );
// 使函数第一个初始块指向loopEntry
BranchInst :: Create ( loopEntry , &* f -> begin ());
下面要将所有基本块装入switch,同时,为每个基本块生成一个唯一的随机数作为块编号,在switch中添加分支,将随机的编号和对应的基本块对应起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Put all BB in the switch
for ( vector < BasicBlock *>:: iterator b = origBB . begin (); b != origBB . end ();
++ b ) {
BasicBlock * i = * b ;
ConstantInt * numCase = NULL ;
// Move the BB inside the switch (only visual, no code logic)
i -> moveBefore ( loopEnd );
// Add case to switch
numCase = cast < ConstantInt > ( ConstantInt :: get (
switchI -> getCondition () -> getType (),
llvm :: cryptoutils -> scramble32 ( switchI -> getNumCases (), scrambling_key ))); // 可以理解成获取随机数,不会重复
switchI -> addCase ( numCase , i ); // 添加随机数编号和BB的关联
}
从这段代码来看,switch var 是在添加 case 的时候生成的,整个代码中都没有维护 switch var 与 基本块关系的变量,这是因为 o-llvm 利用 llvm 本身来维护switch var 值与基本块的对应关系。SwitchInst.findCaseDest 函数可以获取 Switch 中指定基本块的 case 常量。
接下来要调整基本块之间的关系。我们知道。OLLVM-FLA的设计思路是在每个case执行完之后,在case的逻辑最后更新switch var的值。故,OLLVM-FLA会遍历每一个基本块,并依据基本块末尾跳转的类型做处理并设置switch var的值为下一个基本块的case值,这里有三种情况
无后继基本块
也就是该基本块没有末尾跳转。即,该块是以 ret 结尾的结束块。这种块因为不需要重新回到分发器,所以不用做任何处理
// Ret BB
if ( i -> getTerminator () -> getNumSuccessors () == 0 ) {
continue ;
}
// LLVM可用 (BasicBlock *)->getTerminator()->getNumSuccessors() 获取后继块数量
无条件跳转结尾基本块
无条件跳转基本块,后面会有一个后继
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// If it's a non-conditional jump
if ( i -> getTerminator () -> getNumSuccessors () == 1 ) { // 含有一个后继块
// 获取后继基本块
BasicBlock * succ = i -> getTerminator () -> getSuccessor ( 0 );
// 删除到后继块的跳转
i -> getTerminator () -> eraseFromParent ();
// 获取后继基本块的case值
numCase = switchI -> findCaseDest ( succ );
// 如果没有找到后继基本块对应的 case 值,default
if ( numCase == NULL ) {
numCase = cast < ConstantInt > (
ConstantInt :: get ( switchI -> getCondition () -> getType (),
llvm :: cryptoutils -> scramble32 (
switchI -> getNumCases () - 1 , scrambling_key ))); // 获取 default 的 case 值
}
// 更新 switchVar 的值
new StoreInst ( numCase , load -> getPointerOperand (), i );
BranchInst :: Create ( loopEnd , i ); // 跳转到loopEnd
continue ;
}
这里有个坑点,就是这个DefaultCase。根据平坦化的设计思路,后继块应该都在基本块列表中并在前面添加好了case值,也就是说真实块一定都是有case值的,那default块有什么用呢。在建立switch结构的时候,本质上做的就是一次一次条件判断,即我们在CFG图中看到的子分发器,也就是说必然会有一个分支需要指向Default块(虽然这个Default永远不会被运行到),而这条分支就是没有后继case值的,因此需要进行判断。
条件跳转结尾基本块
如果当前是条件跳转基本块,那么应该会有两个分支,即有两个后继
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// If it's a conditional jump
if ( i -> getTerminator () -> getNumSuccessors () == 2 ) {
// 获取两个后继的case值
ConstantInt * numCaseTrue =
switchI -> findCaseDest ( i -> getTerminator () -> getSuccessor ( 0 ));
ConstantInt * numCaseFalse =
switchI -> findCaseDest ( i -> getTerminator () -> getSuccessor ( 1 ));
// 如果后继case值为空则设置为 default case
if ( numCaseTrue == NULL ) {
numCaseTrue = cast < ConstantInt > (
ConstantInt :: get ( switchI -> getCondition () -> getType (),
llvm :: cryptoutils -> scramble32 (
switchI -> getNumCases () - 1 , scrambling_key )));
}
if ( numCaseFalse == NULL ) {
numCaseFalse = cast < ConstantInt > (
ConstantInt :: get ( switchI -> getCondition () -> getType (),
llvm :: cryptoutils -> scramble32 (
switchI -> getNumCases () - 1 , scrambling_key )));
}
// Create a SelectInst
BranchInst * br = cast < BranchInst > ( i -> getTerminator ());
// static SelectInst *Create(Value *C, Value *S1, Value *S2, const Twine &NameStr = "", Instruction *InsertBefore = nullptr,Instruction *MDFrom = nullptr)
SelectInst * sel =
SelectInst :: Create ( br -> getCondition (), numCaseTrue , numCaseFalse , "" ,
i -> getTerminator ());
// 意为 根据br->getCondition()是否为True来决定*sel的值是numCaseTrue还是numCaseFalse,并把这条选择语句插入到i->getTerminator()之前
// 删除结尾跳转指令
i -> getTerminator () -> eraseFromParent ();
// 更新 switchVar 的值
new StoreInst ( sel , load -> getPointerOperand (), i );
BranchInst :: Create ( loopEnd , i ); // 跳转到 loopEnd
continue ;
}
以上就是平坦化混淆实现的全部内容了,最后,函数内调用了一下fixStack函数修复栈
0x02 fixStack函数
OLLVM-FLA最后一步是 fixStack(Function *f) 这一步是处理局部变量分配问题,主要处理两种
PHI Node
非入口基本块中分配的局部变量
对于 PHI Node,fixStack 直接调用 DemotePHIToStack 将 PHI Node 变量 Entry 中分配
DemotePHIToStack ( tmpPhi . at ( i ), f -> begin () -> getTerminator ());
对于非 Entry 基本块中申请的局部变量,若该变量在其它基本块中还有使用的话,需要将该变量提到 Entry 中分配
DemoteRegToStack ( * tmpReg . at ( i ), f -> begin () -> getTerminator ());
0x03 魔改思路
标准OLLVM-FLA已经被玩烂了,基本都是通过loopEnd(预处理器)定位基本块,然后对所有基本块开始符号执行,并将各个基本块连接起来。当然,很多人会在使用OLLVM-FLA的同时搭配BCF和Sub,并开启编译器优化。编译器优化会破坏标准平坦化的五部分结构,故网络上的一键自动化脚本基本都会失效。 但是,编译器优化毕竟是不确定的,希望能通过魔改OLLVM算法,来实现加强混淆。
多个loopEnd
动态生成多个loopEnd,因为识别真实块是需要先识别出loopEnd然后找loopEnd的前继。生成多个loopEnd后分析者必须得找全所有loopEnd才可以适配到所有真实块。
loopEnd直接跳转基本块
因为标准OLLVM是在每个真实块结尾处设置switchVar,loopEnd仅作为跳转到loopEntry的跳板。所以只需要识别出loopEntry的前继就可以识别到所有loopEnd,进而识别出所有真实块,这也是我认为平坦化设计上的最大弱点。为了魔改的时候克服掉这个弱点,可以将修改loopEnd的作用。
大概思想就是主分发器只会进行第一次分发,并执行第一个基本块。第一个基本块执行完毕后进入loopEnd,至此,loopEnd(预处理器)接管主分发器的全部功能,在预处理器里也实现一个switch结构,并插入LoadInst加载switchVar,再用于SwitchInst跳转。即,原先loopEnd会跳转到loopEntry进行下一次分发,魔改后由loopEnd直接跳转到下一个基本块。
基于多个loopEnd和loopEnd直接跳转基本块的思想,实现出了一个魔改平坦化Pass,以下是效果图:
复杂分发过程
OLLVM的平坦化,仅仅是靠一个switch来实现分发,并在每个真实块的末尾修改switchVar的值来继续控制下一次的分发。可以自实现整个分发过程,比如 Pluto FlatteningEnhanced 构造了一颗随机二叉树并将所有真实块排列到树的叶子节点上,并为每个父节点设置分发块。通过树结构为每个 节点/块 设置权值与跳转,自实现整个平坦化的分发过程。 他的算法相较于原版OLLVM-FLA复杂很多,后面会给出分析文章。但是因为他这个套思想没有解决loopEnd的前继就是真实块这个弱点,所以对于符号执行去混淆来说,难度不会提升太多。
0x04 参考资料
基于LLVM Pass实现控制流平坦化
基于LLVM的控制流平坦化的魔改和混淆Pass实战
重写 OLLVM 之控制流平坦化
Pandaos-控制流平坦化源码分析
[原创]ollvm源码分析 - Pass之Flattening
Pluto-Obfuscator