格式化字符printf汇总

▲printf的性质:

▲参数%s:这个参数其本质上是读取对应的参数,并作为指针解析,获取到对应地址的字符串输出。可以通过这个性质来泄露某个内存地址的内容:

例子:

如果输入一个%S,在调用printf之前,栈上会是这种数据,有两个相同的指向%S,一个是会解析成参数%S,一个会使用解析的参数%S作用来打印输入的%S。这里可以计算出偏移量为6,即从栈顶数第6个可以到达我们输入的内容0x0a7325。

img

1.泄露任意地址内容

1
2
3
#注释头

\x01\x80\x04\x08%x.%x.%x.%x.%s

这一段即打印从栈顶往下数5个内容的栈中保存的值,由于第5个参数是%s,所以会将第5个内容当作一个地址来解析,从而打印位于该地址上的值。所以输出应该为0xff95abb4,0x0000012B,0x08048465…..(这都是栈上的内存),最后第五个内容是0x08040801,用参数%s来解析,即打印该地址上保存的内容,地址对应的值为”ELF\x01\x01\x01\n”,这样就可以打印任意内存地址的值。常用来传入got表地址,然后借助该函数泄露某函数got表中的值,即是泄露该函数的真实地址。

▲简化使用:

如果输入的参数保存在栈上很远的位置,那么则需要叠加%x,但是实际中可以使用简单的偏移来打印:\x01\x80\x04\x08%5$s,其中5就代表从esp开始计算偏移量为6。

总的来说就是一个公式,输出偏移量为n的参数的值就是:

%(n-1)$s

(%[addr_offset_value]$[格式化控制字符])

2.任意地址可写:

特殊格式化字符%n家族:

(1)这个字符会将已经输出的字符数写入到指定内存中,例如\x8c\x97\x04\x08%[addr_offset_value]$n,这段格式化字符串会使得地址0x0804978c处的内容变成4,因为这代表了输出了\x8c\x97\x04\x08这四个字符,所以会向0x0804978c这个地址写入4,实际是写入00 00 00 04,共计四个字节。利用这个特性可以修改某个地址的值。

(2)\x8c\x97\x04\x08%2048c%(addr_offset_value)$n:这个代码会将2052,实际是(00 00 08 04) ,共计四个字节写入到地址0x0804978c,因为调用了printf会先打印\x8c\x97\x04\x08四个字符,然后依据格式化字符%2048c,再打印2048个空字符,实际打印了2048+4=2052个字符,对应16进制为0x804,所以在使用是需要减去在%这个格式化字符标志前的字符的个数才是对应的值。

(3)但是在实际中,如果我们想将一个地址的值写入到某个地址,使得可以调用,那么我们需要输入的就会变成0x8xxx….转换成十进制则会特别大,会向服务器发送0x8000…个字符,这在实际中很容易出错。这时候需要用到另一个格式化字符控制:%hhn。

(4)特殊格式化字符%hhn:这个字符可以使得一次往指定地址写入一个字节。所以我们可以将我们需要输入的地址给拆分成4个字节来输入。

(5)例如

\x78\x97\x04\x08\x79\x97\x04\x08\x7a\x97\x04\x08\x7b\x97\x04\x08%16c%5$hhn%99c%6$hhn%129c%7$hhn%4c%8$hhn

这个格式化字符串相当于四个操作:

①\x78\x97\x04\x08%16c%5$hhn:改变地址0x08049778的值为0x20。

这里是20是因为前面已经输出了\x78\x97\x04\x08\x79\x97\x04\x08\x7a\x97\x04\x08\x7b\x97\x04\x08总共16个字符,所以实际写入到地址0x08049778的值为16+16=32=0x20。

②\x79\x97\x04\x08%99c%6$hhn:改变地址0x08049779的值为99+32=131=0x83,同理,因为前面已经输出了32个字符,所以最终写入值为0x83。

③\x7a\x97\x04\x08%129c%7$hhn:改变地址0x0804977a的值为129+131=260=0x104,这里由于是使用%hhn,所以只能写入一个字符,也就是会被截断为0x04

④\x7b\x97\x04\x08%4c%8$hhn:改变地址0x0804977b的值为4+0x104=0x108=0x08。所以最终写入到地址0x08049778中的四个字节为0x08048320,完成写入地址。

总的公式:

[addr]%[padding_count]c%[addr_offset_value]$[格式化控制字符]

▲注意偏移量一直在递增。还有就是因为这是大端存储,所以写入的顺序需要计算一下,防止出现\x00导致字符串输入截断。

▲特殊格式化字符串调用类:Fmtstr

以上的可以直接改成:fmtstr_payload(5, {printf_got_addr:system_plt})将栈上偏移量为6的值改成printf_got_addr,同样将printf_got_addr的值修改为system_plt_addr。也就是在第一次用pringtf时,会修改栈和劫持printf_got_addr为system_plt_addr。下一次再调用printf时,从printf_got_addr指向的就会是system_plt_addr,完成劫持。

但是如果一旦printf_got_addr出现\x00或者不同输入函数所对应的非法字符,就会发生截断,导致出错。因为Fmtstr模块创造的payload中地址会被切割成几段,相当于我们的多段执行,那么最后的几个地址如果其中一个带了非法字符就会出错。

▲printf函数的格式化字符串常见有

(1)%d,%f,%c,%s,(用于读取内存数据)

(2)%x(输出16进制数,前面没有0x),%p(输出16进制数,前面带有0x)

(3)除了%n,%hn,%hhn,%lln,分别对应写入目标空间4字节,2字节,1字节,8字节。

▲由于使用%n之类的控制字符,输入的str(addr)为以字符串形式输入,也就是说会转换成十进制然后转换成对应的ascii码来存储在栈上,所以不会出现0x00这类的字符。但是如果是需要写入的地址,使用的是p64或者p32,这个就是直接对应的,如果这个地址中有00,那么就不会被read函数读取进入。因为read函数默认结尾是0x00,也就是字符串结束符号。所以这种情况下一般都是将p64(addr)放在payload的末尾,从而自动生成00,不会导致被截断。

★技巧总结:

1.劫持某函数为system函数时,是将Got表劫持为plt表,并且该函数的总体结构应该与system函数类似,参数应该是一个地址。

2.fmtstr模块的用法需要注意,有时候也不太好用。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157