前言
本文仅对Arkari用于混淆的pass的源码分析和讲解,没有提供去除混淆的方案,请需要去混淆的师傅自己想出解决方案,希望分析能够帮到各位师傅 文章大部分对源码的解释都直接以注释的形式写在代码中
PS: 本文所有分析均基于开源项目Arkari:Arkari LLVM19.x 架构为ARM64
Arkari支持的混淆特性
- 混淆过程间相关
- 间接跳转,并加密跳转目标(
-mllvm -irobf-indbr
) - 间接函数调用,并加密目标函数地址(
-mllvm -irobf-icall
) - 间接全局变量引用,并加密变量地址(
-mllvm -irobf-indgv
) - 字符串(c string)加密功能(
-mllvm -irobf-cse
) - 过程相关控制流平坦混淆(
-mllvm -irobf-cff
) - 整数常量加密(
-mllvm -irobf-cie
) - 浮点常量加密(
-mllvm -irobf-cfe
)
编译Arkari源码
|
|
间接跳转介绍
在ARM64架构中,间接跳转混淆是一种通过破坏静态控制流可读性的代码保护技术,其核心机制是将程序中的直接跳转(如B
/BL
指令)替换为通过通用寄存器(如X17)间接跳转的模式,常见形式为BR X8
等。由于IDA等静态反编译工具无法确定寄存器中的值,导致程序原始控制流在静态分析下被截断,使得静态分析失效。
源码分析
间接跳转混淆的源码在编译好之后的Arkari/llvm/lib/Transforms/Obfuscation
路径下的IndirectBranch.cpp
文件
|
|
这是IndirectBranch.cpp
的runOnFunction
函数,在OLLVM中runOnFunction函数是pass执行的入口点
Step 1:随机打乱基本块
在函数中函数首先初始化了两个数组:
BBNumbering
: 存储基本块到随机化编号的映射,用于后续计算跳转索引BBTargets
: 临时存储所有唯一条件分支目标块
这里需要提一下,OLLVM中一个函数对象(Function)是由基本块(BasicBlock)组成,而基本块由每一条指令(Instruction)组成
接着调用SplitAllCriticalEdges
函数,分割关键边,原理是在有多个前驱和后继的边之间插入新基本块
再调用NumberBasicBlock
函数,这个函数将所有条件分支目标基本块分配随机化编号,打乱程序原有的顺序执行流程
|
|
代码的注释中已经写的很详细,总结一下就是:寻找条件跳转的后继基本块->随机数打乱基本块->基本块重编号
Step 2:初始化加密密钥
1.密钥生成
|
|
2. 全局变量声明
|
|
变量名 | 类型 | 作用 |
---|---|---|
GXorKey |
GlobalVariable* |
存储全局异或密钥(用于后续 Level 1 or 2 混淆) |
DestBBs |
GlobalVariable* |
存储加密后的跳转地址数组(核心跳转表) |
XorKeys |
GlobalVariable* |
存储动态生成的异或密钥数组(后续 Level 3 使用) |
step 3:根据opt选择混淆强度
这里引用作者的话来解释:
可以使用下列几种方法之一单独控制某个混淆Pass的强度 (Win64-19.1.0-rc3-obf1.5.1-rc5 or later)
如果不指定强度则默认强度为0,annotate的优先级永远高于命令行参数
可用的Pass:
icall
(强度范围: 0-3)indbr
(强度范围: 0-3)indgv
(强度范围: 0-3)cie
(强度范围: 0-3)cfe
(强度范围: 0-3)1.通过annotate对特定函数指定混淆强度:
^flag=1
表示当前函数设置某功能强度等级(此处为1)
1 2 3 4 5 6 7 8
//^icall=表示指定icall的强度 //+icall表示当前函数启用icall混淆, 如果你在命令行中启用了icall则无需添加+icall [[clang::annotate("+icall ^icall=3")]] int main() { std::cout << "HelloWorld" << std::endl; return 0; }
2.通过命令行参数指定特定混淆Pass的强度
Eg.间接函数调用,并加密目标函数地址,强度设置为3(
-mllvm -irobf-icall -mllvm -level-icall=3
)
控制逻辑
|
|
level 0
|
|
总结下来level 0
的处理就是生成一个加密地址的全局变量数组(GV),可以用一个公式来概括:加密地址 = 原始地址 + EncKey
需要注意的是,EncKey是负数,所以代码上看起来是加密地址 = 原始地址 - EncKey
level 1
|
|
level 1
主要的修改点就是ConstantExpr::getXor(AddKey, XorKey)
这里,换成公式就是加密地址 = 原始地址 + (AddKey ^ XorKey)
level 2
|
|
同level 1
,修改点在代码中已标出,公式:加密地址 = 原始地址 + [AddKey ^ (XorKey * Idx)]
稍微解释一下,level 0 和 level 1的key都是固定的,而level 2的key和基本块的编号挂钩,所以每个基本块对应的解密key都不一样
level 3
|
|
level 3
的加密公式可以这么表示: 加密地址 = 原始地址 + [EncKey1 ^ ( (XorKeys[Idx] ^ EncKey1) * Idx )]
level 3
在level 2
的基础上增加了对密钥的混淆。完全随机化的动态密钥 + 双数组隔离 + 混淆的设计,使得原来硬编码的密钥更加安全,在后续解密基本块地址的时候再对混淆的密钥进行还原,这也是Arkari对间接跳转混淆的创新点所在
step 4:解密真实块地址并插入间接跳转指令
|
|
大概的过程就是如下所示(AI生成):
总结
Hakari的间接跳转混淆增加了不同的混淆强度,尤其是Level 3的混淆,对密钥做了进一步混淆,使得通过原始计算密钥来恢复函数的控制流变得更加困难,如果想更加完美的解决混淆,用Angr强制程序走不同分支也许会更加合适