#逆向工程基本原理05
反编译
什么是反编译
反编译(Decompilation)是编译的逆过程:将可读性差的低级(汇编)语言代码转化为可读性强的高级语言代码
1 | ; 汇编代码 |
对应的反编译结果:
1 | void func(int param_1, int param_2, int param_3) { |
反编译的意义在于:
- 降低人工逆向代码的成本
- 基于反汇编代码重新开发闭源软件
- 让逆向分析更加高效
主流反编译器
成熟的反编译器极大降低了逆向门槛,通常有友好的图形化界面,能产生高级代码。但反编译器本身构造复杂,例如 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 | 目标机器代码 → 加载与反汇编 → 汇编代码 → 中间代码生成 → 中间代码表示 |
步骤 1: 加载与反汇编
解析二进制程序获取文件信息(架构、起始地址、段信息、节信息、符号表等),根据文件信息反汇编机器代码
核心问题仍然是区分代码和数据(参见 Lecture 5 反汇编算法)
1 | ELF程序 (011180e0 0430b1e5 020053e1 fcffffba 1eff2fe1) |
步骤 2: 中间代码(IR)生成
为什么需要 IR
不同汇编指令集差异显著(ARM 和 x86 的同一功能代码完全不同),如果为每种架构分别编写反编译器,工程开销极大。IR 屏蔽了指令集差异,只需要编写通用的反编译后端,大大降低了工程开销
1 | 无IR: ARM程序 → ARM反编译器 |
中间代码表示
中间代码表示(IR)是一种表示代码逻辑的数据结构,设计目标:
- 通用:不特定于某一种高级语言/低级语言代码
- 精确:能够精确地表示代码逻辑
- 高效:容易和源代码/目标代码互相转换
本课程使用的 IR 为 P-code,即 Ghidra 反编译器使用的通用表示
P-code
P-code 的表示由以下部分组成:
- 操作码(Operator):指令类型
- 数据:由 Varnode 组成的输入和输出
1 | (register, 0x2c, 4) = LOAD (register, 0x24, 4) |
Varnode
Varnode 定义了特定地址空间的字节序列,由三元组表示:(地址空间, 偏移, 长度)
5 类通用地址空间:
| 地址空间 | 说明 |
|---|---|
| RAM | 内存 |
| Register | 通用寄存器 |
| Unique | 临时寄存器 |
| Stack | 栈空间 |
| Constant | 常量,偏移直接表示常量值本身 |
P-code 翻译为高级语言
1 | (register, 0x2c, 4) = LOAD (register, 0x24, 4) |
反编译通常通过代码分析和代码优化将低抽象等级的 P-code 转化为高抽象等级的 P-code,再进一步翻译为高级语言语句
中间代码生成的一般方法
将每条汇编指令翻译成与之语义等价的 IR,产生的 IR 不包含处理器架构相关信息
1 | ldr r3, [r1, #0x4]! |
翻译规则:
ldr r, [r, #c] → INT_ADD, LOAD;
寄存器 r1 → (register, 0x24, 4),r3 → (register, 0x2c, 4)
步骤 3: 代码分析
代码分析的目标是抽象代码语义,为代码优化和高级语言代码生成提供支持,包括:
- 控制流分析:分析代码的执行流,可用于识别对应的高级语言中的代码结构(顺序、分支、循环结构)
- 数据流分析:分析代码中值的信息,可用于精简生成的高级语言代码
- 精化:恢复编译中丢失的高级语言代码信息(如函数调用参数、变量类型)
- 简化:删除冗余 IR,提高代码可读性
(1) 控制流分析
控制流(Control Flow)指程序中语句执行的顺序关系、跳转关系、依赖关系等信息
高级语言代码的结构特征蕴含控制流信息,但在编译中丢失。IR 指令流是线性的,控制流信息隐藏在跳转指令中
控制流图(CFG)
控制流图(Control Flow Graph)表示了代码中所有可能的控制流,是一种有向图(Directed Graph):
- 节点/顶点:表示顺序执行的代码(基本块)
- 边:表示潜在的控制流转移关系
1 | a = 0 ┌─────────┐ |
基本块(Basic Block)
基本块由顺序执行的指令序列构成,具有以下特点:
- 单入口点:控制流永远从基本块的第一条指令开始
- 单退出点:控制流永远从基本块的最后一条指令离开/结束
CFG 的边
- 出边(Outgoing Edges):表示控制流离开该基本块,指向后继可能执行的基本块
- 入边(Incoming Edges):表示控制流进入该基本块
每个基本块可能有 0、1、2+ 条入边和出边:
- 0 入边:通常为 CFG 入口
- 0 出边:通常为 CFG 出口(如函数返回)
CFG 构建算法
1 | def build_cfg(instructions): |
基本块的划分依据:
- 标签语句(Label):至少包含两个到达当前指令的控制流 → 当前块的结束位置,新块的开始位置
- 分支语句(Branch):至少有两个目标块 → 当前块的结束位置,新块的开始位置
- 普通语句:属于当前基本块
连接基本块时,条件跳转产生两个后继节点,非条件跳转产生一个后继节点
用 CFG 帮助恢复高级语言控制结构
通过分析 CFG 的结构特征,可以识别高级语言中的控制结构。例如,在 CFG 中发现反向跳转回某个条件判断节点,说明代码中包含循环结构
具体的结构化代码恢复方法将在 Lecture 7(反编译 II)中详细介绍
(2) 数据流分析
数据流分析用于计算程序不同位置处值的信息(例如变量取值范围、操作数的可能来源)
常见用途:
- 识别冗余代码
- 推断高级语言语法元素(表达式、变量、变量类型、函数声明等)
识别冗余代码
冗余代码是指产生的值不被使用、也不会产生副作用的代码
识别方法(多轮迭代):
- 假设一开始只有特定有副作用的指令为非冗余,其余指令均为冗余
- 逐条分析指令,若产生的值被非冗余指令使用,则该指令也为非冗余
- 重复步骤 2,直到没有新的非冗余指令被发现
例如,对于以下 P-code:
1 | (unique, 0x3280, 4) = INT_SUB (const, 0x20, 4), (const, 0x2, 4) ; 冗余 |
经过三轮分析后,只保留非冗余的两条指令:
1 | (unique, 0x3580, 4) = INT_LEFT (register, 0x24, 4), (const, 0x2, 4) |
数据流分析的一般方法
分析单元为 CFG 的基本块(基本块内指令执行顺序固定,输入相同初始值则输出固定),沿着 CFG 的有向边进行分析
定义两类函数:
- 转换函数 \(trans\):根据输入计算输出,描述当前基本块对于数据流的影响
- 合并函数 \(join\):合并所有前继基本块的输出,作为当前基本块输入
Worklist 算法
1 | 1. 初始化:对 CFG 所有基本块 in_b = ∅, out_b = ∅, worklist = {b | b ∈ CFG} |
数据流分析可以顺着有向边分析(前向分析),也可以逆着有向边分析(后向分析),具体方向取决于分析目标
步骤 4: 代码优化
使用控制流、数据流分析结果精简代码,常见的优化方法包括:
删除冗余代码
如上文所述,通过多轮迭代分析识别并删除不会影响程序行为的冗余 IR 指令
合并表达式
将 IR 直接提升为高级语言代码时,容易产生冗余的临时变量赋值,降低代码的可读性。合并表达式可以减少冗余赋值,生成更精简的代码
1 | 合并前: |
化简表达式
合并后的表达式可能过于复杂,通过预设规则化简表达式可以提高代码可读性
1 | 化简前: var3 = x + 3 + (x + 3) * 2 |
步骤 5: 高级语言代码生成
包括三个方面:
- 非结构化代码生成:顺序语句,大多数依赖于简单翻译
- 结构化代码生成:分支语句、循环语句,依赖于控制流分析的知识
- 声明代码生成:变量声明、函数声明,依赖于数据流分析的知识
1 | IR 优化后: |
结构化代码的生成将在 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 | X86 Sleigh 规范 ┐ |
Ghidra 反编译器后端技术路线
后端基于 P-code 进行代码分析和优化,将大量冗余的 P-code 精简为核心逻辑,再生成高级语言代码
CodeBrowser 工具
CodeBrowser 是 Ghidra 的代码审计核心界面,功能集成:
- 实时反汇编、反编译
- 符号、类型等信息的管理和显示
- 调用分析工具、插件
- 可视化、可交互
Ghidra 脚本
Ghidra 可以通过脚本进行扩展,让逆向流程更加自动化
编程接口支持 Java 和 Python,脚本管理器可通过 Windows → Script Manager 打开
Java 脚本需要扩展 GhidraScript 类型并重载 run 函数;Python 脚本无需重载,可以直接编写分析逻辑
Ghidra 提供了大量内置脚本,例如用于分析 C++ 类型的脚本,可以直接使用或作为参考