ollvm 算是日常逆向的过程中的一个难点,试想一下当你把程序拖进 ida 后那无穷无尽的 block 出现在面前的感受,这滋味一言难尽呐…

所以很有必要对 ollvm 的混淆与反混淆进行系统的学习,以便在未来实际生活中遇到时,不必慌忙的去 google 寻找答案.

# 预备知识

llvm 是一个完整的编译器架构,作用可以理解为制作一个编译器,llvm 先将源码生成为与目标机器无关的 LLVM IR 代码,然后把 LLVMIR 代码先优化,再向目标机器的汇编语言而努力。经典编译器都可以分为前端、中层优化和后端:

image-20230726095250772

从上图中可以看到 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 混淆之后,函数的控制流明显复杂了许多

image-20230727162443249

打开 BCF 混淆之后 IDA 的伪代码,发现多了许多的 while , if 表达式,伪代码变得十分复杂,也让我们无法一眼就可以看出这是何种加密

image-20230727162652645

# 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 的地址

image-20230727185741834

然后按下 Alt+S 或者 Edit->Segments->Edit segment... 来改变不透明谓词所在的 segment 的读写属性,如图将 Write 复选框取消勾选, .bss段 就设为只读了

image-20230727203726728

光是这样还不够,因为 .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 ) 的汇编

image-20230727230937099

我们重点关注 mov eax, ds:x_9 这行汇编,它的作用是将变量 x_9 的值赋给寄存器 eax

# 参考资料

  • ollvm 快速学习

  • 跟着铁头干混淆 3 ubuntu 下用 docker 编译 ollvm (保证成功)

  • OLLVM 混淆学习(0)—— 环境搭建及混淆初体验

  • 利用 angr 符号执行去除虚假控制流

更新于 阅读次数