保姆级调试指南用GDBPwndbg实战分析CTF Pwn堆题的第一个malloc与free堆漏洞利用一直是CTF Pwn题中的难点与重点。许多初学者虽然掌握了堆管理的基本理论但在实际调试时却无从下手——他们知道chunk应该长什么样却不知道如何在内存中定位它了解bins的工作原理却不会在调试器中观察它们的实际状态。本文将带你用GDBPwndbg插件从零开始跟踪一次完整的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 *main14 # 假设malloc调用在main14 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字节chunkpwndbg x/4gx 0x555555756000 0x555555756000: 0x0000000000000000 0x0000000000000031 0x555555756010: 0x0000000000000000 0x0000000000000000关键观察点第一个qword0x0000000000000000是prev_size字段第二个qword0x0000000000000031是size字段其中低三位0x1表示PREV_INUSE标志位实际大小是0x30包含头部因为0x20用户请求0x10头部0x30使用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 *main28 # 假设free调用在main28 pwndbg continue pwndbg finish现在观察free后的变化pwndbg vis_heap_chunks 0x555555756000 0x0000000000000000 0x0000000000000031 ........1....... 0x555555756010 0x0000000000000000 0x0000000000000000 ................ 0x555555756020 0x0000000000000000 0x0000000000000000 ................ 0x555555756030 0x0000000000000000 0x0000000000020fd1 ................表面看起来似乎没变化这是因为小chunk被放入了fastbin。检查fastbinspwndbg fastbins fastbins [0x20] 0x555555756000 - 0x0 [0x30] 0x0 [0x40] 0x0 ...可以看到我们的chunk0x555555756000被链入了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相同这种内存重用特性是许多堆漏洞利用的基础。通过GDBPwndbg的实时观察可以更直观地理解攻击者如何操纵堆内存布局来实现任意地址读写等操作。