逆向工程实战手册 第 5 章

第 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 断点破解验证类程序

用上一章同一个 crackme1strcmp 比较输入和硬编码 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 锁定线程逐个看

动手练习

  1. 用 ltrace/strace 分析一个 crackme,记录它调用了哪些比较函数。
  2. 用 GDB 断在 strcmp 上,观察两个参数的地址和内容。
  3. 练习"内存转储":在一个验证类程序里,让程序算出 flag,然后在比较点 dump 出来。
  4. 给一个程序下条件断点(寄存器条件)和 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(调试在利用链中的位置)