一、先建立直观理解
一个类比:考古学家的工作
想象你发现了一块古代的石碑(这就是二进制文件):
| 考古工作 | 对应二进制分析 |
|---|---|
| 石碑本身 | 二进制文件(exe、dll、elf) |
| 石碑上的文字 | 机器指令(CPU能执行的代码) |
| 识别这是什么文字 | 格式解析(PE/ELF格式识别) |
| 翻译成现代语言 | 反汇编(机器码→汇编) |
| 理解句子意思 | 反编译(汇编→伪代码) |
| 分析文章结构 | 控制流图(函数、跳转关系) |
| 找出谁引用谁 | 函数调用图 |
| 提取关键人名地名 | 信息提取(字符串、API调用) |
作为源码开发者,平时是看着源码(文章的原始手稿)在工作。而二进制分析是只有石碑(编译后的成品),要反推文章内容。
二、核心概念详解
2.1 二进制格式解析
二进制文件就是计算机可以直接执行的程序。比如你双击运行的
.exe文件,Linux 下的可执行文件,都是二进制文件。
打个比方:
- 源代码(C/Python)就像菜谱(人类能看懂)
- 二进制文件就像做好的菜(计算机直接"吃")
- 二进制分析就像尝菜分析用了什么调料(反推做法)
是什么:识别二进制文件的组织结构,就像解读一本书的目录和章节划分。
为什么需要:不同的操作系统用不同的格式组织二进制文件,必须先理解这个组织方式,才能正确解析内容。
常见格式:
- PE:Windows可执行文件(.exe、.dll)
- ELF:Linux可执行文件
- Mach-O:macOS可执行文件
PE文件结构示例:
┌─────────────────────┐
│ DOS头 │ <- "MZ"开头,兼容性用的
├─────────────────────┤
│ PE头 │ <- 关键信息:入口点、编译时间
├─────────────────────┤
│ 节表 │ <- 有几个节?各节叫什么?
├─────────────────────┤
│ .text (代码节) │ <- 真正的机器指令在这里
├─────────────────────┤
│ .data (数据节) │ <- 全局变量、初始化的数据
├─────────────────────┤
│ .rdata (只读数据) │ <- 字符串常量、只读数据
├─────────────────────┤
│ .idata (导入表) │ <- 调用了哪些Windows API?
└─────────────────────┘
解析出的元数据:
{
"entry_point": "0x401000", // 程序从哪开始执行
"timestamp": "2023-01-01", // 编译时间
"sections": [
{ "name": ".text", "size": 1024, "permissions": "RX" },
{ "name": ".data", "size": 256, "permissions": "RW" }
],
"imported_functions": [ // 调用了哪些系统函数
"MessageBoxA",
"CreateFile",
"WriteFile"
],
"is_packed": false // 是否被加壳保护
}
查看方法:
# 用 file 命令查看
file constraint1.x86
# 输出: ELF 32-bit LSB executable, Intel 80386...
2.2 信息提取
是什么:从二进制中提取出人类可读的有意义信息,这些信息是理解程序行为的线索。
提取的内容:
(1) 字符串提取
机器码无法直接看出文字,但程序运行时需要显示文字、连接网络,这些字符串会以明文形式存在:
# 从恶意软件中提取的字符串示例
http://evil.com/payload.exe
C:\Windows\System32\cmd.exe
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Mutex_MyMalware_2023
这些字符串直接暴露了程序的意图:
- URL → 要连接哪里
- 文件路径 → 要操作什么文件
- 注册表路径 → 要写什么注册表项
- Mutex → 互斥体名(常用来防止多开)
(2) API调用提取
程序要操作文件、网络、注册表,必须调用操作系统API。从导入表可以提取:
// 程序调用了这些函数,暗示了它的行为
CreateFile // 可能要读写文件
RegSetValue // 可能要修改注册表(持久化)
socket // 可能要网络通信
CreateRemoteThread // 可能要做进程注入(恶意行为)
Java视角理解:就像看一个Java类的import语句,虽然不知道具体实现,但能看出它用到了哪些库。
(3) 资源提取
二进制里可以嵌入其他文件:
- 图标、图片
- 配置文件
- 其他可执行文件
- 加密的payload
2.3 反汇编
是什么:把机器码(CPU能执行的字节)转换成汇编语言(人类勉强能读的低级语言)。
为什么需要:机器码是纯粹的字节,如 0x55 0x89 0xE5,没人能直接理解。
转换示例:
机器码 (十六进制) → 汇编指令
0x55 → push ebp
0x89 0xE5 → mov ebp, esp
0x83 0xEC 0x40 → sub esp, 0x40
0x8B 0x45 0x08 → mov eax, [ebp+0x8] ; 获取参数
示例对比
二进制: 01010101 10001001 11100101
汇编: push ebp # 把ebp压入栈
mov ebp, esp # 把esp的值赋给ebp
C语言: function() { # 函数开始
Java开发者视角:这相当于你看到的class文件是字节码,javap命令反汇编成可读格式。只是Java字节码比x86汇编更高级一些。
2.4 中间表示(IR)
是什么:把不同CPU架构(x86、ARM、MIPS)的汇编指令,统一转换成一种架构无关的中间语言。
为什么需要:
- x86和ARM指令完全不同
- 如果直接分析汇编,每种架构都要一套工具
- 转换成统一IR后,分析逻辑可以复用
著名的IR:
- REIL:BinNavi使用的中间语言
- VEX:Valgrind和angr使用的IR
- LLVM IR:LLVM编译框架的IR
- Ghidra的P-Code:NSA Ghidra使用的IR
例子:同一逻辑在不同架构的表现:
x86汇编:
mov eax, [ebp+0x8]
add eax, 0x1
ARM汇编:
ldr r0, [sp, #0x8]
add r0, r0, #1
统一IR (REIL):
load eax, [ebp+0x8] // 读取
add eax, eax, 0x1 // 加1
store [结果地址], eax // 存回
好处:你只需要写一套分析规则,就能分析所有架构的二进制。
2.5 反编译
基本原理:通过分析汇编指令的模式(如函数调用、变量访问等),还原出高级语言结构。
是什么:把汇编语言(或IR)进一步转换成类似高级语言的伪代码,让分析者更容易理解程序逻辑。
为什么需要:汇编语言还是太底层,看几百行汇编很难理解整体逻辑。反编译尝试恢复出接近C语言的表达。
例子:
汇编代码:
push ebp
mov ebp, esp
sub esp, 0x10
mov dword [ebp-0x4], 0x0
cmp dword [ebp+0x8], 0x0
je 0x401020
mov eax, [ebp+0x8]
add eax, 0x5
mov [ebp-0x8], eax
jmp 0x401028
mov dword [ebp-0x8], 0x0
mov eax, [ebp-0x8]
leave
ret
反编译后的伪代码(IDA Pro/Ghidra输出):
int func(int input) {
int result;
if (input != 0) {
result = input + 5;
} else {
result = 0;
}
return result;
}
Java开发者视角:这就像从字节码反编译回Java代码。
2.6 控制流图(CFG)
控制流图展示了程序的"路线图"——代码执行的顺序和可能的分支。
是什么:表示程序执行路径的图,展示代码中所有可能的执行顺序。
基本概念:
- 基本块:一段顺序执行的指令,只有一个入口、一个出口
- 边:表示控制流转移(跳转、调用、返回)
一个简单函数的CFG:
┌─────────────┐
│ 基本块A │
│ mov eax, 1 │
│ cmp ebx, 0 │
└──────┬──────┘
│
┌───────┴───────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 基本块B │ │ 基本块C │
│ je 条件成立│ │ jne条件不立│
│ add eax, 1 │ │ sub eax, 1 │
└──────┬──────┘ └──────┬──────┘
└────────┬───────┘
▼
┌─────────────┐
│ 基本块D │
│ ret │
└─────────────┘
为什么重要:
- 识别循环结构
- 发现不可达代码(可能是混淆)
- 理解程序逻辑
- 找漏洞(比如路径未覆盖)
2.7 函数调用图
是什么:展示函数之间调用关系的图。
┌──────────┐
│ main │
└────┬─────┘
│
┌───────┼───────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ funcA │ │ funcB │ │ funcC │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└─────┬────┘ │
▼ │
┌────────┐ │
│ funcD │◄──────────┘
└────────┘
提取的信息:
- 谁是入口点?
- 哪些函数是关键函数?(被很多函数调用)
- 有没有孤立函数?
- 调用深度和路径
三、把这些概念串起来:一个完整流程
让我们用一个实际的恶意软件分析场景,展示所有这些概念如何协同工作:
场景:收到一个可疑的exe文件
步骤1:格式解析
- 识别是Windows PE文件
- 查看节表:有奇怪的节名,可能加壳
- 导入表:调用了
VirtualAlloc、WriteProcessMemory(可疑!)
步骤2:信息提取
- 字符串:发现
http://evil.com/payload.exe - 资源节:嵌入了一个加密的DLL
- 时间戳:2023年,但编译器的版本看起来像旧的(可能伪造)
步骤3:反汇编/反编译
- 将机器码转成汇编
- 进一步反编译成伪代码
- 发现关键逻辑:解密资源中的DLL,然后注入到explorer.exe
步骤4:构建中间表示
- 统一成P-Code或REIL
- 准备进行深入分析
步骤5:提取控制流图
- 识别出解密循环
- 发现反调试技巧(检测
IsDebuggerPresent) - 发现条件跳转:如果检测到调试器,跳转到无害代码
步骤6:提取函数调用图
main→check_debugger→decrypt_payload→inject_to_process- 清晰看到程序的核心逻辑链
步骤7:综合分析
- 这是一个恶意软件
- 它用反调试躲避分析
- 核心行为:下载payload,注入进程
- 归因:代码特征和某个已知恶意家族相似
二进制分析如何工作
-
加载(Loading)
proj = angr.Project("program.exe") # 解析文件格式,加载到虚拟内存 -
反汇编(Disassembly)
block = proj.factory.block(0x80483ed) # 把二进制转成汇编指令 -
构建CFG
cfg = proj.analyses.CFGFast() # 分析跳转指令,构建控制流图 -
提升到IR
# 把x86指令转成VEX IR,统一格式 -
分析(Analysis)
- 数据流分析:变量怎么传递
- 符号执行:探索所有可能路径
- 约束求解:找出满足条件的输入
-
反编译(Decompilation)
dec = proj.analyses.Decompiler(func) # 把IR转成高级语言结构
四、作为Java开发者,如何理解这些概念?
从你熟悉的东西出发
| Java世界 | 二进制世界 | 对应关系 |
|---|---|---|
.class 文件 | PE/ELF 文件 | 都是编译后的格式 |
javap -c 反汇编 | IDA Pro反汇编 | 都展示字节码/指令 |
| JD-GUI反编译 | Ghidra反编译 | 都恢复成伪代码 |
| 方法调用关系 | 函数调用图 | 都是调用关系 |
| if-else/循环 | 控制流图 | 都是执行路径 |
| Maven依赖 | 导入表 | 都是外部依赖 |
核心思维转变
| 维度 | Java开发 | 二进制分析 |
|---|---|---|
| 信息 | 有源代码,信息丰富 | 丢失了高级信息,只有机器码 |
| 理解方式 | 自上而下(读源码) | 自下而上(从指令反推) |
| 工具 | IDE、调试器 | 反汇编器、反编译器 |
| 难点 | 业务逻辑复杂 | 信息缺失、混淆保护 |
五、核心工具与其对应的概念
| 工具 | 主要功能 | 涉及的概念 |
|---|---|---|
| Detect It Easy | 格式识别、查壳 | 格式解析 |
| Binwalk | 固件解包、扫描 | 格式解析、信息提取 |
| strings | 提取字符串 | 信息提取 |
| Ghidra | 反汇编、反编译、分析 | 所有概念都涉及 |
| IDA Pro | 商业级反汇编/反编译 | 所有概念都涉及 |
| x64dbg | 动态调试 | 验证CFG、理解逻辑 |
| Radare2 | 开源逆向框架 | 所有概念都涉及 |
六、总结:一张图理解全部
原始二进制文件 (.exe/.elf)
↓
[格式解析] ─→ 元数据:入口点、节表、导入表
↓
[反汇编] ───→ 汇编代码
↓
[中间表示] ──→ 架构无关的IR (REIL/VEX/P-Code)
↓
[反编译] ───→ 伪代码(类C语言)
↓
[分析引擎]
├── [控制流图] ─→ 基本块、跳转关系、循环
├── [调用图] ───→ 函数调用关系
└── [数据流] ───→ 变量如何传递
↓
[信息提取] ──→ 字符串、API调用、资源
↓
[分析结果] ──→ 这是什么?它做什么?是恶意吗?
为什么需要这些分析
-
恶意软件分析
# 找出病毒做了什么 - 修改了哪些文件? - 连接了哪些网络? - 有没有隐藏功能? -
漏洞挖掘
# 找出程序漏洞 - 数组越界? - 缓冲区溢出? - 整数溢出? -
逆向工程
# 理解别人的程序 - 算法是什么? - 协议怎么实现的? - 有没有后门? -
代码优化
# 分析性能瓶颈 - 哪些函数执行最多? - 循环嵌套多深? - 内存访问模式?
真实检测示例
============================================================
分析二进制文件: ./test/constraint1.x86
============================================================
[1] 加载二进制文件...
✓ 架构: X86
✓ 入口点: 0x80482f0
✓ 字节序: Iend_LE
[2] 二进制元数据:
- 文件类型: ELF
- 加载地址: 0x8048000 - 0x804a01f
- 段数量: 3
段 0: 0x8048000-0x804857c (文件偏移: 0x0) [RX]
段 1: 0x8049f08-0x804a000 (文件偏移: 0xf08) [R]
段 2: 0x804a000-0x804a020 (文件偏移: 0x1000) [RW]
[3] 符号信息:
- deregister_tm_clones: 0x8048330
- register_tm_clones: 0x8048360
- frame_dummy: 0x80483c0
- main: 0x80483ed
[4] 构建控制流图 (CFG)...
发现 26 个函数
发现 53 个基本块
发现 66 条控制流边
[5] 函数列表 (前20个):
0x8048294: _init (大小: 35 字节, 基本块: 4)
0x80482d0: __gmon_start__ (大小: 28 字节, 基本块: 2)
0x80482e0: __libc_start_main (大小: 6 字节, 基本块: 1)
0x80482f0: _start (大小: 33 字节, 基本块: 1)
0x8048311: sub_8048311 (大小: 1 字节, 基本块: 1)
0x8048312: sub_8048312 (大小: 14 字节, 基本块: 1)
0x8048320: __x86.get_pc_thunk.bx (大小: 4 字节, 基本块: 1)
0x8048324: sub_8048324 (大小: 12 字节, 基本块: 1)
0x8048330: deregister_tm_clones (大小: 42 字节, 基本块: 5)
0x804835a: sub_804835a (大小: 6 字节, 基本块: 1)
0x8048360: register_tm_clones (大小: 55 字节, 基本块: 5)
0x8048397: sub_8048397 (大小: 9 字节, 基本块: 1)
0x80483a0: __do_global_dtors_aux (大小: 32 字节, 基本块: 4)
0x80483be: sub_80483be (大小: 2 字节, 基本块: 1)
0x80483c0: frame_dummy (大小: 44 字节, 基本块: 5)
0x80483e7: sub_80483e7 (大小: 1 字节, 基本块: 1)
0x80483ed: main (大小: 25 字节, 基本块: 1)
0x8048406: sub_8048406 (大小: 10 字节, 基本块: 1)
0x8048410: __libc_csu_init (大小: 97 字节, 基本块: 7)
0x8048471: sub_8048471 (大小: 2 字节, 基本块: 1)
[6] 函数调用图分析:
发现 0 个函数有调用关系
[main函数调用关系] @ 0x80483ed:
没有调用其他函数
[7] 控制流图分析:
main函数有 1 个基本块:
块 0: 0x80483ed (大小: 25 字节)
push ebp
mov ebp, esp
sub esp, 0x10
... 还有 8 条指令
[8] 基本块详细分析:
分析地址 0x80483ed 的基本块:
- 大小: 25 字节
- 指令数: 11
- VEX语句数: 49
- 跳转类型: 条件跳转/间接跳转
指令序列分析:
[0] 0x80483ed: push ebp
[1] 0x80483ee: mov ebp, esp
[2] 0x80483f0: sub esp, 0x10
[3] 0x80483f3: mov eax, dword ptr [ebp - 8]
[4] 0x80483f6: mov edx, dword ptr [ebp - 0xc]
数据流(寄存器使用):
- 读取寄存器次数: 1
- 写入寄存器次数: 1
[9] 路径分析:
探索前向路径...
- 活跃路径: 1
- 终止路径: 0
- 当前路径地址: 0x8048405
[10] 函数调用图摘要:
被调用最多的函数:
0x80483ed (main): 被 0 个函数调用
============================================================
分析完成!
统计: 26 个函数, 1 个调用关系
============================================================