使用IDA microcode去除ollvm混淆(上)
字数 2002 2025-08-24 20:49:22
IDA Microcode 反混淆技术详解:对抗 OLLVM 混淆
1. 前言
本文详细讲解如何使用 IDA 的 microcode API 来对抗 OLLVM 混淆技术。OLLVM 是一种流行的代码混淆框架,它通过多种技术使代码难以分析和理解。本文将重点介绍如何利用 IDA 7.1+ 提供的 microcode 中间语言和相关 API 来去除 OLLVM 的混淆。
2. IDA 反编译器的两种表示方式
IDA 反编译器中二进制代码有两种表示方式:
-
microcode:
- 处理器指令被翻译成 microcode
- 反编译器对其进行优化和转换
- 使用 HexRaysDeob 插件可以查看和处理 microcode
-
ctree:
- 由优化的 microcode 构建而成
- 用 C 语句和表达式表示,类似 AST 的树结构
- 使用 HexRaysCodeXplorer 插件或 IDApython 中的 vds5.py 可以查看 ctree
反编译流程:二进制指令 → microcode → 优化转换 → ctree → C 代码
3. Microcode 核心数据结构
3.1 mbl_array_t
保存关于反编译代码和基本块数组的信息:
class mbl_array_t {
int qty; // 基本块数组的数量
const mblock_t* get_mblock(int n) const; // 根据序号返回基本块
mblock_t* insert_block(int bblk); // 插入一个基本块
bool remove_block(mblock_t* blk); // 删除一个基本块
bool remove_empty_blocks(); // 删除所有空的基本块
bool combine_blocks(); // 合并线性的基本块
int for_all_ops(mop_visitor_t& mv); // 遍历所有操作数
int for_all_insns(minsn_visitor_t& mv); // 遍历所有指令
int for_all_topinsns(minsn_visitor_t& mv); // 遍历所有顶层指令
};
3.2 mblock_t
表示一个包含指令列表的基本块:
class mblock_t {
mblock_t* nextb; // 双向链表中的下一个基本块
mblock_t* prevb; // 双向链表中的上一个基本块
minsn_t* head; // 指向第一条指令
minsn_t* tail; // 指向最后一条指令
mbl_array_t* mba; // 所属的mbl_array_t
int npred() const; // 前驱者数目
int nsucc() const; // 后继者数目
int pred(int n) const; // 第n个前驱者
int succ(int n) const; // 第n个后继者
minsn_t* insert_into_block(minsn_t* nm, minsn_t* om); // 插入指令
minsn_t* remove_from_block(minsn_t* m); // 删除指令
int for_all_ops(mop_visitor_t& mv); // 遍历所有操作数
int for_all_insns(minsn_visitor_t& mv); // 遍历所有指令
};
3.3 minsn_t
表示一条指令(可以嵌套):
class minsn_t {
mcode_t opcode; // 操作码
int iprops; // 指令性质的位组合
minsn_t* next; // 双向链表中的下一条指令
minsn_t* prev; // 双向链表中的上一条指令
ea_t ea; // 指令地址
mop_t l; // 左操作数
mop_t r; // 右操作数
mop_t d; // 目标操作数
int for_all_ops(mop_visitor_t& mv); // 遍历所有操作数
int for_all_insns(minsn_visitor_t& mv); // 遍历所有指令
};
3.4 mop_t
表示一个操作数,可以表示不同类型的信息:
class mop_t {
mopt_t t; // 操作数类型
uint8 oprops; // 操作数属性
uint16 valnum; // 操作数的值
int size; // 操作数大小
union {
mreg_t r; // mop_r 寄存器数值
mnumber_t* nnn; // mop_n 立即数的值
minsn_t* d; // mop_d 另一条指令
stkvar_ref_t* s; // mop_S 堆栈变量
ea_t g; // mop_v 全局变量
int b; // mop_b 块编号
// ... 其他类型
};
};
4. OLLVM 混淆技术分析
4.1 基于模式的混淆(不透明谓词)
样本中常见的混淆模式:
if ((x * (x - 1)) & 1 == 0) {
// 这部分代码永远不会执行
}
- x 是偶数或奇数
- x-1 和 x 的奇偶性相反
- 偶数 × 奇数 = 偶数
- 偶数的最低位为 0,因此 &1 结果为 0
- 这是一种不透明谓词(Opaque Predicate)混淆方式
4.2 控制流平坦化(Control Flow Flattening)
控制流平坦化的原理:
- 为每个基本块分配一个数字
- 引入块号变量,指示应执行哪个块
- 每个块更新块号变量为其后继者
- 普通控制流被循环内的 switch 语句代替
典型结构:
int block_var = RANDOM_NUMBER;
while (1) {
switch (block_var) {
case BLOCK_A: ...; block_var = NEXT_A; break;
case BLOCK_B: ...; block_var = NEXT_B; break;
// ...
}
}
4.3 异常的栈操作
混淆器使用 __alloca_probe 为函数参数和局部变量保留栈空间,而不是常规的 push 指令,这使得 IDA 难以正确跟踪栈指针。
5. 反混淆器设计与实现
5.1 代码结构
HexRaysDeob 反混淆器的核心组件:
AllocaFixer: 处理__alloca_probeCFFlattenInfo: 控制流平坦化预处理PatternDeobfuscate: 处理基于模式的混淆Unflattener: 处理控制流平坦化DefUtil/HexRaysUtil/TargetUtil: 辅助功能
5.2 对抗控制流平坦化的步骤
第一步:确定平坦块编号到 mblock_t 的映射
- 识别 switch(block) 部分的 microcode 表示
- 分析块号变量(如 ST14_4.4)的比较和跳转
- 建立块编号与 mblock_t 的对应关系
第二步:确定每个平坦块的后继者
- 分析每个平坦块的 microcode
- 识别块号变量的更新操作
- 确定无条件转移(一个后继者)和条件转移(两个后继者)
第三步:直接转移控制流
- 修改 goto 指令的目标,直接指向后继块
- 删除不必要的块号变量更新
- 对于条件转移:
- 复制指令到合适位置
- 修改 goto 指令指向不同目标
- 清理不必要的赋值
5.3 关键实现技术
使用 microcode 而非 ctree 的优势
- microcode 更底层,能更精确地匹配模式
- 可以利用 HexRays 已有的控制流恢复算法
- 在 microcode 级别操作可以保留更多原始信息
成熟度阶段(maturity phases)
HexRays 优化 microcode 时会经历不同成熟阶段(mba_maturity_t):
MMAT_GENERATED: 刚生成的 microcodeMMAT_LOCOPT: 局部优化后MMAT_CALLS: 函数调用分析后
通过 gen_microcode() API 可以指定需要的成熟度级别。
6. 总结
本文详细介绍了如何利用 IDA 的 microcode API 对抗 OLLVM 混淆,重点讲解了控制流平坦化的去除方法。通过分析 microcode 的核心数据结构、理解混淆技术原理,并按照三个步骤(映射块编号、确定后继者、直接转移控制流)实现反混淆,我们可以有效地恢复原始的控制流结构。
关键要点:
- microcode 提供了对反编译中间表示的精细控制
- 控制流平坦化的核心是识别和重建原始控制流转移
- 在 microcode 级别操作可以充分利用 IDA 的优化能力
- 模式匹配和程序分析相结合是反混淆的有效方法