LLVM代码混淆技术详解:从基础概念到实现原理
1. 基本块分割技术
1.1 基本概念
基本块分割是将一个基本块分割为功能等价的若干个基本块,并在分割后的基本块之间添加无条件跳转指令。这种技术类似于"将一句话解释清楚的事情,通过添油加醋变得冗长",类似于营销号的写作风格。
1.2 作用与意义
- 不是真正的混淆技术:基本块分割本身不提供代码保护功能
- 增强混淆效果:通过增加基本块数量,提高后续混淆技术的复杂度
- 基于基本块的混淆基础:许多代码混淆技术依赖大量基本块来实现更好的混淆效果
1.3 实现注意事项
- PHI指令处理:含有PHI指令的基本块需要特殊处理
- 前驱块识别问题:分割后PHI指令可能无法正确识别前驱块
- IDA反编译:单纯的基本块分割会被IDA自动清理,因此仅作为辅助手段
1.4 LLVM实现API
cl::opt模板类:用于获取指令参数(cl表示clang,opt表示option)splitBasicBlock函数:BasicBlock类的成员函数,用于分割基本块isa<>模板函数:类似于Java的instanceof,用于判断指针指向的数据类型
2. 代码混淆基础概念
2.1 代码混淆定义
代码混淆是将计算机程序代码转换为功能上等价但难以阅读和理解的形式的行为。
2.2 基本组成元素
- 函数:代码混淆的基本单位,由若干基本块组成
- 基本块:由一组线性指令组成,有明确入口点和出口点
- 控制流:程序执行过程中可能遍历的所有路径
2.3 不透明谓词
定义:混淆者明确知晓其值,但反混淆者难以推断的变量
分类:
- 永远为真或永远为假
- 可以为真或假
示例:
int x = 5;
if (y > 10 || x * (x + 1) % 2 == 0) {
// 真实代码
} else {
// 虚假代码(永远不会执行)
}
其中x * (x + 1) % 2 == 0恒成立,但反编译器无法识别。
3. 常见混淆技术分类
3.1 符号混淆
- 去除或混淆函数名、全局变量名等符号信息
- 对ELF文件可使用
strip指令去除符号表
3.2 控制流混淆
- 混淆程序正常控制流,保持功能但难以理解逻辑
- 包括控制流平坦化、虚假控制流等技术
3.3 计算混淆
- 混淆程序的计算流程或计算中使用的数据
- 使分析者难以分辨代码执行的具体计算
3.4 虚拟机混淆
- 将指令集转换为自定义指令集,通过解释器执行
- 代表工具:VMProtect
- 优点:保护强度高
- 缺点:性能损耗大、易被报毒
4. OLLVM环境搭建
4.1 环境准备
- 推荐使用Docker容器:避免在Ubuntu上直接配置时的各种错误
- 必要组件:
- gcc8、g++8
- cmake
- git
- LLVM源码
4.2 源码修改
需要修改ollvm目录/include/llvm/ExecutionEngine/Orc/OrcRemoteTargetClient.h文件:
- 将
char修改为uint8_t - 修改前给予文件
chmod 777权限
4.3 编译与验证
完成编译安装后,通过测试代码验证环境是否正常工作。
5. OLLVM基本用法
5.1 控制流平坦化
- 选项:
-mllvm -fla - 附加选项:
-mllvm -split(激活基本块分割) - 分割数量:
-mllvm -split_num=3(指定分割数量)
5.2 虚假控制流
- 选项:
-mllvm -bcf - 混淆次数:
-mllvm -bcf_loop=3(默认1) - 混淆概率:
-mllvm -bcf_prob=40(默认30%)
5.3 指令替换
- 选项:
-mllvm -sub - 替换次数:
-mllvm -sub_loop=3(默认1)
6. 控制流平坦化详解
6.1 基本概念
将正常控制流中基本块之间的跳转关系删除,改用集中的分发块来调度基本块的执行顺序。
6.2 组成元素
- 入口块:进入函数第一个执行的基本块
- 主分发块与子分发块:负责跳转到下一个要执行的原基本块
- 原基本块:混淆前的基本块,真正完成程序功能
- 返回块:返回到主分发块
6.3 实现后的结构
平坦化后的控制流呈现while + switch结构(也可能是if...else结构)。
6.4 实现步骤
- 保存原基本块:将除入口块外的所有基本块保存到vector容器
- 创建分发块和返回块:建立入口块到分发块的绝对跳转
- 实现分发块调度:在入口块创建并初始化switch变量
- 实现调度变量自动调整:在每个原基本块最后添加修改switch变量的指令
- 修复PHI指令和逃逸变量:使用
DemotePHIToStack将PHI指令转化为内存存取指令
6.5 逃逸变量修复
判断为逃逸变量的条件:
- 不能是内存申请指令且不在入口块内
isUseOutsideOfBlock函数判断为真(指令在其他基本块中被使用)
7. 虚假控制流实现
7.1 基本概念
通过插入不可达基本块和由不透明谓词造成的虚假跳转,产生大量垃圾代码干扰分析。
7.2 实现步骤
- 基本块拆分:将基本块拆分为头部、中部和尾部
- 基本块克隆:使用
CloneBasicBlock函数克隆中间块 - 构造虚假跳转:将绝对跳转改为条件跳转
7.3 克隆处理
需要创建createCloneBasicBlock函数进行完整克隆,包括变量映射修复:
// 将原基本块中的变量映射到克隆块
ValueToValueMapTy VMap;
for (auto &I : *BB) {
VMap[&I] = &I.clone();
}
8. 指令替换技术
8.1 实现原理
将正常的二元运算指令替换为等效但更复杂的指令序列。
8.2 替换示例
- 加法替换:
a + b→a - (-b) - 减法替换:
a - b→a + (~b) + 1 - 与替换:
a & b→~(a | ~b) - 或替换:
a | b→(a & ~b) | b - 异或替换:
a ^ b→(~a & b) | (a & ~b)
8.3 注意事项
- 仅支持整数运算替换
- 浮点指令替换会产生舍入错误和误差
9. 随机控制流技术
9.1 特点
- 虚假控制流的变体
- 通过克隆基本块和添加随机跳转来混淆控制流
- 不存在不可达基本块和不透明谓词
- 对抗虚假控制流去除手段更有效
9.2 实现步骤
- 基本块拆分:与虚假控制流类似
- 基本块克隆:需要修复逃逸变量
- 构造随机跳转:插入随机数生成指令和基于随机数的跳转
- 构造虚假随机跳转:插入实际会直接跳转的"随机"跳转指令
9.3 随机数生成
使用LLVM内置函数rdrand生成随机数,编译时需要添加-mattr=rdrnd属性。
10. 常量替代技术
10.1 基本概念
将二元运算指令中使用的常数替换为等效但更复杂的表达式。
10.2 应用示例
将TEA加密中的常量0x9e3779b替换为12167*16715+18858*32146-643678438。
10.3 拓展应用
- 常量数组替代:抹去AES、DES等算法的特征数组
- 字符串替代:防止通过字符串定位关键代码
10.4 实现限制
- 仅支持32位整数常量替换
- 不支持浮点数替换(会产生舍入误差)
总结
LLVM代码混淆技术提供了多层次的代码保护方案,从基本块分割到复杂的控制流混淆,每种技术都有其特定的应用场景和实现要点。在实际应用中,通常需要组合使用多种混淆技术,以达到最佳的代码保护效果。同时,也需要权衡混淆强度与性能开销之间的关系,根据具体需求选择合适的混淆方案。