关于ollvm

llvm

llvm包括许多内容, 这里只粗略介绍

LLVM(Low Level Virtual Machine)是苹果公司的开源编译器框架(最初是由Chris Lattner在2000年开发)

LLVM的架构采用了前后端分离的设计:

  • 前端:负责将源代码转换为LLVM IR。

    LLVM支持多种编程语言,如C、C++、Rust、Swift等。LLVM本身并不包括一个通用的编程语言前端,而是依赖于其他项目(如Clang、Rust的编译器等)提供前端功能。

  • 后端:将LLVM IR转换为目标机器代码,支持多种不同的架构(如x86、ARM、RISC-V等)。

    后端负责优化和生成最终的机器代码。

LLVM的一个核心概念是它的中间表示(Intermediate Representation,IR)。LLVM IR 是一种类似汇编语言的低级表示,它具有三个形式:

  • LLVM位码(LLVM Bitcode):一种二进制格式,通常用于文件存储或传输。
  • LLVM汇编语言:一种文本格式,便于阅读和调试。
  • LLVM机器代码:最终生成的目标机器代码。

LLVM IR非常接近汇编语言,但具有更多的抽象,可以在多种平台之间进行优化和转换。

ollvm

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度

安装

1
2
3
4
git clone -b llvm-4.0 --depth=1 https://github.com/obfuscator-llvm/obfuscator.git
sudo docker pull nickdiego/ollvm-build
git clone --depth=1 https://github.com/oacia/docker-ollvm.git
sudo docker-ollvm/ollvm-build.sh obfuscator/

使用docker进行编译, 编译需要一段时间

安装完成后即可在obfuscator/build_release/bin/目录下找到编译结果

之后我们选择一个小小的demo

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
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>

int check_password(const char* input) {
int sum = 0;
for (int i = 0; input[i] != '\0'; i++) {
sum += input[i];
}
return sum == 1000;
}

int complex_calculation(int a, int b) {
int result = 0;
if (a > b) {
result = a * b + (a ^ b);
} else {
result = a + b * (a & b);
}
return result;
}

void print_message() {
const char* msg = "Hello, OLLVM!";
printf("%s\n", msg);
}

int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
return 1;
}

if (check_password(argv[1])) {
printf("Password correct!\n");
} else {
printf("Password wrong!\n");
}

int x = complex_calculation(10, 20);
printf("Calculation result: %d\n", x);

print_message();

return 0;
}

BCF(虚假控制流)

虚假控制流混淆主要通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编

也就是说一些跳转在运行之前就已经可以确定, 但IDA等工具却无法分析

例如

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}

混淆前

混淆后

混淆

使用ollvm对程序进行BCF混淆

1
clang -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40 test.c -o bcf
  • -mllvm -bcf: 激活虚假控制流
  • -mllvm -bcf_loop=3 : 混淆次数,这里一个函数会被混淆 3 次,默认为 1
  • -mllvm -bcf_prob=40 : 每个基本块被混淆的概率,这里每个基本块被混淆的概率为 40%,默认为 30 %

查看混淆后的结果

可以看到这个表达式y_11 >= 10 && ((((_BYTE)x_10 - 1) * (_BYTE)x_10) & 1) != 0

观察到(((_BYTE)x_10 - 1) * (_BYTE)x_10),也就是x * (x- 1),这个永远是一个偶数, 那么偶数在二进制的表示最低位肯定是0, 所以这个条件永远也不可能满足

但因为y_11x_10是变量, IDA无法确定他们的值, 因此在反编译结果中保留了这部分代码

反混淆

消除不透明谓词

两种方案

第一种: 将mov 寄存器, 不透明谓词全部改为mov 寄存器, 0

样例脚本

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
# 去除虚假控制流 idapython 脚本
import ida_xref
import ida_idaapi
from ida_bytes import get_bytes, patch_bytes

# 将 mov 寄存器,不透明谓词 修改为 mov 寄存器,0
def do_patch(ea):
if get_bytes(ea, 1) == b"\x8B": # mov eax-edi, dword
reg = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
patch_bytes(ea, (0xB8 + reg).to_bytes(1,'little') + b'\x00\x00\x00\x00\x90\x90')
else:
print('error')

# 不透明谓词在.bss 段的范围
seg = ida_segment.get_segm_by_name('.bss')
start = seg.start_ea
end = seg.end_ea

for addr in range(start,end,4):
ref = ida_xref.get_first_dref_to(addr)
print(hex(addr).center(20,'-'))
# 获取所有交叉引用
while(ref != ida_idaapi.BADADDR):
do_patch(ref)
print('patch at ' + hex(ref))
ref = ida_xref.get_next_dref_to(addr, ref)
print('-' * 20)

第二种: 将.bss段改为只读, 并且逐个对不透明谓词进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

模拟执行

使用angr或unicorn等模拟执行工具, 标记不可达块, 再将其nop

例如工具deflat)

d810

d810)是一个十分强大的IDA反混淆插件

安装后在Edit->plugins->D-810打开插件后, 选择规则, 点击start, 然后再次反编译函数即可

d810包含许多默认规则

适用于去虚假控制流, 去指令替换, 去平坦化等情况

d810添加规则

如果有配置文件不包含的情况出现, 可以额外添加配置

打开对应的配置文件\plugins\d810\conf\xxx.json

ins_rules属性添加一个成员

1
2
3
4
5
{
"name": "newrule",
"is_activated": true,
"config": {}
},

然后在根据具体的代码找到plugins\d810\optimizers\instructions\pattern_matching\rewrite_xxx.py, 新增对应的类

FLA(控制流平坦化)

控制流平坦化,主要通过一个主分发器来控制程序基本块的执行流程。该方法将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系。

image-20250414192801135

  • 序言:函数的第一个执行的基本块 主 (子)
  • 分发器:控制程序跳转到下一个待执行的基本块
  • retn 块:函数出口
  • 真实块:混淆前的基本块,程序真正执行工作的块
  • 预处理器:跳转到主分发器

例如一个程序源代码是这样

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}

平坦化后就会变成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
int b = 0;
while(1) {
switch(b) {
case 0:
if(a == 0)
b = 1;
else
b = 2;
break;
case 1:
return 1;
case 2:
return 10;
default:
break;
}
}
return 0;
}

混淆

实现流程

  • 添加一个随机数种子 blockID
  • 保存所有的基本块
  • 将代码中含有switch改为if
  • 删除第一个基本块,第一个需要特殊处理
  • 识别main中的if,并且删除跳转指令
  • 插入一个switch指令
  • 第一个块跳转到loopEntry块
  • 把所有的block保存到switch语句
  • 重新计算switch变量的值
  • 处理不是条件跳转 直接删除jump 跳转到loopEnd 进行下一轮循环
  • 处理条件跳转 对真分支和假分支进行相应处理 真则选择真的ID

如果源代码含有switch, 那么先将里面含switch的改为if-else,再将所有的if-else变为Switch的结果,所以多次进行控制流平坦化就会变得越来越复杂

使用ollvm进行平坦化混淆

1
clang -mllvm -fla -mllvm -split -mllvm -split_num=3 test.c -o fla
  • -mllvm -fla : 激活控制流扁平化
  • -mllvm -split : 激活基本块拆分。与激活时结合使用可提高扁平化效果。
  • -mllvm -split_num=3 : 如果激活此传递,则对每个基本块应用 3 次。默认:1

对于我们的demo, 混淆结果如下

反混淆

反混淆最重要的肯定区分所有的基本快

  • 找到序言块,这是整个函数的入口
  • 序言块的后继是主分发器
  • 主分发器的前驱有两个,除了序言块外,另一个块就是预处理器
  • 预处理器的前驱是真实块
  • 除此之外的其他块是子分发器

d810

d810同样支持去平坦化, 选择规则default_unflatteing_ollvm点击start, 然后再次编译即可

模拟执行

工具deflat)

SUB(指令替换)

这种混淆技术的目标简单来说就是用功能等效但更复杂的指令序列来替换标准二进制运算符(如加法、减法或布尔运算符)。当有多个等效指令序列可供选择时,随机选择一个。

这种混淆方法相对简单,增加的安全性不多,因为它可以通过重新优化生成的代码轻松移除。然而,如果伪随机数生成器的种子值不同,指令替换会在生成的二进制文件中引入多样性。

目前只应用于整型, 如果是浮点数, 可能因为运算符的替换而导致舍入的误差和不必要的数值不精确

混淆

1
clang -mllvm -sub -mllvm -sub_loop=3 test.c -o sub
  • -mllvm -sub : 激活指令替换
  • -mllvm -sub_loop=3: 如果该 Pass 被激活,则对一个函数应用它 3 次。默认:1

demo启用SUB后混淆效果如下

相对原本

1
2
3
4
5
6
7
8
9
int complex_calculation(int a, int b) {
int result = 0;
if (a > b) {
result = a * b + (a ^ b);
} else {
result = a + b * (a & b);
}
return result;
}

确实复杂了许多

反混淆

d810

又是神奇的d810

使用后可以看到少了许多

但依然还存在一些

gooMBA

IDA自带的一个去混淆插件, 不过只能处理一些简单情况, 对ollvm产生的混淆效果并不太好

GAMBA

同样是去混淆, 不过输入基于表达式

HexRaysSA/goomba: gooMBA is a Hex-Rays Decompiler plugin to simplify Mixed Boolean-Arithmetic (MBA) expressions