# 格式化字符串函数介绍
参考ctf wiki:
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串(也就是格式参数说明符,例如%s、%c),根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分:
- 格式化字符串函数
- 格式化字符串
- 后续参数,可选
例子:(这个例子中在第一个参数中设立了三个格式参数说明符,将后续的三个参数按相应格式进行转化)

# 格式化字符串函数
常见的格式化字符串函数:
- 输入:
- scanf
- 输出:
| 函数 | 基本介绍 |
|---|---|
| printf | 输出到 stdout |
| fprintf | 输出到指定 FILE 流 |
| vprintf | 根据参数列表格式化输出到 stdout |
| vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
| sprintf | 输出到字符串 |
| snprintf | 输出指定字节数到字符串 |
| vsprintf | 根据参数列表格式化输出到字符串 |
| vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
| setproctitle | 设置 argv |
| syslog | 输出日志 |
| err, verr, warn, vwarn 等 | 。。。 |
# 格式化字符串
格式化字符串的基本格式如下:
%[parameter][flags][field width][.precision][length]type
//例如:
printf("%2$#[field width][.precision][length]d",16,17);
//输出:17【这里利用n$,指定了使用后面的第几个参数】
各个格式含义如下:
- Parameter可以
忽略或者是:
| 字符 | 描述 |
|---|---|
n$ |
n_是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。 如果任意一个占位符使用了_parameter,则其他所有占位符必须也使用_parameter_。这是POSIX扩展,不属于ISO C。例:printf("%2$d %2$#x; %1$d %1$#x",16,17) 产生"17 0x11; 16 0x10" |
- Flags可为0个或多个:
| 字符 | 描述 |
|---|---|
+ |
总是表示有符号数值的'+'或'-'号,缺省情况是忽略正数的符号。仅适用于数值类型。 |
| 空格 | 使得有符号数的输出如果没有正负号或者输出0个字符,则前缀1个空格。如果空格与'+'同时出现,则空格说明符被忽略。 |
- |
左对齐。缺省情况是右对齐。 |
# |
对于'g'与'G',不删除尾部0以表示精度。对于'f', 'F', 'e', 'E', 'g', 'G', 总是输出小数点。对于'o', 'x', 'X', 在非0数值前分别输出前缀0, 0x, and 0X表示数制。 |
0 |
如果_width_选项前缀以0,则在左侧用0填充直至达到宽度要求。例如printf("%2d", 3)输出" 3",而printf("%02d", 3)输出"03"。如果0与-均出现,则0被忽略,即左对齐依然用空格填充。 |
- Field Width给出显示数值的最小宽度,典型用于制表输出时填充固定宽度的表目。实际输出字符的个数不足域宽,则根据左对齐或右对齐进行填充。实际输出字符的个数超过域宽并不引起数值截断,而是显示全部。宽度值的前导0被解释为0填充标志
- Precision通常指明输出的_最大_长度,依赖于特定的格式化类型。对于d、i、u、x、o的整型数值,是指最小数字位数,不足的位要在左侧补0,如果超过也不截断,缺省值为1。对于a,A,e,E,f,F的浮点数值,是指小数点右边显示的数字位数,必要时四舍五入或补0;缺省值为6。对于g,G的浮点数值,是指有效数字的最大位数;缺省值为6。对于s的字符串类型,是指输出的字节的上限,超出限制的其它字符将被截断。如果域宽为
*,则由对应的函数参数的值为当前域宽。如果仅给出了小数点,则域宽为0。 - Length指出浮点型参数或整型参数的长度。可以忽略
- Type,也称转换说明(conversion specification/specifier),可以是:
| 字符 | 描述 |
|---|---|
d, i |
有符号整数。有符号十进制数值int。'%d'与'%i'对于输出是同义;但对于scanf()输入二者不同,其中%i在输入值有前缀0x或0时,分别表示16进制或8进制的值。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。 |
u |
无符号整数。十进制unsigned int。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。 |
f, F |
double型输出10进制定点表示。'f'与'F'差异是表示无穷与NaN时,'f'输出'inf', 'infinity'与'nan';'F'输出'INF', 'INFINITY'与'NAN'。小数点后的数字位数等于精度,最后一位数字四舍五入。精度默认为6。如果精度为0且没有#标记,则不出现小数点。小数点左侧至少一位数字。 |
e, E |
double值,输出形式为10进制的([-]d.ddd e[+/-]ddd). E版本使用的指数符号为E(而不是e)。指数部分至少包含2位数字,如果值为0,则指数部分为00。Windows系统,指数部分至少为3位数字,例如1.5e002,也可用Microsoft版的运行时函数_set_output_format 修改。小数点前存在1位数字。小数点后的数字位数等于精度。精度默认为6。如果精度为0且没有#标记,则不出现小数点。 |
g, G |
double型数值,精度定义为全部有效数字位数。当指数部分在闭区间 [-4,5] 内,输出为定点形式;否则输出为指数浮点形式。'g'使用小写字母,'G'使用大写字母。小数点右侧的尾数0不被显示;显示小数点仅当输出的小数部分不为0。 |
x, X |
16进制unsigned int。'x'使用小写字母;'X'使用大写字母。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。 |
o |
8进制unsigned int。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。 |
s |
如果没有用l标志,输出null结尾字符串【即\0】直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了l标志,则对应函数参数指向wchar_t型的数组,输出时把每个宽字符转化为多字节字符,相当于调用wcrtomb函数。%s需要对应的参数是地址(&a这种),错误地址会触发段错误【pwn93】 |
c |
如果没有用l标志,把int参数转为unsigned char型输出;如果用了l标志,把wint_t参数转为包含两个元素的wchart_t数组,其中第一个元素包含要输出的字符,第二个元素为null宽字符。 |
p |
void *型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。 |
a, A |
double型的16进制表示,"[−]0xh.hhhh p±d"。其中指数部分为10进制表示的形式。例如:1025.010输出为0x1.004000p+10。'a'使用小写字母,'A'使用大写字母。[2][3](C++11流使用hexfloat输出16进制浮点数) |
n |
不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。 |
% |
'%'字面值,不接受任何flags, width, precision or length。 |
%n系列
构造payload 在比赛或者是 实际 漏洞利用时一次行传输这么大量的字节 会导致网络卡顿或者中断连接。
我们再来了解下”%n“格式化字符的扩展(称不上 其实,就称为一个系列吧)
%n 一次性写入4个字节
%hn 一次性写入2个字节
%hhn 一次性写入1个字节
# 格式化字符串漏洞
约定:在printf("%d %d %d", a, b, c)中,a、b、c被称为格式化参数,其中a是第1个格式化参数,b是第2个格式化参数,c是第三个格式化参数;而字符串"%d %d %d"则是printf的第1个参数【这里就是用于解析的格式化符号实际上是第一个参数(如%d %d %d),a是第二个参数但是第一个格式化参数】
# 32位程序的格式化字符串漏洞
32位程序是利用栈来传参,实际上参数是从右向左依次入栈,栈顶不断升高(esp),对于printf函数第一个参数【用于解析的格式化符号】是最后入栈的位于栈顶,因此会将esp+4【即第二个参数】之后的数据作为匹配格式化符号的对象esp处即使没有实际要打印的数据都用esp+4的数据,
所以想要进行一些任意读写时所用的偏移,一般都是从esp+4的地方开始算【其本身也算】
算偏移:
aaaa-%p-%p-%p-%p-%p-%p-%p-%p
# 任意地址读
%n$s:n$是指定显示第几个参数,使参数输出多次,s指定以字符形式输出
【%偏移$输出格式】
【如:%7$p,是直接输出 偏移为 7 处的内容(“0x + 16进制”)】
注:这里的偏移是以esp+4为第一个值开始算,也就是第一个格式化参数
# 任意地址写
任意地址写主要依赖于%offset$n,会将已经输出的字符个数写入到写入对应的整型指针参数所指的变量,也就是说将偏移处内的数据作为地址,写入到该地址对应的空间中去,相当于有个指针的功效
这里又要注意一个点,既然要任意地址写,那就要控制输出的长度即为要写的值,
需要利用`%<num>c`,利用字段宽度来控制总输出长度,num为多少就会补全为num,【面向的是其后面对应的参数进行补齐,比如说是esp+4的那个参数】,而不是再多打印num个字符【但实际效果是多打印num个字符,因为后面的参数不是payload里的,是栈上的被它给用】,将函数地址作为num时即可将想改写的改为想要跳转的地址
最终利用方式
目标地址%<num>c%offset$n 【注:%<num>c也会消耗一个参数,如果不加offset则会默认利用参数顺序使用第二个】
这里的目标地址可以是got表项,将其内容改写,目标地址在最前面是为了让offset的偏移值刚好对应到它在栈上的位置,然后就可以进行改写内容
例如:
payload=p32(printf_got)+('%{}c'.format(system_plt-4)).encode()+b'%7$n'
这里是要改写printf_got,其实际是在esp+4的第七个参数,利用%<num>c留出system_plt-4的地址个数的字符输出,后面的%7$n即为将esp+4的第七个参数的值作为地址,写入对应地址的空间中
利用fmtstr_payload(offset,{源地址:目的地址})
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
#常用形式:fmtstr_payload(offset,{address1:value1})
payload=fmtstr_payload(6,{printf_got:system})
p.sendline(payload)
p.sendline("/bin/sh\0")
#自动获取偏移【可以自己调试数一下】:
def get_vuln_offset(payload):
p.sendline(payload)
info = p.recv()
return info
vuln_offset = FmtStr(get_vuln_offset).offset
log,info("vuln_offset => %s" % hex(vuln_offset))
# 64位程序的格式化字符串漏洞
64位参数是先用寄存器传参的,参数顺序是从左往右,依次是rdi,rsi,rdx,rcx,r8、r9,当为第七个及以上的参数时就会利用栈传参【栈传参是从右向左】