格式化字符串漏洞
仅供个人学习参考
格式化字符串漏洞原理
漏洞来自c语言的printf函数,因为printf函数不会检查参数的个数与格式化字符串的个数是否对应,也就是说,只要有格式化参数在,即使我们没有往printf函数传去参数,它也会默认在栈上向后(向高地址)寻找对应参数并根据格式化字符串来解析,举个栗子
1 | prinf("%s%s%s"); |
我们虽然没有给printf传参,但它依旧会把后面地址(高地址)上的内容作为参数来使用,栈的情况如下图
可以看到,格式化字符串的内容也是放在了栈上,因为我们没有传参,所以printf会把0xffffada8、0xffffada9、0xffffadaa作为参数,根据%s,将其解析并以字符串形式输出
有如下语句
1 | printf(buf); |
如果我们能控制buf的内容,就可以利用格式字符串漏洞
格式化字符串漏洞利用
泄露栈内存
就像上面举的例子,我们只要多写几个%s,就可将栈上的内,容解析并以字符串形式输出
1 | printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s") |
当然,并非所有栈上的的地址都可以被解析,所以上述方法可能会(且大概率会)造成程序崩溃当然这也是一种攻击手段,只不过作用是让服务器瘫痪
泄露任意地址内存
如果此时栈上存放着got表的某个表项的内容,我们可以利用%s来读取它,并进一步获取libc版本,如果栈上未存放,我们可以将需要的got表表项地址填入栈上(情况与允许的话),然后再读取内容。
修改栈内存
此时我们需要用到 %n 这个格式字符串,其作用是将已经打印出的字节数存放到相应的整形变量上
例:
1 | int a; |
我们将a换成所需修改的(栈上的)变量的地址,就可以修改栈内存
修改任意地址内存
原理与修改栈内存一样,换个目标地址就行
覆写小数字:
如果要覆写的数字较小,比如为2(小于四字节),这样即使在32位的情况下,目标地址也占用了4个字节那么%n输出的数据就不会小于4
解决办法:先填入所需覆写的数据大小对应的字符串(如果要覆写成2,就填’aa’),再填入目标地址
1 | payload=flat([b'aa',b'%k$naa',addr]) ## k记得根据实际情况改变 n后面的'aa'是为了补全四个字节 |
此时栈中情况如图
覆写大数字
如果要覆写的数字比较大,一般情况下我们无法一次性填入几十甚至几百的字符,此时我们可以利用格式化字符的宽度设置来覆写
1 | payload=flat([b'%20c',b'a',addr]) # 此时会在字符'a'前面补19个空格,使得其宽度为20 |
例题
攻防世界的CGfsb
用ida查看
没有栈溢出,发现存在格式化字符串漏洞,我们的目标是使得pwnme=8,然后即可获得flag
思路:可以往s写入pwnme的地址,然后再利用%n往pwnme写入8
exp如下:
1 | from pwn import * |
补:如何看是第几个参数?
gdb中利用x命令来看
x/20wx $esp
意为查看从esp开始的20个字长的内容(以16进制显示)
其中0x61616161 为我们先前利用fgets输入的aaaa,根据它和esp间隔的距离(字长数)来判断,图中esp(即为0xffffcff0)与0x61616161 相距10个字长(从左往右数,0xffffd018为0xffffcff0存储的内容,类推),所以填%10$n。
fmtstr
payload还有一种写法,即利用pwntools的fmtstr
1 | payload=fmtstr_payload(10,{0x0804A068:8}) |
10就是和esp间隔的距离(字长数),0x0804A068为需利用%n修改的地址,8为目标数值
64位格式化字符串漏洞
例题:【2017 UIUCTF pwn200 GoodLuck】https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/fmtstr/2017-UIUCTF-pwn200-GoodLuck
漏洞很明显
我们先将断点打在printf上,随便输个数据看看情况
先看寄存器,格式化字符串存放在rdi寄存器上
然后再看栈
可以看到flag放在0x7fffffffde80的地方(打的本地,自己写的flag),栈顶存放printf的返回地址,flag对应栈上的偏移为4(往下数第四行就有),加上寄存器的5(一共有6个来放参数嘛,rdi是第一个),所以总偏移是9 注意: 这里的flag存放在栈上,所以可以这么计算
exp如下:
1 | from pwn import * |
hijack retaddr
例题:【三个白帽 - pwnme_k0】https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/fmtstr/%E4%B8%89%E4%B8%AA%E7%99%BD%E5%B8%BD-pwnme_k0
发现relro全开,got表只读
ida查看,发现格式化字符串漏洞
而且有后门函数,考虑利用printf修改retaddress,跳转到后门函数
ret的地址通过old rbp的地址减去偏移量来得出,old rbp的地址通过printf泄露
gdb 断点下在printf函数,不难看出rbp偏移为6 ,ret的偏移为7
为什么是old rbp? 因为rbp保存的是old rbp的地址,%p取出的是rbp的内容,即old rbp的地址
计算得old rbp与ret的偏移为0x38 ( 0x7fffffffddf0-0x7fffffffddb8)
我们发现ret上存储的地址为0x400d74,与binsh的地址0x04008AA只有后三位不同,所以我们只用%hn修改两个字节
exp如下
1 | from pwn import * |