重写 OLLVM 之控制流平坦化
2021-01-21 #re #llvmOLLVM 的控制流平坦化是逆向老哥最讨厌的混淆了,因为一旦使用就代表着 IDA 的 decompiler 彻底报废了。所以了解并在自己的项目中重写控制流平坦化挺重要的。
什么是控制流平坦化
名词解释
BasicBlock
代码块,以跳转语句结尾的一段代码。
语言描述控制流平坦化的实现
在 OLLVM 中,Pass 先实现一个永真循环,然后再在这个循环中放入 switch 语句,将代码中除了开始块的所有 BasicBlock 放入这个 switch 语句的不同 case 中,通过修改 switch 的条件,来实现 BasicBlock 之间的跳转。
可行的修复方法
在 利用符号执行去除控制流平坦化 讲解了恢复控制流平坦化的方法,并且在 cq674350529/deflat 使用了 angr 进行实现。
这里引用腾讯 SRC 的思路概括
- 函数的开始地址为序言的地址
- 序言的后继为主分发器
- 后继为主分发器的块为预处理器
- 后继为预处理器的块为真实块
- 无后继的块为retn块
- 剩下的为无用块
怎么实现
在开始前,先展示一个普通的,带有 if
语句的程序的控制流图。
+--------------------------------------+
| entry: |
| ... |
| compare instruction |
| br %cmp_res, label %if, label %else |
+-------------------+-----------+------+
| |
+-------------+ |
v v
+--------+-------+ +--------+-------+
| if: | | else: |
| br label %end | | br label %end |
+--------+-------+ +--------+-------+
| |
+-----------+-------------+
v
+--+---+
| end: |
| ... |
+------+
首先,先判断函数的 BasicBlock 数量,如果只有一个 BasicBlock,那么就不用进行混淆了。然后将函数中原有的的 BasicBlock 存放到容器中备用,并排除第一个 BasicBlock,因为第一个 BasicBlock 要做特殊处理。
bool
然后,将第一个 BasicBlock 和他结尾的 BranchInst 进行分割,并将第一个 BasicBlock 和后面的 BB 断绝关系。
// If firstBB's terminator is BranchInst, then split into two blocks
BasicBlock *firstBB = &*F.;
if
// Remove firstBB
firstBB->->;
经过处理后程序控制流图如下。
+--------------------------------------+
| entry: |
| ... |
+--------------------------------------+
+--------------------------------------+
| tempBB: |
| compare instruction |
| br %cmp_res, label %if, label %else |
+-------------------+-----------+------+
| |
+-------------+ |
v v
+--------+-------+ +--------+-------+
| if: | | else: |
| br label %end | | br label %end |
+--------+-------+ +--------+-------+
| |
+-----------+-------------+
v
+--+---+
| end: |
| ... |
+------+
接下来就是创建主循环和 switch 语句。先在第一个 BB 处创建 switch 使用的条件变量,然后创建循环的开头 BB,循环结束 BB,switch 的 default 块,最后将他们用 Br 相连起来。
// Create main loop
BasicBlock *loopEntry = ;
BasicBlock *loopEnd = ;
BasicBlock *swDefault = ;
// Create switch variable
IRBuilder<> ;
AllocaInst *swPtr = entryBuilder.;
StoreInst *storeRng =
entryBuilder.;
entryBuilder.;
// Create switch statement
IRBuilder<> ;
LoadInst *swVar = swBuilder.;
SwitchInst *swInst = swBuilder.;
BranchInst *dfTerminator = ;
BranchInst *toLoopEnd = ;
经过处理后的程序控制流图如下
+--------------------------------------+ +--------------------------------------+
| entry: | | tempBB: |
| ... | | compare instruction |
+-------------------+------------------+ | br %cmp_res, label %if, label %else |
| +-------------------+-----------+------+
v v---------------------+ | |
+----------------+-----------------+ | +-------------+ |
| Entry: | | v v
| switch i32 %al, label %Default | | +--------+-------+ +--------+-------+
+----------------+-----------------+ | | if: | | else: |
| | | br label %end | | br label %end |
v---------------+ | +--------+-------+ +--------+-------+
+----------------+ | | |
| Default: | | +-----------+-------------+
| br label %End | | v
+------+---------+ | +--+---+
| | | end: |
+-----------------v | | ... |
+------------------+ | +------+
| End: | |
| br label %Entry | |
+--------+---------+ |
| |
| |
+--------------------+
然后就是重头戏了:将 BB 们放入 switch 的 case 中。首先先将循环的结尾移动到 BB 后,然后再放入 case 中。然后再判断 BB 的结尾的语句有多少个继承块,如果为 0 个的话,说明是返回语句,那么就不需要管;如果是 1 个的话,说明是无条件跳转语句,那么就计算与其相连的下一个块的 case 值,并更新 switch 的条件变量的值;如果为 2 个的话,说明是一个条件跳转,则根据条件语句 SelectInst 决定下一个块执行的位置;如果是其他情况,则保持该 BB 不变。最后更新下初始的 switch 条件变量的值,保证第一个块的执行。
// Put all BB into switch Instruction
for
// Recalculate switch Instruction
for
// Set swVar's origin value, let the first BB executed first
ConstantInt *caseCond = swInst->;
storeRng->;
图请参考 Obfuscator-llvm源码分析,用 ASCII 画图有点麻烦...
总结
这个控制流平坦化的代码数量也不多,而且程序的逻辑在理清后很容易理解,所以文章的篇幅很短。