ollvm 算是日常逆向的过程中的一个难点,试想一下当你把程序拖进 ida 后那无穷无尽的 block 出现在面前的感受,这滋味一言难尽呐…
所以很有必要对 ollvm 的混淆与反混淆进行系统的学习,以便在未来实际生活中遇到时,不必慌忙的去 google 寻找答案.
# 预备知识
llvm 是一个完整的编译器架构,作用可以理解为制作一个编译器,llvm 先将源码生成为与目标机器无关的 LLVM IR 代码,然后把 LLVMIR 代码先优化,再向目标机器的汇编语言而努力。经典编译器都可以分为前端、中层优化和后端:
从上图中可以看到 clang 是前端的一个套件,但在实际使用时,我们只可以感受到 clang,也只是在使用 clang,因为编译的时候,是调用 clang 或 clang++ 来编译源码。
而 ollvm 是基于 LLVM 的代码分支的代码混淆,在中间表示 IR 层,通过编写 pass(遍历一遍 IR,可以同时对它做一些操作)来混淆 IR,这样目标机器的汇编语言也就被混淆了
# ollvm 环境搭建
ollvm 搭建的环境为
- ubuntu22.04
# 下载 ollvm 4.0 源码
git clone -b llvm-4.0 --depth=1 https://github.com/obfuscator-llvm/obfuscator.git |
# 安装 docker
sudo apt install docker.io |
# 安装编译 ollvm 的 docker 环境
sudo docker pull nickdiego/ollvm-build |
# 编译 ollvm
# 下载编译脚本
git clone --depth=1 https://github.com/oacia/docker-ollvm.git |
# 编译
ollvm-build.sh
后面跟的参数是 ollvm的源码目录
sudo docker-ollvm/ollvm-build.sh /home/oacia/Desktop/obfuscator/ |
# 创建硬链接
sudo ln ./obfuscator/build_release/bin/* /usr/bin/ |
创建完成硬链接后,使用该命令来检测 clang
是否可用
clang --version |
# 使用测试代码尝试编译
使用一个简单的 RC4 加密来作为测试代码, 该代码将在后续 ollvm 的三种混淆中继续使用
//test.c | |
#include<stdio.h> | |
/* | |
RC4 初始化函数 | |
*/ | |
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len_k) | |
{ | |
int i = 0, j = 0; | |
char k[256] = { 0 }; | |
unsigned char tmp = 0; | |
for (i = 0; i < 256; i++) { | |
s[i] = i; | |
k[i] = key[i % Len_k]; | |
} | |
for (i = 0; i < 256; i++) { | |
j = (j + s[i] + k[i]) % 256; | |
tmp = s[i]; | |
s[i] = s[j]; | |
s[j] = tmp; | |
} | |
} | |
/* | |
RC4 加解密函数 | |
unsigned char* Data 加解密的数据 | |
unsigned long Len_D 加解密数据的长度 | |
unsigned char* key 密钥 | |
unsigned long Len_k 密钥长度 | |
*/ | |
void RC4(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) // 加解密 | |
{ | |
unsigned char s[256]; | |
rc4_init(s, key, Len_k); | |
int i = 0, j = 0, t = 0; | |
unsigned long k = 0; | |
unsigned char tmp; | |
for (k = 0; k < Len_D; k++) { | |
i = (i + 1) % 256; | |
j = (j + s[i]) % 256; | |
tmp = s[i]; | |
s[i] = s[j]; | |
s[j] = tmp; | |
t = (s[i] + s[j]) % 256; | |
Data[k] = Data[k] ^ s[t]; | |
} | |
} | |
void RC4encrypt(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) { | |
RC4(Data, Len_D, key, Len_k); | |
} | |
void RC4decrypt(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) { | |
RC4(Data, Len_D, key, Len_k); | |
} | |
int main() | |
{ | |
// 字符串密钥 | |
unsigned char key[] = "secret"; | |
unsigned long key_len = sizeof(key) - 1;// 字符串最后还有一个 '/0' 所以需要 - 1 | |
// 数组密钥 | |
//unsigned char key[] = {'s','e','c','r','e','t'}; | |
//unsigned long key_len = sizeof(key); | |
unsigned char data[] = { 116, 104, 105, 115, 32, 105, 115, 32, 82, 67, 52, 44, 111, 97, 99, 105, 97 }; | |
// 对明文进行加密 | |
RC4encrypt(data, sizeof(data), key, key_len); | |
for (int i = 0; i < sizeof(data); i++) | |
{ | |
printf("%d, ", data[i]); | |
} | |
printf("\n"); | |
// 对密文进行解密 | |
RC4encrypt(data, sizeof(data), key, key_len); | |
for (int i = 0; i < sizeof(data); i++) | |
{ | |
printf("%c", data[i]); | |
} | |
printf("\n"); | |
return 0; | |
} | |
/* | |
153, 94, 187, 111, 162, 205, 165, 134, 96, 136, 143, 240, 156, 135, 150, 94, 204, | |
this is RC4,oacia | |
*/ |
运行如下命令进行编译
clang test.c -o test
- 如果提示 fatal error: ‘stdio.h’ file not found
尝试下载 g++ 和 gcc
sudo apt-get install g++ | |
sudo apt-get install gcc |
- 如果提示 fatal error: 'stddef.h’或者’stdarg.h’等 file not found
使用该命令复制 clang 所需的头文件到 /usr/include/
, cp -r -i
后面跟的参数为 ollvm的源码目录/build_release/lib/clang/4.0.1/include/.
, 这个文件夹内包含了 clang
编译器所需的头文件
cp -r -i /home/oacia/Desktop/obfuscator/build_release/lib/clang/4.0.1/include/. /usr/include/ |
如果提示有重名文件的话,最好先对 /usr/include
内的重名文件作好备份,然后去掉 -i
参数重新进行复制头文件操作
# 虚假控制流 BCF (Bogus Control Flow)
# 定义
虚假控制流混淆通过加入包含不透明谓词的条件跳转(也就是跳转与否在运行之前就已经确定的跳转,但 IDA 无法分析)和不可达的基本块,来干扰 IDA 的控制流分析和 F5 反汇编。
所谓的不透明谓词,例如
if(x>10 && x<=10){ | |
goto Label1; | |
} |
对于这类表达式,我们可以很明显的看到, x>10 && x<=10
是永假式,所以 goto Label1
这个跳转永远不会被执行,但是对于 IDA 来说可不是这个样子,在静态分析的时候,IDA 并不知道 x
的值是多少,所以说这类虚假控制流就会干扰我们的静态分析.
# ollvm 的 BCF 混淆
使用下列命令对代码进行 BCF 混淆
clang -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40 test.c -o test-bcf |
可用选项:
-mllvm -bcf
: 激活虚假控制流-mllvm -bcf_loop=3
: 混淆次数,这里一个函数会被混淆 3 次,默认为 1-mllvm -bcf_prob=40
: 每个基本块被混淆的概率,这里每个基本块被混淆的概率为 40%,默认为 30 %
可以发现在 BCF 混淆之后,函数的控制流明显复杂了许多
打开 BCF 混淆之后 IDA 的伪代码,发现多了许多的 while
, if
表达式,伪代码变得十分复杂,也让我们无法一眼就可以看出这是何种加密
# ollvm 的 BCF 反混淆
铺垫了这么长的时间,终于要来到本篇文章的第一个有趣的环节,BCF 的反混淆了, 装环境真的好无聊呀😔
我们往上看 while
内的表达式 y_4 >= 10 && (((x_3 - 1) * x_3) & 1) != 0
, 在这个式子中, (x_3 - 1) * x_3)
的值永远为偶数,所以 (x_3 - 1) * x_3) & 1
永远返回 0
, 不等号左边 y_4 >= 10 && (((x_3 - 1) * x_3) & 1)
, 因为是用逻辑与 &&
作为连接词,所以左侧的表达式其实为永假式, y_4 >= 10 && (((x_3 - 1) * x_3) & 1) != 0
永远不成立
对于 BCF, 有 4 种思路可以帮助我们去进行反混淆
# 思路一:将全局变量赋值并将 segment 设为只读
IDA 其实是有死代码消除 (DCE, Dead Code Elimination) 的,但是由于 y_4
, x_3
被定义为了全局变量,在静态分析时,IDA 不知道这个表达式的值是多少,所以 IDA 也不敢轻易的就把这段代码给消除了 (万一把重要的代码也给消除掉了那逆向人员真的要 *** 了)
但是如果我们把这个变量的值定下来,并且将变量所在的 segment
设为 只读
,那这个变量值在没运行前也变不了,IDA 不就可以自己算出来这个表达式的值是多少了嘛,这样那些没有用的跳转 IDA 就可以自动优化了
所以我们先双击 x_3
跳转到 x_3
的地址
然后按下 Alt+S 或者 Edit->Segments->Edit segment...
来改变不透明谓词所在的 segment 的读写属性,如图将 Write
复选框取消勾选, .bss段
就设为只读了
光是这样还不够,因为 .bss段
中的变量还没有被赋过值,所以我们还需要 patch 这个段来固定 .bss段
内变量的值
一个变量一个变量去 patch 显然显得有些麻烦,所以我们可以直接编写 IDApython 脚本来实现一步到位的效果,并且对于常规的 ollvm 的 bcf 混淆来说,bcf 的不透明谓词都是处于 .bss段
中。如果不透明谓词定义在其他段中,将 IDApython 中的代码做出相对应的修改即可
import ida_segment | |
import ida_bytes | |
seg = ida_segment.get_segm_by_name('.bss') | |
for ea in range(seg.start_ea, seg.end_ea,4): | |
ida_bytes.patch_bytes(ea, int(2).to_bytes(4,'little')) | |
''' | |
seg.perm: 由三位二进制数表示,例如一个segment为可读,不可写,不可执行,则seg.perm = 0b100 | |
(seg.perm >> 2)&1: Read | |
(seg.perm >> 1)&1: Write | |
(seg.perm >> 0)&1: Execute | |
''' | |
seg.perm = 0b100 |
# 思路二: patch 不透明谓词的赋值语句汇编
我们可以观察一下这些不透明谓词 (如伪代码为 y_10 < 10 || (((x_9 - 1) * x_9) & 1) == 0
) 的汇编
我们重点关注 mov eax, ds:x_9
这行汇编,它的作用是将变量 x_9
的值赋给寄存器 eax
# 参考资料
-
ollvm 快速学习
-
跟着铁头干混淆 3 ubuntu 下用 docker 编译 ollvm (保证成功)
-
OLLVM 混淆学习(0)—— 环境搭建及混淆初体验
-
利用 angr 符号执行去除虚假控制流