$ cat ~ / posts /reverse /lecture05 3.6k Words ~ 13 Mins
cover.png
逆向工程基本原理05

#逆向工程基本原理05

exdoubled Lv5

反编译

什么是反编译

反编译(Decompilation)是编译的逆过程:将可读性差的低级(汇编)语言代码转化为可读性强的高级语言代码

1
2
3
4
5
6
7
; 汇编代码
add r1, r0, r1, lsl #0x2
ADDR:
ldr r3, [r1, #0x4]!
cmp r3, r2
blt ADDR
bx lr

对应的反编译结果:

1
2
3
4
5
6
7
8
void func(int param_1, int param_2, int param_3) {
int *piVar1;
piVar1 = (int *)(param_1 + param_2 * 4);
do {
piVar1 = piVar1 + 1;
} while (*piVar1 < param_3);
return;
}

反编译的意义在于:

  • 降低人工逆向代码的成本
  • 基于反汇编代码重新开发闭源软件
  • 让逆向分析更加高效

主流反编译器

成熟的反编译器极大降低了逆向门槛,通常有友好的图形化界面,能产生高级代码。但反编译器本身构造复杂,例如 RetDec 反编译器就有 20w+ 行 C/C++ 代码,依赖于 14 个开源项目

反编译器商业模式开源情况主要团队特点
IDA Pro付费闭源Hex-rays广泛的处理器支持,强大的反编译能力,高度可定制的插件系统
JEB付费闭源PNF Software强大的 Android, Java, Web Assembly 等目标平台支持
Binary Ninja付费API 开源Vector 35 Inc用户友好的交互式分析,强调自动化分析和插件系统,价格较低
Ghidra免费开源NSA广泛的处理器支持,强大的反编译能力,开源且受政府支持
RetDec免费开源Avast Software使用 LLVM IR,提供 API 服务和 Web 界面
angr免费开源UCSB SecLab / ASU SEFCOM继承多种程序分析技术(如符号执行),适用于自动化任务

对于初学者来说,Ghidra 是最佳选择:免费开源、功能强大、社区活跃。IDA Pro 虽然是行业标准,但价格不菲,但也有神秘渠道获取

反编译的一般架构

反编译器通常分为前端和后端两部分:

  • 前端:解析二进制程序,生成中间代码表示(IR)
  • 后端:分析和优化中间代码,生成高级语言(伪)代码

一般流程为:

1
2
3
4
5
目标机器代码 → 加载与反汇编 → 汇编代码 → 中间代码生成 → 中间代码表示
↓ ↓
反编译器前端 反编译器后端

代码分析 → 代码优化 → 高级语言代码生成 → (伪)高级语言代码

步骤 1: 加载与反汇编

解析二进制程序获取文件信息(架构、起始地址、段信息、节信息、符号表等),根据文件信息反汇编机器代码

核心问题仍然是区分代码和数据(参见 Lecture 5 反汇编算法)

1
2
3
4
5
6
7
8
9
10
ELF程序 (011180e0 0430b1e5 020053e1 fcffffba 1eff2fe1)
↓ 解析文件信息(架构: ARM32, 起始地址, 段信息...)
↓ 反汇编
汇编代码:
add r1, r0, r1, lsl #0x2
ADDR:
ldr r3, [r1, #0x4]!
cmp r3, r2
blt ADDR
bx lr

步骤 2: 中间代码(IR)生成

为什么需要 IR

不同汇编指令集差异显著(ARM 和 x86 的同一功能代码完全不同),如果为每种架构分别编写反编译器,工程开销极大。IR 屏蔽了指令集差异,只需要编写通用的反编译后端,大大降低了工程开销

1
2
3
4
5
6
无IR:  ARM程序 → ARM反编译器
X86程序 → X86反编译器

有IR: ARM程序 → ARM反编译前端 ┐
X86程序 → X86反编译前端 ┤→ 通用反编译后端
MIPS程序 → MIPS反编译前端┘

中间代码表示

中间代码表示(IR)是一种表示代码逻辑的数据结构,设计目标:

  • 通用:不特定于某一种高级语言/低级语言代码
  • 精确:能够精确地表示代码逻辑
  • 高效:容易和源代码/目标代码互相转换

本课程使用的 IR 为 P-code,即 Ghidra 反编译器使用的通用表示

P-code

P-code 的表示由以下部分组成:

  • 操作码(Operator):指令类型
  • 数据:由 Varnode 组成的输入和输出
1
2
(register, 0x2c, 4) = LOAD (register, 0x24, 4)
指令输出 操作码 指令输入
Varnode

Varnode 定义了特定地址空间的字节序列,由三元组表示:(地址空间, 偏移, 长度)

5 类通用地址空间:

地址空间说明
RAM内存
Register通用寄存器
Unique临时寄存器
Stack栈空间
Constant常量,偏移直接表示常量值本身
P-code 翻译为高级语言
1
2
3
(register, 0x2c, 4) = LOAD (register, 0x24, 4)
↓ 直接翻译
reg_2c = *reg_24

反编译通常通过代码分析和代码优化将低抽象等级的 P-code 转化为高抽象等级的 P-code,再进一步翻译为高级语言语句

中间代码生成的一般方法

将每条汇编指令翻译成与之语义等价的 IR,产生的 IR 不包含处理器架构相关信息

1
2
3
4
ldr r3, [r1, #0x4]!
↓ 翻译为 P-code
(register, 0x24, 4) = INT_ADD (register, 0x24, 4), (const, 0x4, 4)
(register, 0x2c, 4) = LOAD (register, 0x24, 4)

翻译规则:

ldr r, [r, #c]INT_ADD, LOAD

寄存器 r1(register, 0x24, 4)r3(register, 0x2c, 4)

步骤 3: 代码分析

代码分析的目标是抽象代码语义,为代码优化和高级语言代码生成提供支持,包括:

  1. 控制流分析:分析代码的执行流,可用于识别对应的高级语言中的代码结构(顺序、分支、循环结构)
  2. 数据流分析:分析代码中值的信息,可用于精简生成的高级语言代码
    • 精化:恢复编译中丢失的高级语言代码信息(如函数调用参数、变量类型)
    • 简化:删除冗余 IR,提高代码可读性

(1) 控制流分析

控制流(Control Flow)指程序中语句执行的顺序关系、跳转关系、依赖关系等信息

高级语言代码的结构特征蕴含控制流信息,但在编译中丢失。IR 指令流是线性的,控制流信息隐藏在跳转指令中

控制流图(CFG)

控制流图(Control Flow Graph)表示了代码中所有可能的控制流,是一种有向图(Directed Graph):

  • 节点/顶点:表示顺序执行的代码(基本块)
  • :表示潜在的控制流转移关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 0        ┌─────────┐
b = 1 │ a=0 │
│ b=1 │
└────┬────┘

┌─────────┐
│ a < 10 │←──┐
└──┬───┬──┘ │
a≥10 │ │ a<10 │
↓ ↓ │
┌────────┐ ┌──────┐
│return a│ │a += b│
└────────┘ │b += 1│
└──┬───┘
└──────┘
基本块(Basic Block)

基本块由顺序执行的指令序列构成,具有以下特点:

  • 单入口点:控制流永远从基本块的第一条指令开始
  • 单退出点:控制流永远从基本块的最后一条指令离开/结束
CFG 的边
  • 出边(Outgoing Edges):表示控制流离开该基本块,指向后继可能执行的基本块
  • 入边(Incoming Edges):表示控制流进入该基本块

每个基本块可能有 0、1、2+ 条入边和出边:

  • 0 入边:通常为 CFG 入口
  • 0 出边:通常为 CFG 出口(如函数返回)
CFG 构建算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def build_cfg(instructions):
BBs = []
BB = BasicBlock()

for ins in instructions:
if ins.is_label():
BBs.append(BB)
BB = BasicBlock()
BB.add(ins)
elif ins.is_branch():
BB.add(ins)
BBs.append(BB)
BB = BasicBlock()
else:
BB.add(ins)

return BBs

基本块的划分依据:

  1. 标签语句(Label):至少包含两个到达当前指令的控制流 → 当前块的结束位置,新块的开始位置
  2. 分支语句(Branch):至少有两个目标块 → 当前块的结束位置,新块的开始位置
  3. 普通语句:属于当前基本块

连接基本块时,条件跳转产生两个后继节点,非条件跳转产生一个后继节点

用 CFG 帮助恢复高级语言控制结构

通过分析 CFG 的结构特征,可以识别高级语言中的控制结构。例如,在 CFG 中发现反向跳转回某个条件判断节点,说明代码中包含循环结构

具体的结构化代码恢复方法将在 Lecture 7(反编译 II)中详细介绍

(2) 数据流分析

数据流分析用于计算程序不同位置处值的信息(例如变量取值范围、操作数的可能来源)

常见用途:

  • 识别冗余代码
  • 推断高级语言语法元素(表达式、变量、变量类型、函数声明等)
识别冗余代码

冗余代码是指产生的值不被使用、也不会产生副作用的代码

识别方法(多轮迭代):

  1. 假设一开始只有特定有副作用的指令为非冗余,其余指令均为冗余
  2. 逐条分析指令,若产生的值被非冗余指令使用,则该指令也为非冗余
  3. 重复步骤 2,直到没有新的非冗余指令被发现

例如,对于以下 P-code:

1
2
3
4
5
6
7
8
9
10
11
12
(unique, 0x3280, 4) = INT_SUB (const, 0x20, 4), (const, 0x2, 4)          ; 冗余
(unique, 0x3300, 4) = INT_RIGHT (register, 0x24, 4), (unique, 0x3280, 4) ; 冗余
(unique, 0x3400, 4) = INT_AND (unique, 0x3300, 4), (const, 0x1, 4) ; 冗余
(register, 0x68, 1) = SUBPIECE (unique, 0x3400, 4), (const, 0x0, 4) ; 冗余
(unique, 0x3580, 4) = INT_LEFT (register, 0x24, 4), (const, 0x2, 4) ; 非冗余(被下面使用)
(register, 0x66, 1) = INT_CARRY (register, 0x20, 4), (unique, 0x3580, 4) ; 冗余
(register, 0x67, 1) = INT_SCARRY (register, 0x20, 4), (unique, 0x3580, 4); 冗余
(register, 0x24, 4) = INT_ADD (register, 0x20, 4), (unique, 0x3580, 4) ; 非冗余(被下面使用)
(register, 0x64, 1) = INT_SLESS (register, 0x24, 4), (const, 0x0, 4) ; 冗余
(register, 0x65, 1) = INT_EQUAL (register, 0x24, 4), (const, 0x0, 4) ; 冗余
……
variable = OP (register, 0x24, 4) ; 有副作用,非冗余

经过三轮分析后,只保留非冗余的两条指令:

1
2
(unique, 0x3580, 4) = INT_LEFT (register, 0x24, 4), (const, 0x2, 4)
(register, 0x24, 4) = INT_ADD (register, 0x20, 4), (unique, 0x3580, 4)
数据流分析的一般方法

分析单元为 CFG 的基本块(基本块内指令执行顺序固定,输入相同初始值则输出固定),沿着 CFG 的有向边进行分析

定义两类函数:

  • 转换函数 \(trans\):根据输入计算输出,描述当前基本块对于数据流的影响
  • 合并函数 \(join\):合并所有前继基本块的输出,作为当前基本块输入
Worklist 算法
1
2
3
4
5
6
7
1. 初始化:对 CFG 所有基本块 in_b = ∅, out_b = ∅, worklist = {b | b ∈ CFG}
2. 对每一个基本块进行下面的操作直到 worklist 为空(或迭代一定次数):
a. 从 worklist 取出一个基本块 b
b. 合并所有 prev(b) 基本块的输出作为 b 的输入:
in_b = join(out_p) for p ∈ pred(b)
c. 计算 b 基本块的值信息:out_b = trans_b(in_b)
d. 如果 out_b 更新了,则将 b 的后继基本块加入 worklist 中重新分析

数据流分析可以顺着有向边分析(前向分析),也可以逆着有向边分析(后向分析),具体方向取决于分析目标

步骤 4: 代码优化

使用控制流、数据流分析结果精简代码,常见的优化方法包括:

删除冗余代码

如上文所述,通过多轮迭代分析识别并删除不会影响程序行为的冗余 IR 指令

合并表达式

将 IR 直接提升为高级语言代码时,容易产生冗余的临时变量赋值,降低代码的可读性。合并表达式可以减少冗余赋值,生成更精简的代码

1
2
3
4
5
6
7
合并前:
1. var1 = x + 3
2. var2 = var1 * 2
3. var3 = var1 + var2

合并后:
var3 = x + 3 + (x + 3) * 2

化简表达式

合并后的表达式可能过于复杂,通过预设规则化简表达式可以提高代码可读性

1
2
化简前: var3 = x + 3 + (x + 3) * 2
化简后: var3 = (x + 3) * 3

步骤 5: 高级语言代码生成

包括三个方面:

  1. 非结构化代码生成:顺序语句,大多数依赖于简单翻译
  2. 结构化代码生成:分支语句、循环语句,依赖于控制流分析的知识
  3. 声明代码生成:变量声明、函数声明,依赖于数据流分析的知识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IR 优化后:
(unique, 0x3580, 4) INT_MULT (register, 0x24, 4), (const, 0x4, 4)
(unique, 0x10000008, 4) INT_ADD (register, 0x20, 4), (unique, 0x3580, 4)
(register, 0x24, 4) CAST (unique, 0x10000008, 4)
...

↓ 翻译 + 合并表达式

z = y + x * 4

↓ 控制流知识 → 生成循环结构

do ... while ...

↓ 数据流知识 → 生成函数声明

int func(int param_1, int param_2, int param_3);

结构化代码的生成将在 Lecture 7(反编译 II)中详细介绍

Ghidra 反编译器

Ghidra 简介

Ghidra 是一个软件逆向工程(SRE)框架,由 NSA 创建和维护:

  • 多功能:支持反汇编、汇编、反编译、绘图和脚本,以及数百个其他特性
  • 多平台:能分析 Windows、macOS 和 Linux 上的二进制程序
  • 多架构:支持 X86/ARM/MIPS 等指令集和 ELF/PE/Mach-O 等文件格式
  • 可扩展:可以使用 Java 或 Python 开发脚本扩展功能
  • 多场景:旨在解决复杂软件逆向工作的扩展和团队协作问题

已被 NSA 应用于各种安全问题,包括恶意代码分析和漏洞分析

Ghidra 反编译前端技术路线

前端的核心技术是 Sleigh

  • Sleigh 是一种规范,用于定义汇编指令集
  • Sleigh 也是一套库/工具,输入编写好的规范,就可以生成反汇编器和中间代码生成器

Sleigh 的优势在于易扩展:不需要编写指令集处理的具体逻辑,简化了新反汇编器和中间代码生成器的开发流程

1
2
3
X86 Sleigh 规范  ┐
ARM Sleigh 规范 ├→ Sleigh 工具 → 反汇编器 + 中间代码生成器
MIPS Sleigh 规范 ┘

Ghidra 反编译器后端技术路线

后端基于 P-code 进行代码分析和优化,将大量冗余的 P-code 精简为核心逻辑,再生成高级语言代码

CodeBrowser 工具

CodeBrowser 是 Ghidra 的代码审计核心界面,功能集成:

  • 实时反汇编、反编译
  • 符号、类型等信息的管理和显示
  • 调用分析工具、插件
  • 可视化、可交互

Ghidra 脚本

Ghidra 可以通过脚本进行扩展,让逆向流程更加自动化

编程接口支持 Java 和 Python,脚本管理器可通过 Windows → Script Manager 打开

Java 脚本需要扩展 GhidraScript 类型并重载 run 函数;Python 脚本无需重载,可以直接编写分析逻辑

Ghidra 提供了大量内置脚本,例如用于分析 C++ 类型的脚本,可以直接使用或作为参考

$ discussion
# Comments
waline