本篇主要讲述两个知识点: 格式化字符串任意写和泄露基址
我们在最初的格式化字符串漏洞学习中 已经掌握了查看偏移和篡改地址的数据的能力
但是如果是篡改puts函数的got表呢?
我们知道 动态链接的情况下 当我们调用一个函数时
他会寻址其got表内存储的真实地址(即对应函数在libc文件中的地址) 从而成功调用
如果我们将其got表内存储的真实地址修改为其他函数的真实地址
那么当程序调用原函数时 就相当于调用了篡改后的函数
payload = fmtstr_payload(offset, {puts_got:system_addr }) |
以上述payload为例 假设我们需要修改puts函数的got表 使其为system函数的地址
那么我们就可以这样构造payload(这里注意一下,fmtstr这个工具是会自己补齐字长的 这将影响到我们下文中一道例题 现在留个意就行了)
ps:并且这个工具默认生成的是32位情况下 如果需要切换到64位 需要自己手动添加
context.arch = "amd64" |
但是一般题目除非出题人好心 不然真实地址还是得我们自己泄露的吧
那如何一并利用格式化字符串泄露函数的真实地址呢?
还记不记得 格式化字符串最开始的漏洞利用 就是泄露栈上的内容 如果我们将got表写入栈上 那是不是也可以通过格式化字符串漏洞将其泄露出来?
payload = b"%n$s".ljust(16,b"\x00")+p64(puts_got) |
这里有几点要注意一下 一个是n 注意是地址所在的偏移
还有一点是格式化字符这里选择的是s
最后一个疑惑在于为什么要用\x00补齐16个字节 这个我也不懂 死记就完事了(你也可以试试不补齐 然后看会泄露个啥出来)
好了 接下来用一题例题来演示一下 方便理解(例题还涉及到了fini劫持的知识点 不懂的话建议先去看另外一篇)
HNCTF2022-[WEEK2]fmtstr_level2
附件有给libc文件 猜测要用到泄露基址
checksec看一下进制和保护
有canary 要么泄露绕过 要么就不能栈溢出了
再看一下程序
int __cdecl main(int argc, const char **argv, const char **envp) |
唯一看起来有价值的就只有main函数了 没有任何的后门函数 甚至buf的字节也不够栈溢出
但是注意看 最后的puts输出的字符串有sh
那么可以猜测出题目的解法是修改got表
结果我们只有一次格式化字符串任意写的机会 好像并不能满足泄露地址后再修改got表的需求
但是如果我们将fini_array的值改为main函数 那么程序结束后 就会重新返回到main函数 那么我们就有了第二次利用格式化字符串的机会
于是解题思路可以分为两步
1.修改fini_array和泄露函数真实地址
2.将puts_got修改为system函数
那么接下来开始编写exp
gdb查看了偏移以后 发现我们输入的第一个字长的数据位于偏移6的地方
第一个payload的难点在于搞清楚两个格式化字符串的偏移和payload的结构
payload = fmtstr_payload(6, {fini_addr:main_addr}) |
按照我们上文所说的是不是应该这么构造payload 但是你会发现最后泄露出来的地址是
aaaaba+fini_array的地址(0x4031f0)
前面的aaaaba是什么东西?
我们打印出fmtstr构造的数据看看
可以看到aaaaba出自这里 这里就是我们上文所说到的fmtstr的自动补齐一个字长
而后面的\x00也是为了传送地址(但是地址只有三字节 所以需要5个\x00才补齐一个字长)
那么说回我们刚才的错误 其原因在于我们需要将格式化字符串放在一起 地址放在一起
才能两次利用一个漏洞点
所以 正确的payload应该把aaaaba替换成泄露地址的格式化字符串
payload = b"%182c%11$lln%91c%12$hhn%47c%13$hhn%14$sa\xf01@\x00\x00\x00\x00\x00\xf11@\x00\x00\x00\x00\x00\xf21@\x00\x00\x00\x00\x00" |
但是这里我们会发现 recv接收到的数据太多了 像ret2libc中的接收办法显然是会出错的
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00")) |
这里用到[-6:] 只接收后六个字节
那么我们此时成功进行了fini劫持 我们再输入io.recv()就会发现又接收到了main函数开始时puts的那些字符串
第二次的payload就简单至极了 最后放下完整的exp吧
from pwn import* |