格式化字符串漏洞
格式化字符串漏洞
0x00 格式化字符串介绍
格式化字符串(format string),是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,通俗讲,就是将计算机内存表示的数据转化为人类可读字符串格式。
在C语言中,有如下会进行格式化字符串输出的函数
fprint() print() sprintf() snprintf() dprintf() vfprintf() vprint() vsprintf() vsnprintf() vdprintf()
c常用的还是printf
格式化字符串是由普通字符(包括“%”)和转换规则构成的字符序列,普通字符被原封不动的复制到输出流中。转换规则则根据与实参对应的转化指示符对其进行转换,然后将结果写入到输出流
转换规则
**%[parameter][flags][width][.precision][length]type ** 举其中两个例子
parameter 用来指定某个参数
eg:
结果:
length指出浮点型参数或整型参数的长度
hh:输出1byte
h:输出2byte
l:输出4byte
ll:输出8byte
基本的格式化字符串参数
%c:输出字符,配上%n可用于向指定地址写数据。例子如下:
输出结果:
与%c有同样效果的格式化字符串还有%d和%s,它们都可以输出大量字符
%d:输出十进制整数,配上%n可用于向指定地址写数据。
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。%p在格式化字符串漏洞中用来泄露信息看以下例子
Output: 0x12345678
Output:0xffcf9b48
%s:%s 可以获取变量对应地址的数据,即将栈中数据当作一个地址,获取这个地址中的数据,存在0截断假设存在如下程序
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
%n转换指示符将当前已经成功写入流或者缓冲区的字符个数存储到参数指定的位置中,是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。
看以下例子
0x01 漏洞基本原理
eg1:
我们知道栈是由高地址向地址增长的,printf函数的参数是逆序被压入栈中,那么参数在栈中出现的位置顺序与printf参数的位置顺序是一致的
按照规定,"%s %d %s"
这一串格式化字符串,会被一个一个字符读取,读到%
匹配参数,并输出。
所以结果如下
以上是正常的操作方式,如果我们在参数不变的情况下对格式化字符串进行修改,那么便会造成漏洞
在%s %d %s
之后加上%x %x %x %3$s
,如下
在参数只有3个的情况下,我们打印了7个值(算上\n
),前3个参数对应printf给的3个参数,但是后4个对应0xffffd570 0xffffd574 0xffffd578 0xffffd56c 这4个栈内存空间。(%3$s是第三个参数的意思),所以此番操作已是栈数据泄露
eg2:
再有一个例子,这个例子的格式化字符串变为可控
可以看到把本不该输出的内存数据输出了
所以格式化字符串的致命之处在于格式化字符串要求的参数与实际的参数不匹配
0x02 漏洞利用
利用格式化字符串漏洞,可以使程序崩溃、栈数据泄露、任意地址内存泄露、栈数据覆盖、任意地址内存覆盖
0x0 栈数据泄露
前面基本原理有涉猎,再看另一示例
根据前面所讲,如此泄露十分简单易懂
0x1 任意地址内存泄露
%s旨在取出指针的内容,那么构造一个含got表项的格式化字符串,然后找到其在栈中的位置,那么就可以泄露函数地址
确定参数位置
还是上一题的代码,我们输入
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
目的是为了找到输入的字符串在栈中的具体位置
发现0x41414141在第13个参数的位置,所以明确了输入的字符所在位置
当然用fmtarg命令也可以很快找出参数位置,需要进行调试
尝试泄露地址:
当确定输入参数的位置后,我们可以输入got表地址配合%13$s来输出函数真实地址,操作如下:
找一下print@got的地址
但是我发现输入的地址有问题
输入的0x0804c00c
变成了0x2e0804c0
,是'\x0c'不见了
具体是什么原因也不是很清楚,应该不是不可见字符的原因,因为我试了其他的并没有问题,反正'\x0c'是被程序忽略了。
那没有办法只能更换其他的got地址
学了一个新的查找got方式,这样就不用在ida找了
我们试一下__libc_start_main
的got
发现没有问题
好,exp安排
确实拿到了地址,检验一下,没有问题!
这就是格式化字符串的任意地址泄露
0x2 栈数据覆盖
据前面所提到的%n参数,这个参数会将字符个数存储到参数指定的位置中,那么可以利用该参数覆盖栈中的一些数据。
还是拿上面的代码距离
通过覆盖我们可以把arg2改成任意数字,好比如0x00000018
那么如何构造格式化字符串?
风水构造
我们的思路是:这串格式化字符串,要包含有arg2在栈中的地址,然后我们用$n的方式去定位这个地址在栈中位置,再利用%n把字符数赋值给该地址指向的内容,就是把arg2的值改了。
通过调试我们知道输入的串放在"%15$p"及后面
而且我们知道了arg2在栈中的位置0xffffd4b8
,即\xb8\xd4\xff\xff
这样格式化字符串已占去4字节
再加上填充,比如%8x表示8字符宽的十六进制数,占8字节,比如%12d,占12字节
那么加起来4+8+12就刚好是24
那么这串格式化字符串就长这样
放进文本
我们可以看到0xffffd4b8
的位置变成了0x00000018,arg2的值已经被我们覆盖
0x3任意地址内存覆盖(栈数据覆盖进阶)
覆盖为小值
当然如果按照上面的构造覆盖的值最小只能是4,因为必须有地址,所以必须占4字节。
那么如果把地址放后面呢%n会把它算进个数吗
类似于"AA%17$nA"+"\xb8\xd4\xff\xff"
(两个A在于让n计数为2,第17个是因为地址放后,前面有两个四字节)
同时因为地址放到后面所以$n后要补A跟前面的字符串凑成4的倍数,这样地址才能存入一个完整的单元(4字节)
调试,成功覆盖
覆盖为1就把A挪到后边,像"A%17$nAA"+"\xb8\xd4\xff\xff"
覆盖为大值
要知道当要写入一个地址,类似0xffffd4b8,这个数的值是很大的,如果直接按照字符输入%n写入个数的方法会造成好多的字符占用,无疑会让程序崩溃。
所以要换一种思路覆盖——逐字节的覆盖
也就是说更改一个4字节的A地址,我们可以用四个跳转地址,指向A地址的每一个字节,然后再覆盖
如果要把0xffffd4b8
的值从0x88888888
改成0x12345678
输入AAAABBBBCCCCDDDD确定位置
构造的字符串如下:
前四个字节分别是四个跳转地址,占16字节
使用hhn的形式,只会保存低字节:
0x78(16+104=102 ->0x78)、0x65(120+222=342 ->0x0156 ->0x56)、0x43(342+222=564 -> 0x0234 ->0x34)、0x12(564+222 = 786 -> 0x312 -> 0x12)
注意:真实情况下,不同机子的地址会变化,需要先泄露一个栈地址,然后再根据泄露栈地址推算栈上的其他地址
0x4 局限
以上的操作都是对于 32位linux系统的,对于64位linux系统,由于有寄存器的参与实际操作会有不同
比如泄露栈上地址的参数位置就不一样,因为多了六个寄存器在存储参数,分别是RDI、RSI、RDX、RCX、R8、R9
也正是如此,我们如果还要修改arg2就不现实了,因为他被存入寄存器中,我们没法再如前面通过地址定位然后修改。
__EOF__

本文链接:https://www.cnblogs.com/damoxilai/p/16056561.html
关于博主:网安小萌新一名,希望从今天开始慢慢提高,一步步走向技术的高峰!
版权声明:达摩西来
声援博主:达摩西来
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!