格式化字符串小实验

来源:互联网 发布:最新免费域名更新 编辑:程序博客网 时间:2024/06/04 17:46
    在论文里看到格式化字符串攻击的说明,不是很理解,决定实践一下,结合Tim Newsham的Format String Attacks(http://forum.ouah.org/FormatString.PDF)进行实验。这里只测试了一下%x和%n参数,更多种类的格式化字符串攻击请查阅其他资料。至于测试的环境,应当是C编译环境都可以,但是在实验过程中,我发现dev c++默认不对%n进行预期操作,而是像遇到停止符一样的效果,查了一下发现由于printf函数在使用%n存在安全问题,所以VS2008在默认的情况下禁止这样使用(http://blog.csdn.net/delphiwcdj/article/details/6238708),可能dev c++也是如此,但目前没有找到打开的开关,转向gcc。
环境:Ubuntu 12.04 32位,内核3.8.0。为了演示简单,这里关闭了地址随机化。
格式化函数:用于将编程语言的原型变量转换为用户可读的字符串形式,如fprintf,printf,sprintf,snprintf,vfprintf,vprintf,vsprintf,vsnprintf,格式化字符串参数包括%%,%p,%d,%c,%u,%x,%s,%n等,其中%x从栈中读取数据,%s读取字符串,%n将当前打印出的字符数目写入参数指定的地址。(详见:https://www.owasp.org/index.php/Format_string_attack)
(1)先查看一下缺少部分参数的情况,不难得知,这里操作符与参数对应的情况是从左到右进行匹配,如果从右到左第一个打印的应该是123的%x格式。
#include <stdio.h>int main(){    printf("%d %x %x\n",123);    return 0;}
    %d打印出123的值,暂且不知后面两个%x打印了不知什么数据,实际上是读取了栈中123参数地址后面(高地址)的两个整数。
(2)无实参的情况
#include <stdio.h>int main(){    int a = 0;    int b = 1;    int c = 2;    printf("a@%p b@%p c@%p\n",&a,&b,&c);    printf("%x %x %x %x %x %x %x\n");    return 0;}
    通过gcc简单编译运行,得到以下的结果。
    这里发现第二个printf打印出了a,b,c变量的地址,中间隔了一个数之后,打印出了a,b,c的值,为什么会这样,不妨通过insight或gdb调试查看一下程序运行时的内存布局。这里使用可视化的insight进行查看,尽管关闭了地址随机化,但是insight调试中的程序地址和程序实际运行的地址是不一样的,会有一个偏移,如下:
    为了更具体地了解一下程序的运行情况,查看寄存器,得知esp为0xbffff650,查看esp地址起始的内存布局。在第二个printf处设了断点,结合汇编代码一起看,可知0xbffff664~0xbffff66c依次是a,b,c变量,esp+4~+c分别是第一个printf的参数:&a,&b,&c,esp处的0x8048510是格式化字符串的位置,常量字符串地址应该在数据段。第二个printf只是设置了0x8048520为格式化字符串地址,main函数没有为它传入其他参数,而printf不知道,当printf解析到4个%x格式符时,自动读取了比esp高的4个栈内存单元当作参数,因此打印出了我们看到的六个值,包括a,b,c的值。
(3)%n测试
#include <stdio.h>int main(){    int a = 0;    int b = 1;    int c = 2;    printf("a@%p b@%p c@%p\n",&a,&b,&c);    printf("hello world%n\n");    printf("%d %d %d\n",a,b,c);    return 0;}
    在知道程序栈布局之后,我们尝试改写变量a的值,这里把"hello world"的字符数即11写入a。这里printf("hello world%n\n");第一个参数为格式化字符串"hello world%n\n"地址,因为遇到了%n,将后一个参数作为写入地址,因为第一个printf中将&a当作了第二个参数依然留在栈中,因此%n写入了&a,改变了a的值。这个实验说明了%n可能导致一些误操作。
(4)模拟format string attack中的实验
    上一个实验中因为第一个printf调用使用了&a作为地址,默认为第二个参数,很容易就改写了a的值。但是一般的攻击程序中可能不会在此前打印变量地址,更多的只是调用printf(字符串),因此需要特殊构造的格式来进行攻击。format string attack中利用snprintf,将argv[1]写入buf,这里的str直接静态初始化了,通过printf来攻击,原理都是类似的。这里的a地址0xbffff6a4和%x是通过调试和测验事先知道的,如果填成随意一个地址运行程序可能会导致core dump。
#include <stdio.h>int main(){    char str[20]="\xa4\xf6\xff\xbf%x%x %x %x %x%n";    int a = 0;    printf(str);    printf("a=%d\n",a);    return 0;}
(注意这里的第一个%x打印出的是d,为了使str的地址对齐4,缓冲区大小须是4的倍数,大小为20时空格分配有点不够用了,和后面的值连在了一起。)这里的%n对应printf认为的的第7个参数,刚好是str的低4个字节,存放的是0xbffff6a4即a的地址,因此%n向a的地址写入已输出的字节数31。
为了知道第多少个参数对应的是str和a,可以先通过多个%x打印栈布局得知。对于调试得知a地址的方法是随便填充一个数作为前4字节(中间不要出现0x00字节,否则会被当作终止符),如\x12\x34\x56\x78,然后导致core dump,通过gdb 可执行程序 core查看程序崩溃时的环境。
    进入gdb之后看到程序在vfprintf(printf内部调用的)处终止了,具体哪一步出错这里没有进一步探究。
    因为不是main的栈帧了,所以esp并不是main的esp,ebp保存上一个栈帧esp+4的位置,按道理即是printf的栈帧,不过我们还是可以在此附近查看寻找是否有main栈帧的内容。\x12\x34\x56\x78%x%x %x %x %x%n的16进制格式还是比较好辨认的,找到0x78563412,发现0xbffff6a8是str地址,这也与按顺序推导的第一个参数0xbffff690处的值一致,因为printf将str当作了格式化字符串,因此str地址是第一个参数,0xbffff6a4即为a的地址。这里发现另一个有意思的东西是这段代码里加入了canary的检查,开始从gs加载值存在栈中,而最后通过异或操作进行比较,如果不同则调用__stack_chk_fail异常处理函数,而前面的测试代码是没有的。


0 0
原创粉丝点击