格式化字符串漏洞
格式化字符串漏洞
一、X86
二、格式化字符串漏洞原理
格式化串漏洞和普通的栈溢出有相似之处,但是还是有所不用。都是利用程序中存在的漏洞劫持程序流程的。
a、什么是格式化字符串?
像printf()、fprintf()等一系列的函数可以按照一定的格式将数据进行输出,举个例子:
1 | printf("Hello %s","World"); |
执行完这个函数会返回字符串:Hello World
这个printf函数的第一个参数就是格式化字符串,它用来告诉程序将数据以什么格式输出。
printf()函数的一般形式为:printf(“format”,输出列表),关注点在format上,它的结构是:
%[ 标志 ] [ 输出最小宽度] [.精度] [长度] 类型,其中跟格式化字符串漏洞有关系的主要有下列几点:
1、输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多余定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补空格或0.
比如:
1 | printf("%2d",123); //结果为123 |
2、类型
1 | %c:输出字符,配上%n可以向指定地址写数据 |
正常使用printf函数:
1 | char str[100]; |
带有漏洞的:
1 | char str[100]; |
对比两段代码,第二个程序中的printf函数参数我们是可控的,在控制了format参数之后结合printf()函数特性就能构造相应的攻击。
printf函数的参数个数不固定
可以利用这一特性进行越界数据的访问。
先访问一个正常的程序
1 |
|
编译运行:
1 | gcc -m32 -fno-stack-protector -o a a.c 32位程序 |
接下来试试增加一个printf()的format参数,修改后编译运行:
1 | root@zhuyuan-vm:/home/zhuyuan/pwn/format# gcc -fno-stack-protector -o a a.c |
GCC会报警告,但是还是能通过运行,这时候看比原来多输出了一个0,用gdb调试看看。
在printf函数处下断点,运行观察一下
此时程序停在printf函数入口处0xf7c57a70
查看此时的栈布局:
1 | pwndbg> x/10x $sp |
可以看到确实在栈空间内存在0x00000000
1 | pwndbg> x/20x $sp |
只要能够控制format参数,我们就可以一直读取内存数据。
再举个例子实验一下
修改程序,gdb调试
1 |
|
运行程序,输入AAAA%08x%08x%08x%08x%08x%08x
1 | zhuyuan@zhuyuan-vm:~/pwn/format$ ./b |
输出的结果是AAAA%08x%08x%08x%08x%08x%08x
看一下执行printf函数时堆栈,观察0x41414141,这是str的开始
1 | pwndbg> x/10x $sp |
继续执行,看看输出结果是什么,成功读到了AAAA
1 | AAAA000000c8f7e2a620565561c7f7c184bef7fd029441414141 |
如果想获取指针指向的内存数据,那么我们可以尝试加上%s
利用%n格式符写入数据
%n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址,看下面的代码:
1 |
|
编译运行:
现在我们明白了如何利用%n向内存中写入值,所以我们可以修改某一个函数的返回地址从而劫持程序执行流程,但是返回地址需要的是一个地址,但是%n只能修改值,直接修改不太现实,所以还需要利用一个特性。
自定义打印字符串宽度
回顾前文讲过的,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
实验一下,修改上文代码:
1 |
|
num果然被修改了,那么这样如果我们想修改为一个地址,只要把16进制的地址转化为10进制数作为格式化符控制宽度就可以了。
举个例子:
修改num的值为0x8048000
1 |
|
三、格式化字符串漏洞利用
当printf函数没有参数时,比如printf(“%s %d %f”),程序会照样运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为:
- 解析其地址对应的字符串
- 解析其内容对应的整形值
- 解析其内容对应的浮点值
对于第一种情况,如果里面的地址为不可访问地址,比如0,那么程序就会崩溃,所以即使prrintf函数没有给出参数,也会按照格式化字符串给出的格式打印出接下来三处地址中内容。
1 | %d:十进制,输出十进制整数 |
程序崩溃
拿到程序之后可以通过输入若干个%S来进行判断是否存在格式化字符串漏洞
1 | %s%S%S%S%S%S%S%S%S%S%S%S%S%S |
如果存在格式化字符串漏洞,在输入一长串%s之后,printf会将%s作为格式化字符串,将对应地址中的内容以字符串的形式输出出来。但是栈上不可能每个值都对应了合法地址,所以数字对应的内容可能不存在,这个时候就会使程序崩溃。
在linux中,存取无效的指针会引起进程受到SIGSEGV信号,从而使程序非正常终止并产生核心转储。(程序运行过程中出现异常崩溃终止时的内存,寄存器状态,堆栈指针,内存管理信息等记录下来保存的文件)
泄露栈内存
实验
1 |
|
运行程序,在printf函数入口处下断点,查看此时栈中内存
继续运行查看程序输出是否于箭头猜想一致
输出结果于猜想一致,继续运行到第二处printf函数查看一下栈内存:
跟输出结果一致。用%p的效果跟%x差不多,就是多了0x
由于栈上的数据会因为每次分配的内存页不同,所以并不是每次得到的结果都一样。如果我们想输出一个特定位置的内容,就需要修改一下
1 | %n$x |
举个例子
输入%3$x时,第一处printf运行后的结果。
第二次printf后的结果,输出的是偏移为3的参数
获取栈变量对应字符串
把格式化字符串的%x改为%s,因为%s会以字符串的形式输出栈地址的内容。
利用%x或%p获取对应栈的内存。
利用%s获取变量所对应的地址的内容,但是有0截断
利用%n$x获取指定参数的值,利用%n$获取指定参数对应地址的内容。
泄露任意地址内存
有的时候需要泄露got表地址,从而获取到Libc版本以及其它函数地址。所以完全控制泄露某个指定地址的内存就很重要。
调用输出函数时,第一个参数的值就是该格式化字符串的地址。
由于我们可以控制格式化字符串,如果我们知道格式化字符串在输出函数调用是是第几个参数,那么我们就可以用%n$s泄露这个参数地址的内容。
1 | 1、先确定格式化字符串为第几个参数 |
可以看出AAAA是作为第四个参数被打印的,所以我们可以将AAAA替换为got表地址,再把%p换为%s,再指定获取真实地址
1 | from pwn import* |
覆盖内存
想修改栈上变量的值,需要%n。
用法:
1 | ...[overwrite addr]...%[overwrite offest]$n |
步骤:
- 确定覆盖地址
- 确定相对位移(找格式化字符串的第几个参数)
- 进行覆盖
实验:
源码
1 |
|
通过结果可以得到变量c在格式化字符串的第6个参数,根据步骤构造payload
1 | c_addr + %12d + %6$n |
第一个c_addr是由程序自己打印的c_addr,我们就是要修改这处地址的内容,%12d是为了和不全16个字节,因为%n就是覆盖已经打印的字符个数,所以要想更改为16,必须要先打印16个字符,最后的%6$n是为了向第6个参数内写入16.
EXP:
1 | from pwn import* |
成功覆盖了C变量的值
以上内容大致就是X86下的格式化字符串漏洞的基本原理及其利用,X64跟X86有略微的差别,但是思路还是一致的。