关于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位码(.bc):一种二进制格式,通常用于文件存储或传输, 它体积小、解析快,适合在编译器内部和分布式编译中传输或存储中间表示。
  • 文本形式(.ll):一种文本格式,便于阅读和调试,类似于汇编语言,包含指令、函数、基本块、变量等信息。
  • 内存形式:内存形式是LLVM IR在内存中的数据结构表示,供LLVM的各种工具和优化器使用

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, 新增对应的类

源码分析

源码在于/lib/Transforms/Obfuscation/BogusControlFlow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  virtual bool runOnFunction(Function &F){
// Check if the percentage is correct
if (ObfTimes <= 0) {
errs()<<"BogusControlFlow application number -bcf_loop=x must be x > 0";
return false;
}

// Check if the number of applications is correct
if ( !((ObfProbRate > 0) && (ObfProbRate <= 100)) ) {
errs()<<"BogusControlFlow application basic blocks percentage -bcf_prob=x must be 0 < x <= 100";
return false;
}
// If fla annotations
if(toObfuscate(flag,&F,"bcf")) {
bogus(F);
doF(*F.getParent());
return true;
}

return false;
} // end of runOnFunction()

runOnFunction是拓展的pass类必须实现的入口

主要是检查了一下传入的混淆次数和概率是否合规, 然后使用toObfuscate判断是否传入了开启bcf的参数

接着就进入真正的混淆过程bogus

1
2
3
4
5
6
7
8
void bogus(Function &F) {
// For statistics and debug
++NumFunction;
int NumBasicBlocks = 0;
bool firstTime = true; // First time we do the loop in this function
bool hasBeenModified = false;
DEBUG_WITH_TYPE("opt", errs() << "bcf: Started on function " << F.getName() << "\n");
DEBUG_WITH_TYPE("opt", errs() << "bcf: Probability rate: "<< ObfProbRate<< "\n");

NumFunction用于记录处理过的函数数量,每调用一次 bogus 函数就会增加。

NumBasicBlocks用于统计当前函数中的基本块数量。

firstTime表示是否是第一次迭代该函数。在第一次迭代时进行一些额外的初始化操作。

hasBeenModified记录该函数是否经过修改(即是否插入了虚假的控制流)。

1
2
3
4
5
6
7
8
9
10
11
12
NumTimesOnFunctions = ObfTimes;
int NumObfTimes = ObfTimes;
do {
DEBUG_WITH_TYPE("cfg", errs() << "bcf: Function " << F.getName()
<<", before the pass:\n");
DEBUG_WITH_TYPE("cfg", F.viewCFG());
// Put all the function's block in a list
std::list<BasicBlock *> basicBlocks;
for (Function::iterator i = F.begin(); i != F.end(); ++i) {
basicBlocks.push_back(&*i);
}
DEBUG_WITH_TYPE("gen", errs() << "bcf: Iterating on the Function's Basic Blocks\n");

主循环对于每个函数,会执行 ObfTimes 次混淆操作。每次循环都会对该函数的基本块进行遍历。

获取基本块通过遍历函数中的所有基本块,将它们存储在 basicBlocks 列表中。之后对这些基本块进行处理。

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
while(!basicBlocks.empty()){
NumBasicBlocks++;
// Basic Blocks' selection
if((int)llvm::cryptoutils->get_range(100) <= ObfProbRate){
DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
<< NumBasicBlocks <<" selected. \n");
hasBeenModified = true;
++NumModifiedBasicBlocks;
NumAddedBasicBlocks += 3;
FinalNumBasicBlocks += 3;
// Add bogus flow to the given Basic Block (see description)
BasicBlock *basicBlock = basicBlocks.front();
addBogusFlow(basicBlock, F);
}
else{
DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
<< NumBasicBlocks <<" not selected.\n");
}
// remove the block from the list
basicBlocks.pop_front();

if(firstTime){ // first time we iterate on this function
++InitNumBasicBlocks;
++FinalNumBasicBlocks;
}
}
DEBUG_WITH_TYPE("gen", errs() << "bcf: End of function " << F.getName() << "\n");
if(hasBeenModified){ // if the function has been modified
DEBUG_WITH_TYPE("cfg", errs() << "bcf: Function " << F.getName()
<<", after the pass: \n");
DEBUG_WITH_TYPE("cfg", F.viewCFG());
}
else{
DEBUG_WITH_TYPE("cfg", errs() << "bcf: Function's not been modified \n");
}
firstTime = false;
} while(--NumObfTimes > 0);

遍历基本块每次循环选择一个基本块进行处理。选择的条件是一个随机数(由 llvm::cryptoutils->get_range(100) 生成)是否小于等于 ObfProbRate,这决定了某个基本块是否被选择进行混淆。

如果基本块被选中,调用 addBogusFlow 函数在该基本块中插入虚假的控制流。addBogusFlow 是实际执行插入伪流逻辑的地方

观察addBogusFlow函数

1
2
3
4
5
6
7
BasicBlock::iterator i1 = basicBlock->begin();
if(basicBlock->getFirstNonPHIOrDbgOrLifetime())
i1 = (BasicBlock::iterator)basicBlock->getFirstNonPHIOrDbgOrLifetime();
Twine *var;
var = new Twine("originalBB");
BasicBlock *originalBB = basicBlock->splitBasicBlock(i1, *var);
DEBUG_WITH_TYPE("gen", errs() << "bcf: First and original basic blocks: ok\n");

首先,获取基本块 basicBlock 中的第一个非 PHI 节点、调试信息或者生命周期指令的位置。如果存在这样的节点,将拆分操作从该位置开始。

调用 splitBasicBlock 方法将原始基本块 basicBlock 拆分为两个部分。splitBasicBlock返回切割的后半部分, 前半部分则存储在原来的指针

第一部分(basicBlock):只保留 PHI 节点(用于变量传递)、调试信息等,不包含实际指令。

第二部分(originalBB):包含原来的所有代码。

1
2
3
Twine * var3 = new Twine("alteredBB");
BasicBlock *alteredBB = createAlteredBasicBlock(originalBB, *var3, &F);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Altered basic block: ok\n");

alteredBBoriginalBB 的修改版本,可能包含一些无用的指令或变形后的代码。看起来像是程序可能执行的分支,但实际上不会真正运行(因为条件总是跳转到 originalBB)。

createAlteredBasicBlock 函数返回一个与给定基本块类似的基本块,插入到给定基本块的紧后面与原始基本块链接。实现就不专门解析了

1
2
3
4
alteredBB->getTerminator()->eraseFromParent();
basicBlock->getTerminator()->eraseFromParent();
DEBUG_WITH_TYPE("gen", errs() << "bcf: Terminator removed from the altered"
<<" and first basic blocks\n");

移除原来的跳转指令(Terminator),准备插入新的控制流

1
2
3
4
5
6
7
Value * LHS = ConstantFP::get(Type::getFloatTy(F.getContext()), 1.0);
Value * RHS = ConstantFP::get(Type::getFloatTy(F.getContext()), 1.0);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Value LHS and RHS created\n");

Twine * var4 = new Twine("condition");
FCmpInst * condition = new FCmpInst(*basicBlock, FCmpInst::FCMP_TRUE , LHS, RHS, *var4);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Always true condition created\n");

建了一个浮动类型的常量 LHSRHS,它们的值都是 1.0。随后,创建了一个 FCmpInst(浮动比较指令),用于比较 LHSRHS。由于这两个值相等,因此条件永远为 true, 也就是总是走固定路径

1
2
3
BranchInst::Create(originalBB, alteredBB, (Value *)condition, basicBlock);
DEBUG_WITH_TYPE("gen",
errs() << "bcf: Terminator instruction in first basic block: ok\n");

basicBlock 的末尾 现在变成一个 虚假的条件跳转:

如果 conditiontrue → 跳转到 originalBB(真实代码)。

如果 conditionfalse → 跳转到 alteredBB(假代码)。

但由于 condition 永远为 true,所以 alteredBB 永远不会执行,只是用来迷惑逆向分析者。

1
2
BranchInst::Create(originalBB, alteredBB);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Terminator instruction in altered block: ok\n");
  • alteredBB 的末尾 直接跳回 originalBB,形成一个看似循环的结构。
1
2
3
4
5
6
BasicBlock::iterator i = originalBB->end();
Twine * var5 = new Twine("originalBBpart2");
BasicBlock * originalBBpart2 = originalBB->splitBasicBlock(--i , *var5);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Terminator part of the original basic block"
<< " is isolated\n");
originalBB->getTerminator()->eraseFromParent();

originalBB 再切一次,分成:

originalBB(前半部分)。

originalBBpart2(后半部分,主要是原来的终止指令)

1
2
3
4
Twine * var6 = new Twine("condition2");
FCmpInst * condition2 = new FCmpInst(*originalBB, CmpInst::FCMP_TRUE , LHS, RHS, *var6);
BranchInst::Create(originalBBpart2, alteredBB, (Value *)condition2, originalBB);
DEBUG_WITH_TYPE("gen", errs() << "bcf: Terminator original basic block: ok\n");

再插入一个虚假条件, 和之前一样,condition2 也 永远为 true,所以 originalBB 总是跳转到 originalBBpart2(真实代码)。

alteredBB 仍然只是摆设,不会真正执行

最终控制流结构

1
2
3
4
5
6
7
8
9
basicBlock (初始块)

├─ (condition=true) → originalBB (真实代码)
│ │
│ ├─ (condition2=true) → originalBBpart2 (继续执行)
│ └─ (condition2=false) → alteredBB (假代码,不会执行)

└─ (condition=false) → alteredBB (假代码,不会执行)
└──→ originalBB (跳回去)

实际执行路径(永远走 true 分支):

1
basicBlock → originalBB → originalBBpart2 → 继续执行

虚假路径(迷惑逆向分析者):

1
basicBlock → alteredBB → originalBB → ...

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)

源码分析

源码在于/lib/Transforms/Obfuscation/Flattening.cpp

1
2
3
4
5
6
7
8
9
10
11
bool Flattening::runOnFunction(Function &F) {
Function *tmp = &F;
// Do we obfuscate
if (toObfuscate(flag, tmp, "fla")) {
if (flatten(tmp)) {
++Flattened;
}
}

return false;
}

runOnFunction判断是否开启了fla便进入真正的混淆过程

1
2
3
4
5
char scrambling_key[16];
llvm::cryptoutils->get_bytes(scrambling_key, 16);

FunctionPass *lower = createLowerSwitchPass();
lower->runOnFunction(*f);

生成16字节的随机密钥,用于后续 case 值的混淆, 并将switch语句转为if-else语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 for (Function::iterator i = f->begin(); i != f->end(); ++i) {
BasicBlock *tmp = &*i;
origBB.push_back(tmp);

BasicBlock *bb = &*i;
if (isa<InvokeInst>(bb->getTerminator())) {
return false;
}
}
// Nothing to flatten
if (origBB.size() <= 1) {
return false;
}
// Remove first BB
origBB.erase(origBB.begin());

收集所有基本块, 但如果存在异常调用相关(InvokeInst 是一个特殊的调用指令,它用于异常处理机制)则不对该函数进行平坦化操作

如果基本快数量小于等于1, 那么就没有必要平坦化

1
2
3
4
5
// Get a pointer on the first BB
Function::iterator tmp = f->begin(); //++tmp;
//....
// Remove jump
insert->getTerminator()->eraseFromParent();

获取函数的第一个基本块(f->begin()), 检查该块的终止指令

如果是条件分支指令(BranchInst),获取其指针, 判断是否满足以下任一条件:

  • 是条件分支(br->isConditional())
  • 有多个后继块(getNumSuccessors() > 1)

如果满足上述条件,则在适当位置分割基本块, 默认在终止指令前分割, 如果块中有其他指令(size() > 1),则在倒数第二条指令前分割

将分割出的新块(tmpBB)加入原始块列表(origBB)的头部

无论是否分割,最终都会移除入口块的终止指令

1
2
3
4
5
6
switchVar =
new AllocaInst(Type::getInt32Ty(f->getContext()), 0, "switchVar", insert);
new StoreInst(
ConstantInt::get(Type::getInt32Ty(f->getContext()),
llvm::cryptoutils->scramble32(0, scrambling_key)),
switchVar, insert);

在栈上分配4字节内存空间作为switchvar, 同时用之前的key加密0并存储在switchvar

1
2
loopEntry = BasicBlock::Create(f->getContext(), "loopEntry", f, insert);
loopEnd = BasicBlock::Create(f->getContext(), "loopEnd", f, insert);

在insert块之后创建基本块loopEntry, loopEnd

1
load = new LoadInst(switchVar, "switchVar", loopEntry);

在loopEntry起始位置插入load指令, 读取switchVar的当前值, 结果用于后续switch条件判断

1
2
3
4
5
insert->moveBefore(loopEntry);
BranchInst::Create(loopEntry, insert);
BranchInst::Create(loopEntry, loopEnd);
BasicBlock *swDefault = BasicBlock::Create(..., "switchDefault", f, loopEnd);
BranchInst::Create(loopEnd, swDefault);

将原入口块insert移到loopEntry之前, 并在原入口块末尾插入无条件跳转,跳转到loopEntry, 再使loopEnd块末尾无条件跳回loopEntry

创建名为”switchDefault”的默认case块, 该块直接跳转至loopEnd(理论上不可达)

1
2
3
4
switchI = SwitchInst::Create(&*f->begin(), swDefault, 0, loopEntry);
switchI->setCondition(load);
f->begin()->getTerminator()->eraseFromParent();
BranchInst::Create(loopEntry, &*f->begin());

创建switch指令将之前加载的switchVar值(load)设为判断条件

移除函数第一个块的原始终止指令, 插入新的无条件跳转,强制进入loopEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (vector<BasicBlock *>::iterator b = origBB.begin(); b != origBB.end();
++b) {
BasicBlock *i = *b;
ConstantInt *numCase = NULL;

// Move the BB inside the switch (only visual, no code logic)
//将基本块从原位置移到loopEnd之前
i->moveBefore(loopEnd);

// Add case to switch
// Case值生成
numCase = cast<ConstantInt>(ConstantInt::get(
switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(switchI->getNumCases(), scrambling_key)));
//Switch注册
switchI->addCase(numCase, i);
}

物理重组基本块位置(视觉混淆), 将每个基本块注册为switch语句的一个case, 并为其分配自己的case值

1
2
3
4
5
6
7
8
9
10
11
// Recalculate switchVar
for (vector<BasicBlock *>::iterator b = origBB.begin(); b != origBB.end();
++b) {
BasicBlock *i = *b;
ConstantInt *numCase = NULL;

// Ret BB

if (i->getTerminator()->getNumSuccessors() == 0) {
continue;
}

在往后的部分完成控制流状态机的动态调度逻辑,通过三种方式重构基本块终结指令

  1. 处理返回块(无后继块)
  2. 转换无条件跳转(单后继)
  3. 转换条件分支(双后继)

这一块针对返回块保留原始指令不变

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
// If it's a non-conditional jump
if (i->getTerminator()->getNumSuccessors() == 1) {
// Get successor and delete terminator
// 得到后继块, 并删除当前块的终结指令
BasicBlock *succ = i->getTerminator()->getSuccessor(0);
i->getTerminator()->eraseFromParent();

// Get next case
// 在switch指令中查找目标块的加密case值
numCase = switchI->findCaseDest(succ);

// If next case == default case (switchDefault)
if (numCase == NULL) {
numCase = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}

// Update switchVar and jump to the end of loop
// 存储下一状态值后统一跳转回调度器
new StoreInst(numCase, load->getPointerOperand(), i);
BranchInst::Create(loopEnd, i);
continue;
}

这部分代码的转换类似如下

1
2
3
4
5
6
7
8
9
10
原始结构:
BBx:
...
br label %BBy

转换后:
BBx:
...
store i32 encryptedCase, i32* %switchVar
br label %loopEnd

主要针对单后继情况

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
// If it's a conditional jump
if (i->getTerminator()->getNumSuccessors() == 2) {
// Get next cases
//得到两种条件的后继块
ConstantInt *numCaseTrue =
switchI->findCaseDest(i->getTerminator()->getSuccessor(0));
ConstantInt *numCaseFalse =
switchI->findCaseDest(i->getTerminator()->getSuccessor(1));

// Check if next case == default case (switchDefault)
if (numCaseTrue == NULL) {
numCaseTrue = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}

if (numCaseFalse == NULL) {
numCaseFalse = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}

// Create a SelectInst
// 在该块的终结指令处创建三元选择器,动态计算下一状态
// 保证分支正确处理
BranchInst *br = cast<BranchInst>(i->getTerminator());
SelectInst *sel =
SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "",
i->getTerminator());

// Erase terminator
i->getTerminator()->eraseFromParent();

// Update switchVar and jump to the end of loop
// 所有分支最终收敛到loopEnd
new StoreInst(sel, load->getPointerOperand(), i);
BranchInst::Create(loopEnd, i);
continue;
}

针对条件分支的后继, 完成工作如下

1
2
3
4
5
6
7
8
9
10
原始结构:
BBx:
...
br i1 %cond, label %TrueBB, label %FalseBB

转换后:
BBx:
%nextState = select i1 %cond, i32 encryptedTrueCase, i32 encryptedFalseCase
store i32 %nextState, i32* %switchVar
br label %loopEnd
1
fixStack(f);

最后调用fixstack用于修复phi指令, 所谓phi指令就是llvm用于控制流合并点动态选择变量的指令

它主要用于解决 基本块之间的变量依赖问题,确保 SSA静态单赋值(在 SSA 形式中,每个变量只能被赋值一次)形式的正确性

PHI 指令在 控制流合并点(如前趋块有多个来源) 动态选择变量的值:

1
2
; 假设前趋块是 %entry 和 %else
%result = phi i32 [ 1, %entry ], [ 2, %else ]
  • %result 的值取决于执行路径:
    • 如果来自 %entry,则 %result = 1
    • 如果来自 %else,则 %result = 2

因为phi指令依赖于其前驱块, 所以必须位于基本块的开头, 且每个 [value, label] 对必须指向 当前基本块的前趋块(Predecessor)

1
2
3
4
5
6
7
; 正确:
block:
%x = phi i32 [ 0, %entry ], [ 1, %loop ] ; %entry 和 %loop 必须跳转到 %block

; 错误:
block:
%x = phi i32 [ 0, %wrong_block ] ; %wrong_block 没有跳转到 %block

也就是说phi指令的工作依赖于前驱块, 但在控制流平坦化中基本快之间的前后关系被破坏了, 所以需要想办法修复

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
void fixStack(Function *f) {
// Try to remove phi node and demote reg to stack
std::vector<PHINode *> tmpPhi;
std::vector<Instruction *> tmpReg;
BasicBlock *bbEntry = &*f->begin();

do {
tmpPhi.clear();
tmpReg.clear();

for (Function::iterator i = f->begin(); i != f->end(); ++i) {

for (BasicBlock::iterator j = i->begin(); j != i->end(); ++j) {

if (isa<PHINode>(j)) {
PHINode *phi = cast<PHINode>(j);
tmpPhi.push_back(phi);
continue;
}
if (!(isa<AllocaInst>(j) && j->getParent() == bbEntry) &&
(valueEscapes(&*j) || j->isUsedOutsideOfBlock(&*i))) {
tmpReg.push_back(&*j);
continue;
}
}
}
for (unsigned int i = 0; i != tmpReg.size(); ++i) {
DemoteRegToStack(*tmpReg.at(i), f->begin()->getTerminator());
}

for (unsigned int i = 0; i != tmpPhi.size(); ++i) {
DemotePHIToStack(tmpPhi.at(i), f->begin()->getTerminator());
}

} while (tmpReg.size() != 0 || tmpPhi.size() != 0);
}

该函数用于修复控制流平坦化后可能破坏的SSA形式, 主要处理两种特殊情况:

  1. PHI节点跨基本块依赖问题(PHINode)
  2. 寄存器值逃逸问题(Value escaping)

通过将寄存器变量降级为栈变量(alloca/load/store),确保混淆后的IR仍然符合LLVM规范

tmpPhi收集所有的PHI指令节点

1
2
3
4
5
6
7
8
// 使用isa<>/cast<> RTTI机制识别PHI指令
if (isa<PHINode>(j)) {
PHINode *phi = cast<PHINode>(j);
tmpPhi.push_back(phi);
continue;
}
// PHI节点降级
DemotePHIToStack(tmpPhi.at(i), f->begin()->getTerminator());

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 原始PHI
%val = phi i32 [0, %A], [1, %B]

; 降级后
%val.addr = alloca i32
; 在每个前趋块插入store
A:
store i32 0, i32* %val.addr
br label %merge
B:
store i32 1, i32* %val.addr
br label %merge
merge:
%val = load i32, i32* %val.addr

tmpReg收集所有的寄存器逃逸点

1
2
3
4
5
6
7
8
// 非入口块分配的alloca(避免重复处理)
// 值逃逸(通过valueEscapes判断)或 跨块使用(isUsedOutsideOfBlock)
if (!(isa<AllocaInst>(j) && j->getParent() == bbEntry) &&
(valueEscapes(&*j) || j->isUsedOutsideOfBlock(&*i))) {
tmpReg.push_back(&*j);
}
// 将临时寄存器转换为栈变量
DemoteRegToStack(*tmpReg.at(i), f->begin()->getTerminator());

效果

1
2
3
4
5
6
7
; 原始IR
%x = add i32 1, 2

; 降级后
%x.addr = alloca i32
store i32 3, i32* %x.addr
%x = load i32, i32* %x.addr

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