格式化字符串漏洞(format string vulnerability),也是一种比较常见的漏洞类型。常出现于c语言格式化字符串一系列函数。比如printf,sprintf,fprintf等一系列家族函数。由于此函数对参数类型和个数过滤不严格,导致用户可以构造
任意数据,实现读取写入内存数据,从而实现代码执行。
至于为什么会产生字符串溢出漏洞呢,我们来看看产生溢出的关键函数。比如说
_Check_return_opt_ _CRTIMP int __cdecl printf(_In_z_ _Printf_format_string_ const char *_Format, ...);
这个输出控制字符串函数。解释一下参数:
Format是个格式化控制字符串,里面控制这个字符串输出的格式。
常见格式控制如下列表
...省略号代表是用户可以控制的参数类型和列表。
关键就在于这个位置,参数类型和数量的不确定性,如果允许用户自己定义这两个参数。
那么就可以经过精心够早实现对程序任意位置的数据读取和写入,那么就能构造出漏洞
利用程序,从而控制我们的程序流程。
涉及的关键控制符
%n功能
是将%n之前printf已经打印的字符个数赋值给传入的指针。通过%n我们就可以修改内存中
的值了。例如: printf("aaaaaaa%n\n",&a);
printf("%d\n",a);
可以发现a的值被printf函数修改为了7。这就是%n的功效了。这是一个不常用到的参数.
它的功能是将%n之前printf已经打印的字符个数赋值给传入的指针。通过%n我们就可以
修改内存中的值了。和%sleak内存一样,只要栈中有我们需要修改的内存的地址就可以
使用格式化字符串的漏洞修改它。当然,如果需要修改的数据是相当大的数值时,我们
可以使用%02001d或者%2001x这种形式。
%p功能
格式控制符“%p”中的p是pointer(指针)的缩写。指针的值是语言实现(编译程序)
相关的,但几乎所有实现中,指针的值都是一个表示地址空间中某个存储器单元的整数。
printf函数族中对于%p一般以十六进制整数方式读取输出指针的值,附加前缀0x。
%$功能
格式化字符串的“$”操作符,其允许我们从格式化字符串中选取一个作为特定的参数。
例如:printf("%3$s", 1, "b", "c", 4);
最终会显示结果“c”。这是因为格式化字符串“%3$s”,它告诉计算机“把格式化字
符串后面第三个参数读取告诉我,然后将参数解释为字符串”。所以,也可以这样做
printf("AAAA%3$n");
printf函数将值“4”(输入的A的数量)写入第三个参数指向的地址。
由此可见格式化字符串漏洞主要是:
1.读取任意地址的数值,泄露内存
2.给任意地址写入数据,修改内存
主要实现方式是利用格式化串本身也处于栈中,去用直接参数访问找到这个栈中的格式化串。
这个格式化串可以使用一个要写入的内存地址。也就是直接参数访问+%n格式符+长度表示=
向任意地址写入值,这个写入是不能一次写入4字节的,所以可以分两次写入2字节和分四次
写入1字节。其中hhn是写一个字节,hn是写两个字节。n是写四个字节。
windows系统示例
int _tmain(int argc, _TCHAR* argv[])
{
char str_test1[100];
char str_test2[100];
scanf("%s",str_test1);
printf("%s\n",str_test1);
scanf("%s",str_test2);
printf(str_test2);
return 0;
}
编译完成之后,命令行下面测试。发现同样的输入参数,却输出不同的结果。
而且输出了一部分我们不知道的参数和内存地址。
注意不同地方:
1 主要使用了%P参数控制,泄露了内存地址。
2 printf("%s\n",str_test1);//代码采用格式化进行了过滤,所以并没有输出内存数据。
printf(str_test2); //而这行代码却泄露了我们的内存数据。
Linux系统示例
此处的代码里面有三个整数变量,对应的三个值参数分别为10,20,30第四个没有参数。
但是格式化控制符却有参数控制,因此这个函数将会输出栈地址的30后面的数据也就
是栈的下方数据。
#include
int key=30;
int main(int argc, char** argv)
{
int value=10;
int num=20;
printf("%d,%d,%d,%p\n",value,num,key);
return 0;
}
注意看此函数的参数,只有三个参数,格式化控制符却又四个控制,那么第四个%P输出的
是什么呢。
我们来跟进去看看printf函数调用之前的栈布局和数据
同样的原理,如果printf函数的所有参数可以被我们控制,自定义。
这样的话,我们就可以自己控制堆栈数据了。
比如代码这样编写。
#include
int main(int argc, char** argv)
{
printf(argv[1]);
return 0;
}
岂不很危险了。
下面我们来做个例题吧,来熟悉练习一下这个漏洞原理吧。
先正向给大家看一下这道题目的源代码:
/*
* format.c
* Created on: Apr 12, 2017
* Author: 5t4rk
*/
#include
#include
int nsecret = 0x110;
void give_flag()
{
system("cat /down/key.txt");
}
int main(int argc, char** argv)
{
int * pKey = &nsecret;
printf(argv[1]);
if (nsecret == 2017)
{
give_flag();
}
return 1;
}
阅读分析此源代码,发现有一个函数give_flag,如果实现调用此函数那么就可以直接获
取到机器的shell执行代码权限。但是此处的逻辑必须要满足nsecret==2017这个条件。
才能进入我们的flag获取函数里面,所以此处的关键是如何让nsecret=2017。然后并
没有我们用户直接赋值个nsecret变量的输入和接口,因此直接修改不可能。但是发现
有printf函数的直接调用,argv[1]参数。
大家还记前面讲的漏洞函数,看我们能否利用此函数参数进行修改nsecret变量的值为
2017,基于栈的构造原理,如果成功,那么就会直接进入这个函数。先编译此代码:
得到format这个可执行程序。
然后测试。
测试发现nsecret的变量地址是0x804a024
在对栈上的变量是第七个,于是我们知道只要修改第七个地址数据0x110为
2017就能实现我们的结果。
于是我们想到了前面学习的%n可以实现修改内存地址数据。
结合我们找到的位置,顺带采用%$实现准确覆盖,修改数据。
“%02017u%7$n”
在打印数值右侧用0补齐不足位数的方式来补齐足。
./format "$(python -c 'import sys;sys.stdout.write("%02017x%7$hn")')"