格式化字符串函数介绍
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数,通俗来说。格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。一般来说,格式化字符串在利用时分为三个部分。
- 格式化字符串函数
- 格式化字符串
- 后续参数(可选)
Example
printf("my name is %s, age is %d.", "dashuai", 18);
- my name is %s, age is %d.就是格式化字符串,根据其字符串的格式来解析后续的参数
- dashuai,18均为后续可选参数
常见的格式化字符串函数
- 输入函数:scanf
- 输出函数
函数 | 基本介绍 |
---|---|
printf | 输出到stdout |
fprintf | 输出到指定FILE流 |
vprintf | 根据参数列表格式化输出到stdout |
vfprintf | 根据参数列表格式化输出到FILE流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
格式化字符串基本格式
%[parameter][flags][field width][.precision][length]type
- parameter:n$,获取格式化字符串中的指定参数
- field width:输出的最小宽度
- precision:输出的最大长度
- length,输出的长度
- hh,输出一个字节
- h,输出一个双字节
- type
- d/i,有符号整数
- u,无符号整数
- x/X,16进制
- o,8进制
- s,所有字节
- c,char类型单个字符
- p,void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
- n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
- %
格式化字符串利用
- 使程序崩溃
- 查看进程内容,如可打印栈上的内容。
程序崩溃
- 通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式。
- 只需要输入若干个%s即可。
- 这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。
- 这一利用,虽然攻击者本身似乎并不能控制程序,但这样可以造成程序不可用,比如说,如果远程服务有一个格式化字符串漏洞,我们可以攻击使其崩溃,造成其他用户无法访问。
泄露内存
利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容
- 泄露栈内存
- 获取某个变量的值
- 获取某个变量对应地址的内存
- 泄露任意地址内存
- 利用GOT表得到libc函数地址,进而获取libc,进而获取其他libc函数地址
- 盲打,dump整个程序,获取有用信息
泄露栈内存
- 泄露栈内存得到的结果并不是每次都是一样的,因为栈上的数据会因为每次分配的内存页不同而有所不同,这是因为栈不对内存页做初始化。
- 可以直接获取栈中被视为第n+1个参数的值
%n$x
- 利用如上字符串,就可以获取到第n+1个参数的值
- 用%s获取栈变量对应的字符串
- 当然并不是所有的%s都会正常运行,如果对应的变量不能解析为字符串地址,那么程序就会直接崩溃
- 用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别
- 用%s来获取对应变量所对应地址的内容,只不过有00截断
- 利用%n$x来获取指定参数的值,%n$s来获取指定参数对应地址的内容。
泄露任意地址内存
我们可能想要泄露某一个lib函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址。
- 一般来说,在格式化字符串漏洞中,我们所读取的格式化字符串都在栈上的,那么也就是说,在调用输出函数的时候,其实,第一个参数的值就是该格式化字符串的地址。
- 我们还可以知道,scanf输入的字符串存储在栈的什么位置
- 32位程序每四字节为一个单位
- scanf输入多于四字节的依次向下存储
- 由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,我们就可以通过计算好的偏移,进行任意地址读和写
- 如下图所示
- 如输入为:某个地址(oxffffcffc)+"%6$s",%6$s是将栈上的第七个变量以字符串s解析出来,而在第七个变量上我们存储的是oxffffcffc这个地址,这个地址解析之后是libc_start_main的地址,也就是0x080048292,就会将0x080048292地址输出出来
- 可以通过这种方法读取got表里的libc函数地址
任意地址读的payload
[addr] + %n$s
注意
- 32位程序在函数调用时,如printf函数调用,将所有参数都压在栈上,我们可以直接查看栈的内容,找到偏移位置。
- 64位程序在函数调用时,优先将参数放在6个寄存器中,寄存器放不开的话,再将参数压在栈中。
Example
看一个64位程序的printf任意地址读
- checksec goodluck
- IDA查看伪代码
- 发现printf函数漏洞
发现打开了flag.txt文件,在本地建立一个模拟。
使用gdb调试,确定偏移
gdb goodluck
b printf
r
qqqqqqqqqqqqq
//随便输入一串字符串
我们输入的字符串存储在rdi寄存器中
flag的值位于栈中的第五个位置,但第一个地址(rsp处)为返回地址,故偏移为4
再加上存储在寄存器中的六个值,所以偏移为4+6=10。
故payload为
%9$s
覆盖内存
利用上述方法,不仅可以完成任意地址读,也可以完成任意地址写。
而如何用任意地址写呢?
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
Example
/* example/overflow/overflow.c */
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
编译程序
gcc -m32 -o strfmt strfmt.c
覆盖c的值
- 通过查看源代码或者查看IDA中的伪代码可知,c的值原来为789,需要将其更改为16,就可以进入if语句的第一个分支。
- 并且程序输出了c的地址(因为几乎所有的程序都开启了aslr保护,所以栈的地址一直在变,为了好分析,我们输出了c变量的值)。
- 还是老规矩,第一步先查看偏移,printf函数的第一个参数格式化字符串跟实际存储格式化字符串的地址相对偏移是多少。
- 第二步,确定padding,需要填充的字符串长度多少,这里当c=16时,if语句成立,又因为我们输入了c的地址,在32位程序中地址为4位,离16还差12,所以这里的padding为%012d,12为宽度,0代表不足这个宽度的前面补0,这是printf函数的用法。
- 之后就可以确定payload,进行攻击了。通用payload格式为
[addr of c][padding]%k$n
//addr of c为要修改的变量的地址
//padding为确定的填充长度
//k为偏移,就是实际字符串存储位置与printf第一个变量的便宜差
- 这题的payload为
[addr of c]%012d%6$n
覆盖a的值
a原值为123,这里需要修改为2
- 利用上面的payload,我们用ida找到a的地址0x0804A028
- payload确定为
p32(0x0804A028)+padding+%6$n
- 当我们确定padding填充字符时傻眼了,前面的地址就已经4个字节了,怎么把a的值修改为2呢?
- 这时我们换种思路
payload = "11" + "%8$n" + "11" + p32(0x0804A028)
- "11"为输出成功的2个字符,%8$n为我们确定的第8个偏移位置,11补位,后面为a的地址,这个payload是怎么确定的呢?
- 原本第6个偏移处是我们要写入的地址值,但地址均为4的倍数,没法写入比4小的值,这时我们在第6个偏移处先写入两个字符,然后写入%k$n,k的值稍后确定,但这个值指向了a的地址。
- %k是两个字节,于是第六个偏移处存储的四个字节为11%k,第七个偏移处存储的字节为$n,还差两个字节才达到4个字节,于是我们再加"11",第7个偏移处存储了$n11四个字节。
- 第8个偏移处我们可以写a的地址,于是k确定为8
覆盖b的值
需要将b的值覆盖为0x12345678,才满足要求
- 0x12345678是四个字节
- 变量在内存中的存储格式。首先,所有的变量在内存中都是以字节进行存储的。此外,在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。
- 再者,格式化字符串里面的标志有这么两个标志:
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
- 使用%hhn向某个地址写入单字节,使用%hn向某个地址写入单字节
payload确定
- 确定变量b的地址0x0804A028
- 按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12
- payload可以确定为:
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$hhn'+pad3+'%8$hhn'+pad4+'%9$hhn'
- pad1的确定:由于写入四个地址,四个地址为16个字节,0x78为120,所以还差104个字节,pad1为%104c
- pad2的确定:由于前面已经成功输出了104+16=120个字节,%hhn写入单字节,所以pad2为%222c.(120+222=342,342化为十六进制为0x156,但是写入的hhn只有一个字节,所以0x56写入)
- pad3的确定:由于前面写入了120+222=342字节,我们依旧使用hhn,则0x234=564, 564-342=222.即pad3为%222c(写入的0x234依旧为单字节,0x34)
- pad4依此类推