Unlink 例题分析
[ZJCTF 2019]EasyHeap
RELRO: Partial RELRO可以修改got表。没有开PIE
1 | int __fastcall __noreturn main(int argc, const char **argv, const char **envp) |
create_heap():
在分析堆的时候,最好养成按照顺序分析的习惯,先从 create 开始,通过 IDA 和程序运行共同分析:


这里程序先确定大小,然后程序会 malloc(size)大小的空间,可以输入内容。
edit_heap():


编辑操作,可以修改堆的大小和内容。
delete_heap():
查找索引,进行堆的删除。这里 free 后指针置 0,也就不会存在 uaf 漏洞。
exp 基本思路:
使用 堆溢出配合 Fastbin Attack 伪造 chunk,最终通过 GOT 表劫持来获取 shell。
程序的 edit 功能显然存在堆溢出漏洞,可以越界写入
构建基本函数:
1 | from pwn import * |
堆块布局与漏洞准备
1 | add(0x60,"aaaaa") # index 0 |
- 申请了 3 个大小为
0x60的 chunk(加上 0x10 的 chunk header,实际在内存中的大小为0x70)。 - 释放了 chunk 2。此时 chunk 2 会被放入大小为
0x70的 Fastbin 链表中。此时 Fastbin 结构大致为:head -> chunk 2 -> 0。
堆溢出与 Fastbin fd 篡改
1 | payload1 = b'/bin/sh\x00' + b'a'*0x60 + p64(0x71) + p64(0x6020ad) |
- 首先在 chunk 1 的开头写入 b’/bin/sh\x00’。为最后调用
system('/bin/sh')做准备。 - 用
b'a'*0x60填满 chunk 1 剩下的空间,随后溢出到物理相邻的 chunk 2 刚才被释放进入 Fastbin 的 chunk - 伪造 Chunk Header:p64(0x71) 恢复了 chunk 2 原本的 size 字段(0x70 大小 + PREV_INUSE 标志位 = 0x71),保证堆结构不被破坏。
- 篡改 fd 指针:
p64(0x6020ad)覆盖了 chunk 2 的fd指针。此时 Fastbin 的链表被恶意篡改为:head -> chunk 2 -> 0x6020ad。
利用错位伪造技巧将位于程序的 .bss 段 0x6020ad 伪造成 size
- 当 malloc 从 Fastbin 中取 chunk 时,会进行安全检查:检查该 chunk 的 size 字段是否属于当前的 Fastbin 大小分类。对于 0x70 的 Fastbin,它允许的 size 值是 0x70 到 0x7f,绕过 malloc 检查
分配伪造的 Chunk
1 | add(0x60,"aaaaa") # 重新申请回 chunk 2 |
- 第一次 add 把 chunk 2 从 Fastbin 中拿出来,此时 Fastbin 的 head 指向了我们伪造的地址 0x6020ad
- 第二次 add 中malloc 认为 0x6020ad 是一个可用的 chunk,并将其分配给我们,记为 chunk 3。
- 此时,我们拥有了对
.bss段的写入权限。
覆盖全局指针数组 (BSS 劫持)
1 | payload2 = b'a'*0x23 + p64(elf.got['free']) |
- 在这个分配到的伪造 chunk 中写入数据。
b'a'*0x23是一段 padding(偏移量),用于从0x6020ad + 0x10(用户数据区的起始地址)精确对齐到存储堆块指针的全局数组中index 0的位置。p64(elf.got['free'])将全局数组中 chunk 0 的指针覆盖为了free函数的 GOT 表地址。
GOT 表劫持
1 | payload3 = p64(elf.plt['system']) |
- 程序认为
index 0的指针指向的是一个正常的堆块,但实际上它现在指向的是free@GOT。 - 通过
edit(0),我们将system函数的 PLT 地址(也可以是实际地址)写入了free的 GOT 表项。 - 结果:从现在开始,程序中任何调用
free(ptr)的地方,实际上都会执行system(ptr)。
触发 System 函数获取 Shell
1 | delete(1) |
- 调用
delete(1),原本会执行free(chunk 1)。 - 由于
free被劫持,实际上执行的是system(chunk 1_address)。 - 还记得我们在步骤 2中在 chunk 1 的开头布置了什么吗?是
b'/bin/sh\x00'! - 因此,最终执行的命令是
system("/bin/sh"),成功拿到服务器 Shell,进入交互模式。
1 | from pwn import * |
更新: 2026-04-24 21:42:36
原文: https://www.yuque.com/idcm/wnemg9/uhki8xnzbah1ge81