周末参加了今年的第二次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")
。下面是代码:
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了。
下面是代码:
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>