Pinguw
Articles13
Tags4
Categories3

Categories

Archive

使用LLVM编写简单混淆Pass(二)

使用LLVM编写简单混淆Pass(二)

上次使用 LLVM Pass 实现了控制流平坦化混淆,这次实现的是间接跳转(Indirect Call)

原理

效果:主要是起到了一个静态混淆的作用,打断了函数之间的调用链,无法通过交叉引用定位敏感函数,也不能直接看出某处调用的函数是哪个。

基本想法是把所有的正常 calljmp 都改成先获取一个加密的地址,再通过运算解密这个地址,最后跳转到解密的地址去

那么需要定义一个全局数组存储加密后的地址。

实现

收集跳转指令

针对 CallInst

1
auto *call = dyn_cast<CallInst>(&I)

分配索引:

1
2
3
4
unsigned idx = callCounter++;
unsigned pos = std::rand() % maxCallEntries;
while (usedPos.count(pos)) pos = std::rand() % maxCallEntries;
usedPos.insert(pos);

然后修改原始 call 指令。

针对 BranchInst

1
auto *br = dyn_cast<BranchInst>(&I)

分配索引:

由于条件跳转有两个目标地址,则选取两个相邻的槽位存放:

1
2
3
4
unsigned pos = std::rand() % (maxEntries - 2);
while (usedPos.count(pos) || usedPos.count(pos + 1)) pos = std::rand() % (maxEntries - 2);
usedPos.insert(pos); // true
usedPos.insert(pos + 1); // false

然后修改原始 BranchInst 指令。

初始化跳转表

创建一个 LLVM 数组类型用来存储跳转地址表,每次调用先判断是否定义,如果是则直接返回该数组,否则定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (auto* GV = M->getGlobalVariable("bb_jump_table"))
return GV;

ArrayType* arrType = ArrayType::get(intTy, maxEntries);
auto* table = new GlobalVariable(
*M,
arrType,
false,
GlobalValue::PrivateLinkage,
ConstantAggregateZero::get(arrType),
"bb_jump_table"
);
return table;

跳转索引表做同样操作。

设计间接跳转指令

把原地址加密一下,再存到地址表中,然后在 call 或 jmp 前从表中取出数据,还原地址,然后跳转。

加密逻辑使用加减,计算加密地址:

1
2
3
4
5
6
7
8
9
uint64_t mask = 0x200000 + (((uint64_t)std::rand() << 32) | std::rand()) % 0x200000;
switch (rand_num) {
case 0:
encPtrConst = ConstantExpr::getAdd(encPtrConst, ConstantInt::get(ptrValueType, mask));
break;
case 1;
encPtrConst = ConstantExpr::getSub(encPtrConst, ConstantInt::get(ptrValueType, mask));
break;
}

解密与加密相反即可。

替换原指令

针对 CallInst

获取对应的 idx 后,从表中取出地址,生成指令:

1
2
3
Value* runtimeIdx = irb.CreateLoad(idxPtr); // mov r1, IdxArray[idx]
Value* gep = irb.CreateInBoundsGEP(JumpTable->getValueType(), JumpTable, {irb.getInt64(0), runtimeIdx});
Value* encFuncPtr = irb.CreateLoad(ptrValueType, gep); // mov r2, JumpTable[r1]

最后安装加密的解密方法生成解密指令:

1
2
3
4
5
6
7
8
9
Value* realFuncPtr = nullptr;
switch (rand_num) {
case 0:
realFuncPtr = irb.CreateSub(encFuncPtr, irb.getInt64(mask));
break;
case 1:
realFuncPtr = irb.CreateAdd(encFuncPtr, irb.getInt64(mask));
break;
}

最后添加 call reg 即可。

针对 BranchInst

加解密方式与 call 相同,区别在需要先根据条件选择索引

1
2
3
4
Value* sel = irb.CreateSelect(cond, posVal, posVal2);
// 用 sel 作为索引读取表
Value* gep = irb.CreateInBoundsGEP(BBTable->getValueType(), BBTable, { irb.getInt64(0), sel });
Value* encVal = irb.CreateLoad(int64Ty, gep);

最后添加条件跳转:

1
IndirectBrInst* ind = irb.CreateIndirectBr(targetPtr);

效果

混淆前:

no obf

混淆后:

obf

写在最后

待改进的地方有:加密逻辑过于简单,跳转表未加密等

现在 AI 这么强了,搓个脚本去混淆轻而易举,甚至可能用 mcp 分析的时候顺手就给去掉了。

目前这种混淆只能放在 CTF 新生赛里面逗逗新生了,现在想要对抗 AI 可能需要一些故意的错误引导,让 AI 的分析也变慢。

是时候拥抱 AI 了。🤗

引用:

https://bbs.kanxue.com/thread-283706.htm

https://bbs.kanxue.com/thread-289668.htm

Author:Pinguw
Link:https://pinguw.github.io/2026/02/26/Reverse/%E4%BD%BF%E7%94%A8LLVM%E7%BC%96%E5%86%99%E7%AE%80%E5%8D%95%E6%B7%B7%E6%B7%86Pass%EF%BC%88%E4%BA%8C%EF%BC%89/
看完了吗,再去看看博主的其他文章叭:)