保姆级调试指南:用GDB+Pwndbg实战分析CTF Pwn堆题的第一个malloc与free
堆漏洞利用一直是CTF Pwn题中的难点与重点。许多初学者虽然掌握了堆管理的基本理论,但在实际调试时却无从下手——他们知道chunk应该长什么样,却不知道如何在内存中定位它;了解bins的工作原理,却不会在调试器中观察它们的实际状态。本文将带你用GDB+Pwndbg插件,从零开始跟踪一次完整的malloc(0x20)和free操作,用实战视角观察堆内存的微观变化。
1. 实验环境搭建与基础准备
在开始调试前,我们需要一个可复现的实验环境。创建一个简单的测试程序heap_test.c:
#include <stdlib.h> int main() { void *p1 = malloc(0x20); free(p1); return 0; }编译时务必添加调试信息并关闭ASLR(地址空间随机化)以便观察:
gcc -g -no-pie -o heap_test heap_test.c echo 0 | sudo tee /proc/sys/kernel/randomize_va_space启动GDB并加载Pwndbg插件:
gdb ./heap_testPwndbg会自动加载并显示其特色的彩色界面。如果尚未安装,可以通过以下命令获取:
pip install pwndbg提示:调试堆问题时,建议始终在
malloc调用前设置断点,而非在main函数开始时就查看堆状态。未初始化的堆内存往往包含随机数据,容易造成误解。
2. 初始堆状态分析
在GDB中运行start命令让程序暂停在main函数入口。此时堆尚未初始化,我们可以先观察几个关键地址:
pwndbg> heap Arena not found. Maybe the binary is not dynamically linked or the memory layout is unusual.这个输出表明堆尚未初始化。通过vmmap命令查看内存布局:
pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/user/heap_test 0x555555754000 0x555555755000 r--p 1000 0 /home/user/heap_test 0x555555755000 0x555555756000 rw-p 1000 1000 /home/user/heap_test 0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.31.so ...可以看到此时还没有堆段(HEAP)。接下来我们在malloc调用前设置断点:
pwndbg> break *main+14 # 假设malloc调用在main+14 pwndbg> continue3. 第一次malloc(0x20)的详细跟踪
当程序停在malloc调用前时,我们单步执行(si)进入malloc函数内部。Pwndbg会自动高亮显示当前执行的汇编指令。继续执行直到完成malloc返回:
pwndbg> finish现在再次检查堆状态:
pwndbg> heap Top chunk | PREV_INUSE Addr: 0x555555756000 Size: 0x21000 pwndbg> arenas [ Main arena ]可以看到堆已经初始化,顶部chunk大小为0x21000。查看我们分配的0x20字节chunk:
pwndbg> x/4gx 0x555555756000 0x555555756000: 0x0000000000000000 0x0000000000000031 0x555555756010: 0x0000000000000000 0x0000000000000000关键观察点:
- 第一个qword(0x0000000000000000)是
prev_size字段 - 第二个qword(0x0000000000000031)是
size字段,其中:- 低三位0x1表示
PREV_INUSE标志位 - 实际大小是0x30(包含头部),因为0x20用户请求+0x10头部=0x30
- 低三位0x1表示
使用Pwndbg的vis_heap_chunks命令可以更直观地查看:
pwndbg> vis_heap_chunks 0x555555756000 0x0000000000000000 0x0000000000000031 ........1....... 0x555555756010 0x0000000000000000 0x0000000000000000 ................ 0x555555756020 0x0000000000000000 0x0000000000000000 ................ 0x555555756030 0x0000000000000000 0x0000000000020fd1 ................4. free操作后的堆状态变化
继续执行到free调用并完成:
pwndbg> break *main+28 # 假设free调用在main+28 pwndbg> continue pwndbg> finish现在观察free后的变化:
pwndbg> vis_heap_chunks 0x555555756000 0x0000000000000000 0x0000000000000031 ........1....... 0x555555756010 0x0000000000000000 0x0000000000000000 ................ 0x555555756020 0x0000000000000000 0x0000000000000000 ................ 0x555555756030 0x0000000000000000 0x0000000000020fd1 ................表面看起来似乎没变化?这是因为小chunk被放入了fastbin。检查fastbins:
pwndbg> fastbins fastbins [0x20] 0x555555756000 -> 0x0 [0x30] 0x0 [0x40] 0x0 ...可以看到我们的chunk(0x555555756000)被链入了0x20大小的fastbin。虽然vis_heap_chunks显示没变,但实际chunk内部已经存储了fastbin的链表指针:
pwndbg> x/4gx 0x555555756000 0x555555756000: 0x0000000000000000 0x0000000000000031 0x555555756010: 0x0000000000000000 0x0000000000000000这里似乎没有看到指针?这是因为fastbin是单链表,且当前只有一个chunk,所以fd指针为NULL。如果我们再分配并释放一个0x20的chunk,就能看到链表形成:
void *p1 = malloc(0x20); void *p2 = malloc(0x20); free(p1); free(p2);调试时可以看到:
pwndbg> fastbins fastbins [0x20] 0x555555756040 -> 0x555555756000 -> 0x05. 关键调试技巧与常见误区
在实际调试堆问题时,有几个关键技巧和常见误区需要注意:
内存对齐观察:
- 64位系统下chunk大小总是16字节对齐
- 使用
p/x ((size_t)ptr & ~0xF)可以快速计算chunk头地址
标志位解读:
# Pwndbg中的size解析 def parse_size(size): return size & ~0x7, size & 0x7常见调试误区:
- 忘记在malloc/free后查看状态
- 误读size字段(忘记包含头部大小)
- 忽略fastbin的LIFO特性
- 在多线程环境下未注意arena变化
实用Pwndbg命令:
heap -v # 详细堆信息 bins # 查看所有bins parseheap # 解析堆布局 telescope [addr] # 查看内存内容
6. 进阶:从调试到漏洞利用
理解了基础的内存分配与释放后,我们可以进一步探索如何利用堆漏洞。以简单的Use-after-Free为例:
void *p = malloc(0x20); free(p); malloc(0x20); // 可能重新获得p指向的内存调试时可以观察到:
# 第一次malloc后 pwndbg> p p $1 = (void *) 0x555555756010 # free后 pwndbg> fastbins [0x20] 0x555555756000 -> 0x0 # 第二次malloc后 pwndbg> p p2 $2 = (void *) 0x555555756010 # 与p相同这种内存重用特性是许多堆漏洞利用的基础。通过GDB+Pwndbg的实时观察,可以更直观地理解攻击者如何操纵堆内存布局来实现任意地址读写等操作。