作者:bird@tsrc
1. 背景
1.1 控制流平坦化
控制流平坦化(control flow flattening)的基本思想主要是通过一个主分发器来控制程序基本 块的执行流程,例如下图是正常的执行流程
经过控制流平坦化后的执行流程就如下图
这样可以模糊基本块之间的前后关系,增加程序分析的难度,同时这个流程也很像VM的执行 流程。更多控制流平坦化的细节可以看Obfuscating C++ programs via control flow flattening,本文以 Obfuscator-LLVM 的控制流平坦化为例。
1.2 符号执行
符号执行 是一种重要的形式化方法和软件分析技术,通过使用符号执行技术,将程序中变量的值表示为符号值和常量组成的计算表达式,符号是指取值集合的记号,程序计算的输出被表示为输入符号值的函数,其在软件测试和程序验证中发挥着重要作用,并可以应用于程序漏洞的检测。
符号执行的发展是从静态符号执行到动态符号执行到选择性符号执行,动态符号执行会以具体 数值作为输入来模拟执行程序,是混合执行(concolic execution)的典型代表,有很高的精确度,目前较新的符号执行工具有Triton和angr,本文是以angr为例。
2. 分析
首先写一个简单的示例程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_password(char *passwd) {
int i, sum = 0;
for (i = 0; ; i++) {
if (!passwd[i]) {
break;
}
sum += passwd[i];
}
if (i == 4) {
if (sum == 0x1a1 && passwd[3] > 'c' && passwd[3] < 'e' &&passwd[0] == 'b') {
if ((passwd[3] ^ 0xd) == passwd[1]) {
return 1;
}
puts("Orz...");
}
}
else{
puts("len error");
}
return 0;
}
int main(int argc, char **argv) {
if (argc != 2){
puts("error");
return 1;
}
if (check_password(argv[1])){
puts("Congratulation!");
}
else{
puts("error");
}
return 0;
}
编译
gcc check_passwd.c -o check_passwd
用IDA查看未经过控制流平坦化的控制流程图(CFG)
添加控制流平坦化
build/bin/clang check_passwd.c -o check_passwd_flat -mllvm -fla
可以看到控制流平坦化后的CFG非常漂亮
通过分析可以发现原始的执行逻辑只在真实块(自己想的名称...)以及序言和retn块中,其中会
产生分支的真实块中主要是通过CMOV指令来控制跳转到哪一个分支,因此只要确定这些块的 前后关系就可以恢复出原始的CFG,这个思路主要是参考Deobfuscation: recovering an OLLVM-protected program。
3. 实现
3.1 获取真实块、序言、retn块和无用块
由于angr的CFG跟IDA的有点不同,因此本文使用BARF来获取,后来问了Fish Wang可以 用angr-management 下的to_supergraph来获取。主要思路:
- 函数的开始地址为序言的地址
- 序言的后继为主分发器
- 后继为主分发器的块为预处理器
- 后继为预处理器的块为真实块
- 无后继的块为retn块
- 剩下的为无用块
主要代码:
3.2 确定真实块、序言和retn块的前后关系
这个步骤主要是使用符号执行,为了方便,这里把真实块、序言和retn块统称为真实块,符号 执行从每个真实块的起始地址开始,直到执行到下一个真实块。如果遇到分支,就改变判断值 执行两次来获取分支的地址,这里用angr的inspect在遇到类型为ITE的IR表达式时,改变临时 变量的值来实现,例如下面这个块
使用statement before类型的inspect:
修改临时变量28为false或true再执行就可以得到分支的地址
t48 = ITE(t28,0xca2df6de,0xaf59b039)
如果遇到call指令,使用hook的方式直接返回
主要代码:
3.3 Patch二进制程序
首先把无用块都改成nop指令
然后针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块
针对产生分支的真实块把CMOV指令改成相应的条件跳转指令跳向符合条件的分支,例
如 CMOVZ
改成 JZ
,再在这条之后添加 JMP 指令跳向另一分支
上述就是去除控制流平坦化的总体实现思路。
4. 演示
去除制定函数的控制流平坦化
python deflat.py check_passwd_flat 0x400530
用IDA查看恢复后的CFG
可以看到CFG跟原来的大致一样,然后反编译恢复出原始代码
5. 总结
本文主要针对 x86 架构下 Obfuscator-LLV M的控制流平坦化,但最重要的是去除控制流平坦化 过程中的思路,同时当函数比较复杂时可能速度会有点慢。有时间会以此为基础尝试分析伪造 控制流、指令替换和 VM 等软件保护手段,另外符号执行也可以应用于漏洞挖掘领域,例如借 助符号执行生成覆盖率更高的Fuzzing测试集以及求解达到漏洞点的路径等。由于小弟刚学习 符号执行,可能有理解错误的地方,欢迎研究符号执行或者认为有更好思路的师傅们吐槽。。 。最后,感谢 angr 主要开发者 Fish Wang 在这期间的耐心帮助。
6. 参考
- Obfuscating C++ programs via control flow flattening
- https://github.com/obfuscator-llvm/obfuscator/tree/llvm-3.6.1
- Symbolic Execution and Program Testing
- Selective Symbolic Execution
- CUTE: A Concolic Unit Testing Engine for C
- https://github.com/JonathanSalwan/Triton
- https://github.com/angr/angr
- http://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html
- https://github.com/programa-stic/barf-project
- https://github.com/angr/angr-management
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/192/
暂无评论