摘录自wiki-CTF{只为催促自己的记忆,复写一遍}
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
一般来说,格式化字符串在利用的时候主要分为三个部分
- 格式化字符串函数
- 格式化字符串
- 后续参数,可选
一、格式化字符串参数
C语言中格式字符串的一般形式为: %[指定参数][标志][输出最小宽度][.精度][类型长度]类型, 其中方括号[]中的项为可选项
1、指定参数
parameter:n$,获取格式化字符串中的第n个指定参数。
printf(“%3$x”,”abc”,”5”,”7”); #打印格式化参数中的第3个参数【即函数所有参数的第3+1=4个】 Output:7[16进制结果,省略了0x]
2、类型[具体看ctf-wiki]
我们用一定的字符用以表示输出数据的类型,其格式符和意义下表所示:
字符 | 意义 |
---|---|
c | 输出单个字符 |
d | 以十进制形式输出带符号整数(正数不输出符号) |
e | 以指数形式输出单、双精度实数 |
f | 以小数形式输出单、双精度实数 |
g | 以%f%e中较短的输出宽度输出单、双精度实数,%e格式在指数小于-4或者大 于等于精度时使用 |
G | 以%f%e中较短的输出宽度输出单、双精度实数,%e格式在指数小于-4或者大于等于精度时使用 |
o | 以八进制形式输出无符号整数(不输出前缀O) |
p | △ void * 型,输出对应变量的值。printf(“%p”,a)用地址的格式打印变量a的值,printf(“%p”, &a)打印变量a所在的地址。 |
s | 输出字符串,[没有l标志时,输出至null或按照进度输出] |
x | 以十六进制形式输出无符号整数(不输出前缀OX) |
u | 以十进制形式输出无符号整数 |
n | 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的地址处[默认写入机器字长大小] |
…… | …… |
举例:printf(‘%07d%3$n’,a,b,&c);
//%07d,打印整数a,不足7位长,就前面补0.那么可以保证前面输出了7个字符。%3$n,将第三个参数作为地址[即c变量地址],在该地址下写入已经成功输出的字符个数[即7],那么就是c=0x00000007
注意:printf(“%%”):表示打印字符%,不再接受任何标志flags, 最小宽度width。
3、标志
标志字符为-、+、#、空格和0五种,其意义下表所示:
字符 | 意 义 |
---|---|
(减号-) | 结果左对齐,右边填空格 |
(加号)+ | 输出符号(正号或负号) |
空格 | 输出值为正时冠以空格,为负时冠以负号 |
(井号#) | 对c,s,d,u类无影响;对o类,在输出时加前缀0;对x类,在输出时加前缀0x或者0X;对g,G 类防止尾随0被删除;对于所有的浮点形式,#保证了即使不跟任何数字,也打印一个小数点字符 |
0 | 对于所有的数字格式,用前导0填充字段宽度,若出现-标志或者指定了精度(对于整数),忽略 |
4、输出最小宽度
用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
5、精度
精度格式符以“.”开头,后跟十进制整数。本项的意义是:如果输出数字,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;若实际位数大于所定义的精度数,则截去超过的部分。
printf(“%.3d\n” , 5555);
6、长度
长度格式符为h,l两种,h表示按短整型量输出,l表示按长整型量输出。
h和整数转换说明符一起使用,表示一个short int 或者unsigned short int类型的数值, 示例:%hu,%hx,%6.4hd
l和整数转换说明符一起使用,表示一个long int 或者unsigned long int类型的数值, 示例:%ld,%8lu
L和浮点转换说明符一起使用,表示一个long double的值,示例:%Lf,%10.4Le
准确说,还有类似于:
hh:输出一个字节(8位)[超过这个一个字节的部分截断,高位截断]
h:输出一个双字节(16位)
参考链接:
- 详细各个选项的意义和可取值可参考:https://blog.csdn.net/hudashi/article/details/7080078
- 类型选项可参考: https://ctf-wiki.github.io/ctf-wiki/pwn/fmtstr/fmtstr_intro/
二、常见格式化字符串函数
常见的有格式化字符串函数有:
- 输入
- scanf
- 输出
函数 | 基本介绍 |
---|---|
printf | 输出到stdout |
fprintf | 输出到指定FILE流[文件流] |
vprintf | 根据参数列表格式化输出到 stdout |
vfprintf | 根据参数列表格式化输出到指定FILE流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
setproctitle | 设置argv |
syslog | 输出日志 |
err, verr, warn, vwarn等… | ….. |
三、格式化字符串函数的解析原理
格式化字符串函数是根据格式化字符串参数来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。比如说’%s’表明我们会输出一个字符串参数。
对于这样的例子在32位机器下,在进入printf函数的之前(即还没有调用printf),栈上的布局由高地址到低地址依次如下:[参数从右向左依次入栈,栈向低地址生长]
注:这里我们假设3.14向高地址方向的值为未知的值。
在进入printf之后,函数首先获取第一个参数[在这里就是格式化字符串],一个一个按照字符读取,会遇到两种情况
- 当前字符不是%,直接输出到相应标准输出。
- 当前字符是%, 继续读取下一个字符
- 如果没有字符,报错
- 如果下一个字符是%,输出%[也就是printf(“%%”)的情况]
- 否则根据相应的字符,获取相应的参数,对其进行解析并输出[没有设置n$的时候就向高地址逐个对应位置寻找参数;否则就按照n$的要求,找对应n位置的参数]
那么假设,此时我们在编写程序时候,写成了下面的样子 printf(“Color %s, Number %d, Float %4.2f”); 此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?
程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
- 解析其地址对应的字符串[%s]
- 解析其内容对应的整形值[%d]
- 解析其内容对应的浮点值[%f]
对于2,3来说倒还无妨,但是对于对于1来说,如果提供了一个不可访问地址,比如0,那么程序就会因此而崩溃[如果是服务器遭受这样的攻击,服务崩溃,那么外界就无法进行服务了]。
这基本就是格式化字符串漏洞的基本原理了。
注意!!!!
- %s是输出以栈上布置的参数为地址,那个地址下的字符串
- %d,%f等是输出栈上布置的参数
参考链接:https://ctf-wiki.github.io/ctf-wiki/pwn/fmtstr/fmtstr_intro/