格式化字符串
格式化字符串
利用原理
基本格式
1 | |
parameter
n$,获取格式化字符串中的指定参数
1
2
3
4
5例如
int a=0x1111,b=0x2222,c=0x3333;
printf("%3$p",a,b,c)
//按理来说输出的是0x3333
//但我自己执行的时候输出的是$p,不知道什么原因
flag
field width
- 输出的最小宽度
precision
- 输出的最大长度
length,输出的长度
- hh,输出一个字节
- h,输出一个双字节
type
- d/i,有符号整数
- u,无符号整数
- x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
- o,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
- s,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
- c,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
- p, void * 型,输出对应变量的值。printf(“%p”,a) 用地址的格式打印变量 a 的值,printf(“%p”, &a) 打印变量 a 所在的地址。
- n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
- %, ‘
%‘字面值,不接受任何 flags, width。

参数位置
32位
参数全在栈上
64位
前6个参数由寄存器存放,rdi存储格式化字符串的地址,也就是说从%6$p在栈上
rdi rsi rdx r10 r8 r9
gdb
1 | |
能直接看到地址相对于printf函数和格式化字符串地址的偏移
常见格式化字符串函数

格式化字符串的利用
泄露内存
泄露栈内存
获取栈变量数值
理论依据:格式化字符串函数会根据格式化字符串直接使用栈上自顶向上的变量作为其参数 (64 位会根据其传参的规则进行获取)
这里以32位举例说明

运行查看结果

gdb调试,断点下在两个printf处,查看栈上信息
第一个printf处

可以看到 栈中第一个变量(esp)为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们**输入的格式化字符串对应的地址 **
继续运行

程序 会将栈上的 0xffffcd04 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出 。
但上述输出需要输入很多个%x%x,如果需要获取某个参数,可以直接 %n$x ,这个n就是栈上的参数相对于格式化字符串的偏移,例如上面图片的0xffffcd0c就是%3$x,当然也可以%p来获取地址
需要快速获取位移的话就可以来利用利用原理篇的gdb
获取栈变量对于字符串
就是%s,将对应处的变量解析为字符串地址进行输出
如果不能被解析为字符串变量,则程序会崩溃
所以快速让程序崩溃的一个办法就是输入一大串%s%s….
小技巧总结
- 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
- 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
- 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。
泄露任意地址内存
=完全控制泄露某个指定地址的内存
方法:获取某个addr的内容:addr%k$s这里的k是该格式化字符相对函数调用的第k个参数
那么想办法确认k就好了
方法:
输入[tag]%p%p%p%p%p查看哪个%p跟tag一样(比如aaaa%p%p%p%p%p)

0x41414141跟我输入的AAAA一样,是格式化字符串的第4个参数所以k=4即可输出该地址上的内容
应用:
获取真实地址 例如获取scanf
获取scanf@got

gdb也是输入got\n就可以了然后就scanf@got%4$s就可以输出got真实地址了
1
2
3
4
5
6
7
8
9
10
11
12from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got 意思是会先将scanf@got打印出来然后才是scanf真实地址所以要接收后4字节
sh.interactive()
注意:
并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们想要打印的地址内容的地址位于机器字长整数倍的地址处,一般来说,类似于下面的这个样子。
1 | |
覆盖内存
修改栈上变量的值,甚至修改任意地址变量的内存
方法:%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
以如下代码为例:(32)
1 | |
基本payload格式:
1 | |
其中… 表示我们的填充内容,overwrite addr 表示我们所要覆盖的地址,overwrite offset 地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数
一般步骤:
- 确定覆盖地址
- 确定相对偏移
- 进行覆盖
覆盖栈内存
目的:进入第一个if语句
地址:
上面程序有打印c的地址,直接用即可
偏移:
gdb调试

发现是第6个参数,所以是%6$n
覆盖
payload如下
1
[addr of c]%012d%6$n因为addr of c栈4字节,所以再补充12个字节即可将c覆盖成16
脚本如下
1 | |
覆盖任意地址内存
覆盖小数字
问题:当我需要覆盖的数字小于4/8时,会导致覆盖失败,因为addr of a就占了4字节
解决:没有必要一定将addr放在最前面,只要知道偏移即可
当我们将a覆盖为2时,格式必须是
1 | |
围绕这个展开
确定地址

确定偏移
aa%k$n已经占了6字节,在覆盖栈内存时可知偏移为6,这里我们构造成aa%k$aa这样就占了8字节,刚好占2,所以将上面的偏移往后推2即可,可得出k为8进行覆盖
1
2
3
4
5
6sh = process('./overwrite')
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()
覆盖大数字
变量在内存中的存储格式:
首先,所有的变量在内存中都是以字节进行存储的。此外,在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。
types:
1 | |
所以可利用%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节
确定覆盖的地址

如何覆盖

确定偏移
由覆盖栈内存可得出偏移是6payload格式
1
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$hhn'+pad2+'%7$hhn'+pad3+'%8$hhn'+pad4+'%9$hhn'
例子
hijack GOT
在目前的 C 程序中,libc 中的函数都是通过GOT 表来跳转的。此外,在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的 因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。
加入将函数A的地址覆盖为函数B的地址,有如下步骤:
确定函数 A 的 GOT 表地址。
- 这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。
确定函数 B 的内存地址
- 这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。
将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。
这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种
写入函数:write 函数。
ROP
1
2
3pop eax; ret; # printf@got -> eax
pop ebx; ret; # (addr_offset = system_addr - printf_addr) -> ebx
add [eax] ebx; ret; # [printf@got] = [printf@got] + addr_offset格式化字符串任意地址写
payload = fmtstr_payload(offset,{printf_got:system_plt}) 用来将地址替换掉,这里是将printf_got替换成为system_plt,offset是格式化字符串的偏移
这个是专门为32位程序格式化字符串漏洞输出payload的一个函数
hijack retadder
利用格式化字符串来控制程序的返回地址(可能是用在开了RELRO,无法修改got的情况?)
思路:
存储返回地址的内存本身是动态变化的,但是其相对于 rbp 的地址并不会改变,所以我们可以使用相对地址来计算
- 确定偏移
- 获取函数的 rbp 与返回地址
- 根据相对偏移获取存储返回地址的地址
- 将执行 system 函数调用的地址写入到存储返回地址的地址。