4、GDB核心转储分析:生成core dump、使用gdb分析崩溃现场、查看调用栈与寄存器状态
程序崩溃是漏洞分析与逆向工程中最常见的切入点。无论是段错误(Segmentation Fault)、非法指令(Illegal Instruction)还是断言失败,崩溃瞬间的进程内存映像、寄存器状态和调用栈都是还原事故现场的第一手证据。GDB 的核心转储(Core Dump)分析能力,正是将这些静态的转储文件转化为动态执行轨迹的关键手段。本章将系统讲解如何生成 core dump、如何用 GDB 加载并分析崩溃现场,以及如何从调用栈与寄存器中提取对漏洞利用或缺陷定位至关重要的信息。
4.1 核心转储的生成机制与配置
Core dump 是操作系统在进程异常终止时,将进程的地址空间、寄存器上下文、打开的文件描述符以及信号处理信息等写入磁盘的文件。在 Linux 系统中,core dump 的生成受 ulimit -c 和 /proc/sys/kernel/core_pattern 双重控制。默认情况下,多数发行版出于磁盘空间和安全考虑会禁用 core dump,因此第一步是确认并开启该功能。
# 查看当前 core 文件大小限制(0 表示禁用)
ulimit -c
# 临时允许生成 core 文件(不限制大小)
ulimit -c unlimited
# 永久生效:写入 /etc/security/limits.conf
# * soft core unlimited
# * hard core unlimited
# 查看 core 文件的命名与存储路径
cat /proc/sys/kernel/core_pattern
# 常见输出:core 或 /var/lib/systemd/coredump/core.%e.%p.%t
对于漏洞分析场景,建议将 core_pattern 设置为包含可执行文件名、PID 和时间戳的格式,便于多份 core 文件的管理。例如:
echo '/tmp/cores/core.%e.%p.%t' > /proc/sys/kernel/core_pattern
此外,还需要注意 fs.suid_dumpable 参数。当调试 setuid 程序时,内核默认禁止生成 core dump 以防止敏感信息泄露。若确需分析,可临时设置 echo 1 > /proc/sys/fs/suid_dumpable,但生产环境务必保持默认值 0。
4.2 使用 GDB 加载并定位崩溃指令
GDB 加载 core dump 的基本语法为 gdb [可执行文件] [core文件]。加载后,GDB 会自动定位到崩溃时的指令位置,并显示崩溃原因。以下是一个典型的崩溃分析会话:
$ gdb ./vuln_program /tmp/cores/core.vuln_program.12345.1700000000
GNU gdb (Ubuntu 12.0.90) 12.0.90
...
Core was generated by `./vuln_program'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000401234 in foo (p=0x0) at vuln.c:12
12 return *p + 1;
(gdb) info registers
rax 0x0 0
rbx 0x7fffffffe3a0 140737488347040
rcx 0x0 0
rdx 0x0 0
rsi 0x0 0
rdi 0x0 0
rbp 0x7fffffffe2c0 0x7fffffffe2c0
rsp 0x7fffffffe2a0 0x7fffffffe2a0
rip 0x401234 0x401234 <foo+16>
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) bt
#0 0x0000000000401234 in foo (p=0x0) at vuln.c:12
#1 0x0000000000401278 in bar () at vuln.c:20
#2 0x00000000004012b0 in main () at vuln.c:30
(gdb) frame 1
#1 0x0000000000401278 in bar () at vuln.c:20
20 foo(ptr);
(gdb) info locals
ptr = 0x0
(gdb) list
15 void bar() {
16 int *ptr = NULL;
17 // ... 某些逻辑
18 foo(ptr);
19 }
从上述输出可以清晰看到:崩溃发生在 foo 函数中,原因是空指针解引用(rdi=0x0)。调用栈显示 bar 函数将空指针传给了 foo。此时,我们可以进一步使用 disassemble 查看崩溃指令的汇编上下文:
(gdb) disassemble foo
Dump of assembler code for function foo:
0x0000000000401220 <+0>: push rbp
0x0000000000401221 <+1>: mov rbp,rsp
0x0000000000401224 <+4>: mov QWORD PTR [rbp-0x8],rdi
0x0000000000401228 <+8>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040122c <+12>: mov eax,DWORD PTR [rax] <-- 崩溃点
0x000000000040122e <+14>: add eax,0x1
0x0000000000401231 <+17>: pop rbp
0x0000000000401232 <+18>: ret
End of assembler dump.
通过反汇编,我们确认崩溃指令是 mov eax, DWORD PTR [rax],即从 rax(值为 0)指向的地址读取 4 字节数据,触发段错误。这种“源码 + 汇编”双重验证的方式,是漏洞分析中确认根因的黄金标准。
4.3 深入分析调用栈与寄存器状态
调用栈(Backtrace)记录了崩溃时刻的函数调用链,是理解程序执行路径的核心。GDB 提供多种命令来遍历和分析栈帧:
| 命令 | 作用 | 典型场景 |
|---|---|---|
bt / backtrace | 打印完整调用栈 | 快速了解崩溃点与调用链 |
bt full | 打印调用栈及每帧的局部变量 | 需要查看各层函数的参数与局部状态 |
frame N | 切换到第 N 帧 | 检查特定函数的上下文 |
info args | 查看当前帧的参数 | 确认传入的异常值 |
info locals | 查看当前帧的局部变量 | 定位变量赋值异常 |
up / down | 向上/向下切换栈帧 | 逐层检查调用关系 |
寄存器状态分析同样关键。在 x86-64 架构下,重点关注以下寄存器:
- RIP:指令指针,指向崩溃指令的地址,是反汇编的起点。
- RSP:栈指针,指向当前栈顶,可用于检查栈溢出或栈布局。
- RBP:帧基址指针,用于定位当前函数的栈帧边界。
- RDI/RSI/RDX/RCX/R8/R9:前六个整数参数传递寄存器,崩溃时往往包含关键指针或长度值。
- RAX:返回值寄存器,也常作为临时变量使用,崩溃时可能保存了被解引用的地址。
例如,在分析栈缓冲区溢出漏洞时,RSP 的值与返回地址在栈上的偏移关系至关重要。通过 x/20gx $rsp 可以查看栈内存内容,结合反汇编计算返回地址的存储位置,从而判断溢出是否覆盖了返回地址。
memcpy、strcpy)时,调用栈可能只显示库函数名。此时应使用 frame 1 或 frame 2 切换到上层应用代码帧,查看是哪个业务函数调用了库函数,以及传入的参数值。这往往是漏洞的真正入口。
4.4 案例拆解:从崩溃现场到漏洞根因
下面通过一个完整的案例,演示如何从 core dump 出发,逐步定位一个真实的堆缓冲区溢出漏洞。
背景与约束:某网络服务程序 httpd_srv 在处理 HTTP 请求时崩溃,系统日志显示 SIGSEGV。该服务使用 C 语言编写,启用了 ASLR 和 NX,但未启用栈保护(-fno-stack-protector)。我们获得了崩溃时的 core 文件。
初始现象:GDB 加载后显示崩溃在 memcpy 函数内部,调用栈如下:
#0 __memcpy_avx_unaligned () at ../sysdeps/x86_64/multiarch/memcpy-avx-unaligned.S:145
#1 0x0000000000401a2b in parse_request (req=0x603010, len=0x200) at httpd.c:85
#2 0x0000000000401b78 in handle_connection (fd=4) at httpd.c:120
#3 0x0000000000401c40 in main () at httpd.c:150
关键数据:切换到第 1 帧,查看局部变量:
(gdb) frame 1
(gdb) info locals
buf = 0x7fffffffe1a0 "GET / HTTP/1.1\r\nHost: example.com\r\n"
dest = 0x603010
size = 0x200
(gdb) p/x $rdx
$1 = 0x200
(gdb) p/x $rsi
$2 = 0x7fffffffe1a0
(gdb) p/x $rdi
$3 = 0x603010
分析判断:从寄存器可以看出,memcpy(dest=0x603010, src=栈上buf, size=0x200)。但 dest 指向堆地址(0x603010),而 size 为 0x200(512 字节)。进一步查看 parse_request 的源码与反汇编:
(gdb) list httpd.c:80
80 void parse_request(char *req, size_t len) {
81 char *dest = malloc(256);
82 memcpy(dest, req, len); // 漏洞:len 未校验
83 // ...
84 }
问题清晰了:malloc(256) 分配了 256 字节的堆缓冲区,但 memcpy 复制了 512 字节,导致堆溢出。崩溃发生在 memcpy 内部,是因为复制到堆元数据区域时破坏了堆结构,后续的 free 或 malloc 操作触发了段错误。
实际操作步骤:
- 使用
x/20gx 0x603010查看堆缓冲区内容,确认溢出数据已覆盖相邻堆块。 - 使用
p &((struct malloc_chunk*)0x603010-1)->size检查堆块头部,发现 size 字段被篡改。 - 通过
info proc mappings查看堆段范围,确认 0x603010 位于堆段内。 - 结合反汇编确认
len参数来自 HTTP 请求头中的Content-Length字段,未做上限校验。
结果对比:修复方案是将 memcpy 改为 memcpy(dest, req, min(len, 256)),并在调用前校验 len 是否超过缓冲区大小。修复后,使用相同请求测试,服务不再崩溃。
风险与代价:该漏洞可被远程利用,攻击者通过构造超长 Content-Length 实现堆溢出,可能进一步利用堆元数据覆盖实现任意代码执行。由于服务以 root 权限运行,风险等级为严重。复盘经验:所有涉及外部输入的长度参数,必须在复制前进行边界校验;同时建议编译时启用 -fstack-protector-all 和 -D_FORTIFY_SOURCE=2 作为纵深防御。
4.5 常见误区与总结复盘
在 core dump 分析中,初学者常犯以下错误:
- 忽略 core_pattern 配置:修改了
ulimit -c但未检查core_pattern,导致 core 文件被 systemd 接管或写入非预期路径。 - 直接分析库函数崩溃帧:在
memcpy或strcpy等库函数处崩溃时,应切换到上层应用帧,而不是在汇编层面盲目猜测。 - 混淆栈溢出与堆溢出:通过
RSP与返回地址的关系判断栈溢出;通过堆块头部与相邻块内容判断堆溢出。两者分析方法完全不同。 - 忽略寄存器中的指针值:
RDI、RSI等寄存器保存的指针往往直接指向崩溃相关的内存区域,应优先检查。 - 未保存原始 core 文件:分析过程中可能修改了进程状态(如调用
set命令),导致 core 文件被污染。建议先复制一份原始 core 文件再操作。
总结复盘,core dump 分析的核心方法论可以归纳为:先看信号与崩溃指令,再看调用栈定位函数,然后检查寄存器与内存,最后结合源码与反汇编确认根因。这一流程适用于绝大多数崩溃类漏洞的初步分析。对于更复杂的场景(如堆元数据破坏、use-after-free),还需要结合堆内存布局和分配历史进行深入分析,这将在后续章节中展开。
set pagination off 关闭分页,避免长输出时交互中断。同时,将常用的分析命令写入 .gdbinit 文件,例如 define btfull 宏自动执行 bt full + info registers,可显著提升分析效率。