格式化字符串漏洞(Format String Vulnerability)是一种常见的软件安全漏洞,它通常出现在处理用户输入的字符串格式化操作中。这种漏洞允许攻击者控制程序的输出格式,进而可能泄露内存信息、执行任意代码或导致程序崩溃。
前置知识
printf中的格式控制符
格式控制符告诉函数如何解析和处理传递给它们的参数。如果printf中的格式控制符没有对应的参数,那么将会泄漏内存中的数据。一个%可以解析一个参数,也就是对应一个参数。
%c:以字符形式输出%d:以十进制整数形式输出%x:以十六进制形式输出,%7$x表示输出参数列表中的第七个参数%p:以十六进制形式输出,并加上前缀0x%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置。%n:写入的地址空间为4个字节%hn:写入的地址空间为2个字节%hhn:写入的地址空间为1个字节%lln:写入的地址空间为8个字节
pwntools中的fmtstr_paylod
fmtstr_payload是pwntools中的一个函数,其函数原型如下:
from pwn import * |
非栈上的格式化字符串漏洞
我们通常所说的在栈上的格式化字符串漏洞的意思就是我们可以直接把数据写到栈上,然后对栈上的数据进行一系列的攻击。如A是栈上的一个地址,B是我们要写到A里的printf的got表值,那么现在就是A -> B -> C,C是printf函数的plt表值。然后我们可以修改B这个地址中的内容即C,改成后门函数的地址。而非栈上的格式化字符串漏洞就是我们不能直接把B写到栈上,但是依然可以泄漏和修改栈中的地址等数据。如果我们不能直接把B写到栈上,我们可以先在栈上找到D -> E -> F把F修改成B就变成了D -> E -> B,但是我们最后的目的是修改B指向的地址,所以说这里的E必须是与A和D一样的栈地址,那么E -> F就变成了E -> B -> C,又回到了栈上。
例题
jarvisoj_fm
先看主函数,直接给我们system("/bin/sh"),当x == 4的时候才能执行这个函数。我们看到第10行有个printf(buf)再结合第9行,不难知道这是一个格式化字符串漏洞。那我们就可以利用这个漏洞把x写成4,就可以拿到shell。

先计算偏移量,偏移量为11,然后构造payload。我们要写入的内容是4,巧的是我们要写入的地址也是4个字节,所以我们直接发送地址就可以写了,payload = p32(value_addr)+b'%11$n'。
exp
from pwn import * |
axb_2019_fmt32

我们看到read函数,可以读入0x100(256)个字节,而s的长度为257,因此没有办法溢出。继续往下看,printf(format)这一行明显存在格式化字符串漏洞,程序中也没有system函数和/bin/sh字符串。那大概思路就是利用格式化字符串漏洞泄漏printf的地址(本来我想修改read的got表值的,但是后来才相等修改后就相当于程序中没有read函数了,这样肯定是不行的),算出libc基地址以及system函数地址。
怎么泄漏printf的地址呢?本题中我们通过read函数将输入的内容写到s中,而s又是printf函数的参数,因此我们可以利用格式化字符串漏洞去泄漏栈上的内容,因此可以先把printf在got表中值写到栈上,再去泄漏程序运行起来后printf在got表中的地址即printf的真实地址。这就需要计算我们写入的内容在栈上的位置的偏移量,输入aaaa %p %p %p %p %p %p %p %p %p %p %p得到输出的内容数一下偏移量为8。

不过我们也发现了0x20616161中只有3个a,是因为format中还有Repeater:这些字符的原因,所以我们尝试着再输入Aaaaa %p %p %p %p %p %p %p %p,发现0x61616161刚好就是aaaa。

得到偏移量后可以开始构造第一个payload泄漏printf地址了,如下:
p.recvuntil(b'me:') |
得到printf真实地址后,可以计算libc基地址以及system函数的地址,并把printf的got表值改成system函数的地址,这样再次传入参数 '/bin/sh' 再执行printf时,由于将got表给修改了,就相当于执行了system 函数 即:执行system('/bin/sh')。这也解释了为什么刚开始我改puts函数的got表不行,因为就算把puts的got表的值改成system函数的got表值,我们后面也不会再执行puts函数,也就执行不了system函数。
法一
题目如果是32位的程序,可以用pwntools中的fmtstr_payload函数直接修改地址,如下:
base_addr = printf_addr-libc.sym['printf'] |
exp
from pwn import * |
拿到flag

法二
一个字节一个字节的修改,这个方法对32位和64位的都适用。
先把两个地址打印出来,发现四个字节中只有最高字节f7是一样的,我们们需要修改后3个字节,也就是需要这样修改e3 --> e2,10 --> 29,20 --> 40(当然地址是不确定的,肯定不能直接改数字)。
print('printf_addr =>',hex(printf_addr)) |

虽然每次地址都会变化,但是我们可以把每次的地址表示出来。sys_addr&0xff将sys_addr与0xff进行按位与运算,得到的结果把sys_addr的低8位(8位1个字节)保留下来了,高位被全部置零。sys_addr&0xff00将sys_addr二进制形式的第9到16位保存下来了,其他位全部置零举个例子就是11111111 11111111 11111111变成了00000000 11111111 00000000。>>8是再将结果右移8位即00000000 11111111 00000000变成了00000000 11111111。(sys_addr&0xff00)>>8就是把sys_addr的倒数第二个字节保留下来了。
sys_addr1 = sys_addr&0xff |
然后下面这一步中sys_addr1-(9+13)减的是printf中前面参数字节的总和,程序中的Repeater:是9个字节,payload2 = b'a'+p32(strlen_got)+p32(strlen_got+1)+p32(strlen_dot+2)一共13个字节。(其实为什么要这样我也不知道)
sys_addr1_value = sys_addr1-(9+13) # 前面已经输出了0xa+0x10个字符,要减去 |
最后构造payload2,bytes(str(sys_addr1_value),encoding='utf-8')的意思是把sys_addr1-value转换为字节串bytes类型。%hhn是修改低1个字节,printf_got+1会把printf_got的倒数第二个字节变成倒数第一个,然后就可以修改低2个字节了。
payload2 = b'a'+p32(printf_got)+p32(printf_got+1)+p32(printf_got+2) |
exp
from pwn import * |
拿到flag

axb_2019_fmt64
源码

思路
第一步,计算偏移量为8

到了与32位程序不一样的地方了,泄漏地址这里如果是按照32的写成payload = p64(puts)+b'%8$saaaa',然后我们看到发送的数据在puts_got与%8$s之间有很多'00',字符串中的'00'就代表结束,所以在printf到'00'的时候就以为字符串后面没有内容了,后面的内容也就不会被printf了。

因此我们现在要换个写法构造payload,payload = b'%9$saaaa'+p64(puts_got)这样我们可以看到'00'就在后面了,有用的字符串也就不会再被截断了。9是因为现在p64(puts_got)变成了第2个参数了。

刚开始我泄漏的是printf函数的got表值,但是不知道为什么一直不成功,后来看别的师傅的wp泄漏的都是puts函数就可以,后来我用read,strlen这些函数都可以。然后我发现puts函数发送的是0x601018(重定位前),接收的是0x7fd7f1458690(重定位后)

而printf函数发送的与接收的一样都是0x601030,也就是说并没有泄漏出printf重定位后的got表值,所以就不能泄漏printf的got表值

payload1 = b'%9$saaaa'+p64(puts_got) |
打印出来他们的地址,发现只有后3个字节不一样,我们只需要修改后3个字节即可

同32位,分别表示出这3个字节
sys_addr1 = system_addr&0xff |
构造第二个payload的时候同样不能和32位的题一样了,payload2 = b'%'+bytes(str(sysaddr1_value),encoding='utf-8')+b'c'+b'%13$hhn' 的意思是将sys_addr1_value的值作为字符写入到第13个参数所指向的内存地址中,%hhn是只修改1个字符的意思。
payload2 = b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%13$hhn' |
exp
from pwn import * |
拿到flag

hitcontraining_playfmt(ebp链)
保护&源码
查看保护,32位程序,有RWX段我们第一时间想到用shellcode。

源码:
int do_fmt() |
思路
分析代码,printf(buf)格式化字符串漏洞,buf在.bss段。大概思路就是利用格式化字符串漏洞修改函数返回地址为buf的起始地址+4(因为执行ret指令就需要跳出循环,就需要buf的前4个字节是quit),最后发送shellcode。
下图中ebp下面的0xffffd07c这个地址中放的是函数返回地址,我们可以把0xffffd098改成0xffffd07c,这样0xffffd088 -> 0xffffd098就变成了0xffffd088 -> 0xffffd07c -> 0x80485ad(play+77),我们也就可以修改0xffffd07c中的内容即返回地址为shellcode的首地址了。

exp
from pwn import * |
拿到flag
