第 5 章 动态分析(GDB / 调试器)
静态分析给出"猜测",动态分析验证"事实"。动态分析 = 运行程序、下断点、看内存寄存器、跟踪调用。
📍 知识点地图 | 主题:动态分析 | 前置:第4章 | 后续:第6章 | 核心概念:GDB、断点、内存转储、ltrace/strace
5.1 什么时候必须动态
- 静态代码混淆严重(控制流平坦化、VM)
- 密钥/flag 运行时才计算
- 有反调试/反虚拟机检测
- 程序行为依赖运行时状态(环境、时间、随机数)
- 静态猜不出参数/返回值,需要观察
5.2 先上最简单的动态
# 库调用跟踪(很多题直接出答案)
ltrace ./binary
ltrace -e strcmp+strlen ./binary
# 系统调用跟踪
strace -f -s 500 ./binary
strace -f ./binary 2>&1 | grep -iE "open|read|write"
# 时间冻结(让随机性消失)
# LD_PRELOAD 一个替换 time() 的 .so
5.3 GDB 基础
gdb ./binary
| 命令 | 作用 |
|---|---|
info functions |
列出函数 |
info registers |
寄存器 |
break main / b *main+0x10 |
断点(PIE 用相对偏移) |
r / run <args> |
运行 |
s / si |
单步(函数级/指令级) |
n |
单步跳过 |
c |
继续 |
finish |
运行到函数返回 |
x/s $rsi x/20gx $rsp x/i $rip |
内存/栈/指令查看 |
p $rax p/x $rax |
打印寄存器 |
bt |
调用栈 |
info proc mappings |
内存布局(找 libc 基址) |
set follow-fork-mode child |
跟踪子进程 |
一行自动化(gdb -ex)
gdb -q -ex "b *main+0x123" -ex "run" -ex "x/s \$rsi" -ex "quit" ./binary
5.4 高级 GDB:pwndbg / GEF
安装后自动增强,CTF/pwn 首选:
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
# 或 GEF: git clone https://github.com/bata24/gef
pwndbg 常用增强命令:
vmmap # 内存映射(找 libc/stack/heap 地址)
heap # 堆信息
bins # 所有 bin 列表(tcache/fastbin/unsorted…)
telescope $rsp # 栈指针视图(16 个值)
got # GOT 表
5.5 GDB 技巧(实战总结)
1. 内存转储拿答案
b *main+0xca # 停在最终比较
run
x/s $rsi # 看程序算好的字符串
2. 条件断点
b strcmp if $rdi == 0x401000 # 仅当第一个参数是目标地址
b *0x401234 if $rax == 0x1337 # 仅当寄存器满足
3. 观察点(watchpoint)
watch *(int*)$rsi # 内存变化时断下(找谁改写了它)
watch *0x601080
4. GDB Python 脚本
# ~/.gdbinit 或会话内 source
(gdb) python
import gdb
gdb.execute("b *0x401234")
bp = gdb.parse_and_eval("$rax")
print(bp)
(gdb) end
经典用法:用 GDB 脚本做逐字节爆破——跑 N 次,每次输入不同候选,监控比较结果。
5. 反向调试 rr
rr record ./binary # 录制
rr replay # 回放,可反向单步
# gdb 中: rs (reverse-step) / rc (reverse-continue)
# 程序崩了往回找第一次写坏内存的位置,神器
6. LD_PRELOAD 侧信道(跨进程观察)
// hook memcmp/strcmp,按返回前比较字节数推断正确前缀
// 编译: gcc -shared -fPIC -o hook.so hook.c -ldl
// 使用: LD_PRELOAD=./hook.so ./binary
5.6 x64dbg(Windows 动态调试)
Windows 下主流动态调试器(替代 OllyDbg)。
| 快捷键 | 作用 |
|---|---|
| F9 | 运行 |
| F2 | 断点 |
| F7 / F8 | 单步进/单步过 |
| F4 | 运行到光标 |
| Ctrl+G | 跳到地址 |
| Ctrl+F2 | 重启 |
常用功能:内存窗口、寄存器窗口、调用栈、模块列表、Scylla(脱壳)、x64dbg 脚本自动化(命令行 + Python 插件)。
Windows 反调试点常用 API
IsDebuggerPresent / CheckRemoteDebuggerPresent
NtQueryInformationProcess(ProcessDebugPort)
NtSetInformationThread(ThreadHideFromDebugger)
PEB.BeingDebugged
5.7 lldb(macOS/iOS)
lldb ./binary
(lldb) breakpoint set --name main
(lldb) run
(lldb) register read
(lldb) x/10gx $rsp
(lldb) memory read --format hex --size 8 --count 16 $rsp
# 支持 Python 脚本
5.8 动态分析检查清单
- [ ] 先 ltrace/strace 快扫一遍
- [ ] 断在 main / 关键函数入口
- [ ] 观察关键比较点的寄存器/内存
- [ ] 尝试内存转储策略(让程序自己算)
- [ ] 需要时条件断点/watchpoint
- [ ] 反调试拦截时:patch 或 LD_PRELOAD
- [ ] 记录每一步的证据(命令、地址、输出)
5.9 完整实战演示:GDB 断点破解验证类程序
用上一章同一个 crackme1(strcmp 比较输入和硬编码 key),演示动态分析如何比静态更快。
方法 1:直接断在 strcmp(最快)
$ gdb -q ./crackme1
(gdb) b strcmp # 断在比较函数
(gdb) r # 运行,随便输入 "aaaa"
Breakpoint 1, 0x... in strcmp ()
(gdb) x/s $rdi # 第一个参数:我们的输入
0x7fffffffe1e0: "aaaa"
(gdb) x/s $rsi # 第二个参数:程序期望的值 ← key 在这
0x4052a: "s3cr3t_k3y_2024"
一行搞定(无交互):
$ gdb -q -ex "b strcmp" -ex "run" -ex "x/s \$rsi" -ex "q" ./crackme1
方法 2:断在比较点,看寄存器判定分支
(gdb) b *main+0x2e # 断在 test eax,eax 之后(地址按 main 偏移)
(gdb) r
(gdb) p/x $eax # strcmp 返回值
$1 = 0xffffff9f # 非 0 → 输入错误
方法 3:ltrace 秒杀(这类题最快的动态手段)
$ ltrace ./crackme1
printf("Enter key: ") = 10
scanf("%s", 0x7ffc...) = 1
strcmp("aaaa", "s3cr3t_k3y_2024") = -97 # ← 明文 key 直接露出来
puts("Wrong!") = 7
经验:带
strcmp/memcmp的验证类题目,ltrace一行出答案。这是"快速捡漏"最实用的例子。
方法 4:条件断点只抓特定比较
(gdb) b strcmp if strcmp 的第一个参数 == 0x4052a
# 等价写法(仅当 rsi 指向 .rodata 的 key 时才断)
(gdb) b strcmp if $rsi == 0x4052a
方法 5:内存转储策略(程序自己算 key 时用)
当 key 不是硬编码、而是程序算出来的:
(gdb) b *main+0x2e # 最终比较完成处
(gdb) r
(gdb) x/s $rsi # 程序算好的期望值
练习变体
| 变体 | 动态对策 |
|---|---|
| 程序先对 key 做变换再比较 | 断在比较点 dump 变换结果(方法 5) |
| 有反调试(ptrace) | set follow-fork-mode child + LD_PRELOAD 替换 ptrace |
| 随机 key(每次不同) | 断点观察+重放逻辑,或找种子 |
| 异步多线程比较 | set scheduler-locking on 锁定线程逐个看 |
动手练习
- 用 ltrace/strace 分析一个 crackme,记录它调用了哪些比较函数。
- 用 GDB 断在 strcmp 上,观察两个参数的地址和内容。
- 练习"内存转储":在一个验证类程序里,让程序算出 flag,然后在比较点 dump 出来。
- 给一个程序下条件断点(寄存器条件)和 watchpoint,观察触发时机。
深入阅读
- 仓库:
skills/reverse-engineering/tools.md(GDB 章节:PIE、自动化、内存查看) - 仓库:
skills/reverse-engineering/tools-advanced.md(高级 GDB:Python 脚本、rr、pwndbg/GEF、约束提取) - 仓库:
skills/reverse-engineering/anti-analysis.md(反调试检测点总表:ptrace、PEB、TLS 回调等) - 仓库:
skills/pwn-chain/SKILL.md(调试在利用链中的位置)