逆向工程实战手册 第 4 章

第 4 章 静态分析

静态分析 = 不运行程序,从文件本身提取信息。它是逆向的第一站,80% 的简单题目在这一步就解完了。

📍 知识点地图 | 主题:静态分析 | 前置:第3章 | 后续:第5章 | 核心概念:侦察、radare2、IDA、Ghidra、交叉引用、比较点

4.1 静态分析思维链

1. 侦察: 类型/架构/字符串/导入 → 建立方向感
2. 定位: 从字符串、导入、入口点找到可疑函数
3. 反汇编: 看关键函数的指令流
4. 反编译: 用伪代码理解逻辑(IDA/Ghidra)
5. 交叉引用: 谁引用了我感兴趣的字符串/地址
6. 还原: 命名、加注释、恢复算法

4.2 侦察命令全家桶

# 基本信息
file binary
rabin2 -I binary

# 字符串(第一步永远先看这个)
strings binary | grep -iE "flag|secret|key|password|http"
rabin2 -z binary
rabin2 -zz binary          # 更详细(含宽字符串)

# 导入导出(猜功能)
rabin2 -i binary
rabin2 -E binary
objdump -T binary

# 符号
nm binary                  # 有符号时直接看函数名
readelf -s binary

4.3 radare2 上手(CLI 首选)

r2 -A binary               # 打开并自动分析
# 或分步: r2 binary 后手动 aaa

# 常用内部命令
aaa            # 自动分析(默认先 aaa,不要一上来 aaaa)
afl            # 列出函数
iz             # 列出字符串
iS             # 节区
is             # 符号
afl~main       # 过滤 main
iz~http        # 过滤 http 相关字符串
s main         # 跳到 main
pdf            # 反汇编当前函数
axt <addr>     # 谁引用了该地址(交叉引用)
px 64          # 十六进制查看
pd 20          # 反汇编 20 条
VV             # 可视化
q              # 退出

定位关键逻辑的标准套路

1. afl~main / afl~sym.      # 找 main 和符号函数
2. iz~flag / iz~error / iz~http   # 从字符串找线索
3. axt <字符串地址>          # 找谁引用了它
4. s <引用点> + pdf         # 深入分析该函数

r2 自动化(一次性输出)

r2 -A -q -c "afl;iz;ii;q" binary
r2 -A -q -c "s main; pdf" binary

4.4 IDA Pro(深度分析首选)

IDA 的核心优势:反编译器(Hex-Rays) 把汇编变成伪 C 代码,效率极高。

标准工作流(以 ida-pro-mcp 为例)

# 1. 启动服务器
powershell -File "C:\Users\1\src\reverse-skill\skills\ida-reverse\scripts\start.ps1"   # 输出 OK:72

# 2. 打开文件(大文件加超时)
powershell -File "C:\Users\1\src\reverse-skill\skills\ida-reverse\scripts\open.ps1" -Path "C:\target.exe" -TimeoutSeconds 600
# 输出 OK:文件名:session_id

# 3. 概况分析(第一步永远是 survey)
# idapro_survey_binary(detail_level="minimal")
#   → 架构、入口点、有趣字符串、导入分类(加密/网络/文件IO)、热门函数

# 4. 深入关键函数
# idapro_analyze_function(addr="函数名")   # 伪代码+字符串+常量+调用者
# idapro_decompile(addr="函数名")
# idapro_disasm(addr="函数名", max_instructions=50)

# 5. 交叉引用与数据流
# idapro_xrefs_to(addrs="关键地址")
# idapro_callgraph(roots=["关键函数"], max_depth=3)
# idapro_trace_data_flow(addr="关键地址", direction="backward", max_depth=5)

# 6. 记录优化
# idapro_set_comments(...)  /  idapro_rename(...)

# 7. 写报告

IDA 使用准则(直接来自实战经验)

  1. 先 survey 后深入——全局概览建立方向,别直接跳代码
  2. 持续加注释和重命名——分析中不断更新函数/变量名
  3. 跟踪交叉引用——发现有趣数据,立刻 xrefs_to 看谁引用
  4. 遇到混淆——先做预处理(字符串解密、导入哈希去除、控制流平坦化去除)再分析
  5. C++ STL——用 FLIRT/Lumina 先识别库函数,再分析业务逻辑
  6. 不要暴力破解——从反汇编推导解决方案
  7. 进制转换用工具idapro_int_convert),别手算

4.5 Ghidra(免费替代)

# 无头模式(headless)批量分析
analyzeHeadless project/ tmp -import binary -postScript script.py
  • 打开文件 → Auto Analysis → 从符号/字符串找 main → 反编译(Decompile 窗口)
  • 优势:免费、多架构、脚本化(Python/Jython)、插件丰富(D-810 去混淆、GOOMBA)
  • 劣势:反编译质量略逊 Hex-Rays

4.6 反编译器对比神器:dogbolt.org

把二进制上传 dogbolt.org,一次拿到 IDA/Ghidra/RetDec/angr 等十几种反编译结果并排对比。验证反编译猜想时的标准做法

4.7 静态分析的核心技巧

1. 从最终比较点倒推(最重要!)

验证类程序(crackme/flag check)的最终形态:

transform(input) == stored_target   → 逆向 transform,把 stored_target 反变换回来
transform(stored_target) == input   → flag 就是变换后的数据,直接正向套用

判断"比较方向"是解验证类题的第一关键。

2. 内存转储策略

让程序自己算出答案,然后在最终比较处断点 dump: b *main+OFFSET 停在最终比较,输入任意长度正确的输入,x/s $rsi dump 出计算结果。

3. 诱饵 flag 检测

多个"假目标"排在真检查之前。找最终的那个比较(不同的成功提示字符串的引用点),别在第一个比较就停。

4. GDB 调试 PIE

gdb ./binary
start                    # 先让 PIE 基址解析
b *main+0xca            # 用相对 main 的偏移下断点
run

4.8 静态分析检查清单

  • [ ] file 确认类型/架构
  • [ ] strings 全部可疑字符串(含 URL/路径/错误提示)
  • [ ] 导入表(加密函数?网络 API?文件操作?)
  • [ ] 找到 main / 入口点
  • [ ] 找到所有比较点 / 成功失败分支
  • [ ] 交叉引用关键字符串和常量表
  • [ ] 尝试 dogbolt 多反编译器对比
  • [ ] 命名 + 注释,输出伪代码级理解

4.9 完整实战演示:用 radare2 解一个 crackme

以 crackmes.one 上最常见的"输入 key 验证"类题目为例,展示完整静态解题流程。

题目crackme1(Linux ELF 64 位),运行后要求输入 key,错误输出 Wrong!,正确输出 Good!

Step 1: 侦察

$ file crackme1
crackme1: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped

$ rabin2 -z crackme1
vaddr=0x00400510 paddr=0x00000510 ordinal=000 sz=6 len=5 type=ascii string=Wrong!
vaddr=0x00400516 paddr=0x00000516 ordinal=001 sz=6 len=5 type=ascii string=Good!
vaddr=0x0040051b paddr=0x0000051b ordinal=002 sz=11 len=10 type=ascii string=Enter key:

$ rabin2 -i crackme1
imports: strcmp, puts, printf, scanf

分析not stripped(有符号)→ 直接看函数名;导入了 strcmp → 大概率是字符串比对。

Step 2: 找 main 和关键函数

$ r2 -A crackme1
[0x00400560]> afl
0x004004d0 1 16  sym.imp.strcmp
0x00400560 3 23  entry0
0x004005a6 4 99  sym.main
[0x00400560]> s sym.main
[0x004005a6]> pdf

反汇编输出(关键部分):

0x004005a6  lea rdi, [0x0040051b]      ; "Enter key:"
0x004005ad  call sym.imp.printf
0x004005b2  lea rsi, [rbp - 0x20]      ; 输入缓冲区
0x004005b6  lea rdi, [0x00400527]      ; "%s" 格式串
0x004005bd  call sym.imp.__isoc99_scanf
0x004005c2  lea rsi, [0x0040052a]      ; ← 关键!这是个地址
0x004005c9  lea rdi, [rbp - 0x20]      ; 我们输入的字符串
0x004005cd  call sym.imp.strcmp       ; ← 比较输入 vs 0x4052a
0x004005d2  test eax, eax
0x004005d4  jne 0x4005e4              ; 不等 → 跳向 Wrong
0x004005d6  lea rdi, [0x00400516]     ; "Good!"
0x004005dd  call sym.imp.puts

读法strcmp(我们的输入, 地址 0x0040052a 处的字符串)——key 就藏在这个地址。偏移 0x52a 在 .rodata(字符串区)。

Step 3: 读 key

[0x004005a6]> psz @ 0x0040052a        # 读该地址的字符串
s3cr3t_k3y_2024

答案:输入 s3cr3t_k3y_2024 即可通过。

Step 4: 验证

$ ./crackme1
Enter key: s3cr3t_k3y_2024
Good!

变体练习(举一反三)

变体 破解思路
比较前做 XOR 变换 找到变换函数,把 .rodata 的密文反变换回来
比较前对输入做哈希 找哈希算法,爆破/碰撞/直接看比较目标
strcmp 换成逐字节 cmp 找每个比较的立即数,拼出 key
字符串被拆分存在多处 axt 追踪每个片段的引用点
比较值运行时才计算 改走动态分析(第 5 章):断在比较点 dump

动手练习

  1. 拿一个 crackme(crackmes.one 上有大量分级样本),用 radare2 走完整套静态流程:侦察 → 找比较点 → 还原校验逻辑。
  2. axt 从字符串追踪到函数,练熟"字符串→引用→函数"这条黄金链路。
  3. 同一样本分别用 radare2、Ghidra、dogbolt 反编译,对比理解差异。

深入阅读

  • 仓库:skills/radare2/SKILL.md + references/cheatsheet.md(完整 r2 工作流与速查)
  • 仓库:skills/ida-reverse/SKILL.md(72 个 MCP 工具全列表 + 工作流)
  • 仓库:skills/reverse-engineering/tools.md(GDB/Ghidra/Unicorn/Binary Ninja/RetDec)
  • 仓库:skills/reverse-engineering/tools-advanced.md(高级:Ghidra 脚本、LIEF、patch 策略)
  • 仓库:skills/reverse-engineering/SKILL.md(Quick Wins + 初始分析 + 比较方向)