第 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 使用准则(直接来自实战经验)
- 先 survey 后深入——全局概览建立方向,别直接跳代码
- 持续加注释和重命名——分析中不断更新函数/变量名
- 跟踪交叉引用——发现有趣数据,立刻
xrefs_to看谁引用 - 遇到混淆——先做预处理(字符串解密、导入哈希去除、控制流平坦化去除)再分析
- C++ STL——用 FLIRT/Lumina 先识别库函数,再分析业务逻辑
- 不要暴力破解——从反汇编推导解决方案
- 进制转换用工具(
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 $rsidump 出计算结果。
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 |
动手练习
- 拿一个 crackme(crackmes.one 上有大量分级样本),用 radare2 走完整套静态流程:侦察 → 找比较点 → 还原校验逻辑。
- 用
axt从字符串追踪到函数,练熟"字符串→引用→函数"这条黄金链路。 - 同一样本分别用 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 + 初始分析 + 比较方向)