1. 格式化字符串漏洞基本原理
格式化字符串漏洞在通用漏洞类型库CWE中的编号是134,其解释为“软件使用了格式化字符串作为参数,且该格式化字符串来自外部输入”。会触发该漏洞的函数很有限,主要就是printf、sprintf、fprintf等print家族函数。
介绍格式化字符串原理的文章有很多,我这里就以printf函数为例,简单回顾其中的要点。
printf("format_string", arg1, arg2, ...)
其第一个参数为格式化字符串,用来告诉程序以什么格式进行输出!然后是参数1(偏移1),参数2(偏移2),...
下面是详细的说明图:
2. 格式化漏洞测试
对于下面的程序
|
|
gdb调试该程序,然后展示栈内容,得到如下图:
打印结果:
343332311234
在格式化字符串 一文中,我们讲道n\$表示第n个参数
|
|
A、6\$ 表示栈空间从format string后第一个参数算起,偏移量为6的那个栈位置。
- 在上图中偏移0(格式化字符串)对应的是地址0xffffcef0,而其存放格式化串的栈地址是0xffffcf04;
- 偏移为6的地址为0xffffcf08,值为0x34333231("1234")
B、如果将上面的format换成%6\$s1234,那么就相当于打印地址0x34333231处的字符串。而如果此处为funA@got地址,那么输出的字符串就会是funA的funA@plt
C、若输入“%2214c%6\$hn”,其中,2214是0x8a6的10进制
- %2214c输出2214(0x2214)个字符
- %6\$hn表示将前面输出的字符数(即2214)写入到偏移8处值所指向的内容的低两字节处,即修改$[0x34333231]_{low-2bytes}=0x08a6$ 。假设[0x34333231]=0x88888888,此时就会变为0x888808a6
- 但是若用n来将[0x34333231]修改为0x888808a6,则%2214c需改为%2290616486c。表示输出2290616486个字符,开销太大,特别是在远程攻击时,所以很可能会导致程序崩溃或等候时间过长。
3.基本的格式化字符串参数
- %c:输出字符,配上%n可用于向指定地址写数据。
- %d:输出十进制整数,配上%n可用于向指定地址写数据。
- %x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
- %p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
- %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
- %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10\$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%\$hn表示写入的地址空间为2字节,%\$hhn表示写入的地址空间为1字节,%\$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%\$hn或%\$hhn来适时调整。
- %n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据
格式化字符串漏洞发生在栈中,利用方式以下几种:
- 修改变量的值,绕过认证
- 覆写GOT表
- 修改栈中保存的返回地址
buf
存储在堆中,也就是说我们输入的内容不会出现在栈里,那就不能通过向栈中写入地址的方式进行攻击,不过可以利用栈中已有的值进行攻击。
小技巧总结
- 利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。
- 利用%s来获取变量所对应地址的内容,只不过有零截断。
- 利用%order\$x来获取指定参数的值,利用%order\$s来获取指定参数对应地址的内容。
4. 实验测试
4.1 CCTF-pwn3
本题只有NX一个安全措施,且为32位程序。
通过IDA逆向及初步调试,可以知道以下两点。
用户名是“sysbdmin”字符串中每个字母的ascii码减1,即rxraclhm;
在打印(即get功能,sub_080487F6函数)put(本程序功能,不是libc的puts)上去的文件内容时存在格式化字符串漏洞,且格式化字符串保存在栈中,偏移为7。
主要利用思路就是先通过格式化字符串漏洞泄露出libc版本,从而得到system函数调用地址;然后将该地址写到puts函数GOT表项中,由于程序dir功能会调用puts,且调用参数是用户可控的,故当我们以“/bin/sh”作为参数调用puts(也就是dir功能)时,其实就是以“/bin/sh”为参数调用system,也就实现了getshell。
|
|
代码写的有点冗余,但主要的思路还是比较清晰:
leak system_addr => write system_addr to puts@got => concat the /bin/sh => system('/bin/sh')
题目算是没什么坑的fmt的题,个人觉得这些题还是有以下解题技巧:
1.函数封装的好,能够节省很多时间;
2.使用%10$x这样的形式确定参数的偏移,使用%10\$s这样的形式泄露特定地址数据,使用%c %n的组合拳来改写数据;
3.尽量使用%hn和%hhn,避免过多的返回;
4.最好在每次使用%n修改地址后,使用%s去确认一下修改是否成功,对于新手而言能节省大量的时间。