zoukankan      html  css  js  c++  java
  • 格式化字符串攻击原理及示例.RP

    格式化字符串攻击原理及示例

    一、类printf函数簇实现原理

    类printf函数的最大的特点就是,在函数定义的时候无法知道函数实参的数目和类型。

    对于这种情况,可以使用省略号指定参数表。

    带有省略号的函数定义中,参数表分为两部分,前半部分是确定个数、确定类型的参数,第二部分就是省略号,代表数目和类型都不确定的参数表,省略号参数表中参数的个数和参数的类型是事先的约定计算出来的,每个实参的地址(指针)是根据确定参数表中最后一个实参的地址算出来的。

    这里涉及到函数调用时的栈操作。函数栈的栈底是高地址,栈顶是底地址。在函数调用

    时函数实参是从最后一个参数(最右边的参数)到第一个参数(最左边的参数)依次被压入栈顶方向。也就是说函数调用时,函数实参的地址是相连的,并且从左到右地址是依次增加的。如:

     1 #include <stdio.h>  
     2 #include <stdlib.h>  
     3   
     4 void fun(int a, ...)  
     5 {  
     6     int i;  
     7     int *temp = &a;  
     8     temp++;  
     9     for (i = 0; i < a; ++i)  
    10     {  
    11         printf("%d ",*temp);  
    12         temp++;  
    13     }  
    14     printf("/n");  
    15 }  
    16   
    17 int main()  
    18 {  
    19     int a = 1;  
    20     int b = 2;  
    21     int c = 3;  
    22     int d = 4;  
    23     fun(4, a, b, c, d);  
    24     return 0;  
    25 }   

    在上面的例子中,void fun(int a, ...)函数约定第一个确定参数表示省略号参数表中参数的个数,省略号参数表中的参数全都是int 类型的,这样fun函数就可以正常工作了。

    类printf函数簇的工作原理和fun函数是一样的,只不过更为复杂和精巧。

    如printf的函数形式为 int printf(const char *fmt, …)。

    由于printf函数实现的功能比较复杂,我们来看一个我们自己实现的myprintf函数,改函数不涉及低层系统io操作。

     1 #include <stdio.h>  
     2 #include <stdlib.h>  
     3   
     4 void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,//参数必须都是int 类型  
     5 {  
     6     char* pArg=NULL; //等价于printf原始实现的va_list  
     7     char c;  
     8     pArg = (char*) &fmt; //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值  
     9      pArg += sizeof(fmt); //等价于原来的va_start  
    10   
    11     do  
    12     {  
    13         c =*fmt;  
    14         if (c != '%')  
    15         {  
    16             putchar(c); //照原样输出字符  
    17           }  
    18         else  
    19         {  
    20             //按格式字符输出数据  
    21                switch(*++fmt)  
    22             {  
    23                 case 'd':  
    24                     printf("%d",*((int*)pArg));  
    25                     break;  
    26                 case 'x':  
    27                     printf("%#x",*((int*)pArg));  
    28                     break;  
    29                 default:  
    30                     break;  
    31             }  
    32             pArg += sizeof(int); //等价于原来的va_arg  
    33         }  
    34         ++fmt;  
    35     }while (*fmt != '/0');  
    36     pArg = NULL; //等价于va_end  
    37     return;  
    38 }  
    39   
    40 int main(int argc, char* argv[])  
    41 {  
    42     int i = 1;  
    43     int j = 2;  
    44     myprintf("the first test:i=%d/n",i,j);  
    45     myprintf("the secend test:i=%d; %x;j=%d;/n",i,0xabcd,j);   
    46     return 0;  
    47 }  

    myprintf函数中也有类似的约定,确定参数表中最后一个参数是一个const char* 类型的字符串,在这个字符串中出现“%d”和“%x”次数的和就是省略号参数表中参数的个数,省略号参数表中的参数类型也都是int类型。

    同样的,实际的printf函数也有这样的约定:确定参数表中最后一个参数是一个const char* 类型的字符串,省略号参数表中参数个数就是这个字符串中出现的“%d”,“%x”,“%s”…次数的和,省略号参数表中参数的类型也是由“%d”,“%x”,“%s”……等格式化字符来指示的。

    因此,类printf函数中省略号参数表中参数的个数和类型都是由类printf函数中的那个格式化字符串来决定的。

    二、格式化字符串攻击原理

    因为类printf函数中省略号参数表中参数的个数和类型都是由类printf函数中的那个格式化字符串来决定的,所以攻击者可以利用编程者的疏忽或漏洞,巧妙构造格式化字符串,达到攻击目的。

    如果一个程序员的任务是:打印输出一个字符串或者把这个串拷贝到某缓冲区内。他可以写出如下的代码:printf("%s", str);但是为了节约时间和提高效率,并在源码中少输入6个字节,他会这样写:printf(str);

    为什么程序员写的是错误的呢?他传入了一个他想要逐字打印的字符串。实际上该字符串被printf函数解释为一个格式化字符(formatstring),printf就会根据该字符串来决定printf函数中省略号参数表中参数的格式和类型,如果这个程序员想要打印的字符串中刚好有“%d”,“%x”之类的格式化字符,那么一个变量的参数值就从堆栈中取出。

    比如:

     1 #include <stdio.h>  
     2 #include <stdlib.h>   
     3   
     4 int main(int argc, char* argv[])  
     5 {  
     6     if(argc != 2)  
     7         return 0;  
     8     printf(argv[1]);  
     9     return 0;  
    10 }

    当./a.out “hello world”时一切正常,但是当./a.out “%x”时,就会有莫名其妙的数字被打印出来了。

    很明显,攻击者至少可以通过打印出堆栈中的这些值来偷看程序的内存。但是有些事情就不那么明显了,这个简单的错误允许向运行中程序的内存里写入任意值。

    printf有一个比较另类的用法:%n,当在格式化字符串中碰到"%n"的时候,在%n域之前输出的字符个数会保存到下一个参数里。例如,为了获取在两个格式化的数字之间空间的偏量:

    1 int main(int argc, char* argv[])  
    2 {  
    3     int pos, x = 235, y = 93;  
    4     printf("%d %n%d/n", x, &pos, y);  
    5     printf("The offset was %d/n", pos);  
    6     return 0;  
    7 }

    输出4(“235 ”的长度)

    %n格式返回应该被输出的字符数目,而不是实际输出的字符数目。当把一个字符串格式化输出到一个定长缓冲区内时,输出字符串可能被截短。不考虑截短的影响,%n格式表示如果不被截短的偏量值(输出字符数目)。为了说明这一点,下面的代码会输出100而不是20:

    1 int main()  
    2 {  
    3     char buf[20];  
    4     int pos, x = 0;  
    5     snprintf(buf, sizeof(buf), "%.100d%n", x, &pos);  
    6     printf("position: %d/n", pos);  
    7     return 0;  
    8 }

    而%n和%d,%x,%s的显著的不同就是%n是会改变变量的值的,这也就是格式化字符串攻击的爆破点。

    三、一个实际的例子

    下面这个例子至少可以X86的Redhat和arch Linux下面进行演示。

     1 #include <stdio.h>  
     2 #include <stdlib.h>  
     3 #include <string.h>  
     4   
     5 char daddr[16];  
     6   
     7 int main(int argc, char **argv)  
     8 {  
     9     char buf[100];  
    10     int x;  
    11     x = 1;  
    12     memset(daddr,'/0',16);  
    13     printf("before format string x is %d/%#x (@ %p)/n", x, x, &x);  
    14     strncpy(daddr,"PPPPPPP%n",9);         
    15     snprintf(buf,sizeof(buf),daddr);   //实施格式化字符串攻击  
    16   
    17     buf[sizeof(buf) - 1] = 0;  
    18     printf("after format string x is %d/%#x (@ %p)/n", x, x, &x);  
    19     return 0;  
    20 }

    运行的结果是:x被成功的改成了7。

    上面的例子利用了linux函数调用时的内存残像,来实现格式化字符串攻击的。(参考的经典文章是用猜地址的方法来实现的,猜的一头雾水)

    这里我们来分析一下main函数中的堆栈变化情况:

    绘图1

    如上图所示,在调用snprintf函数之前,首先调用了printf函数,printf的函数第四个参数是&x,这样在main函数的堆栈内存中留下了&x的内存残像。当调用snprintf时,系统本来只给snprintf准备了3个参数,但是由于格式化字符串攻击,使得snprinf认为应该有四个参数传给它,这样snprintf就私自把&x的内存残像作为第4个参数读走了,而snprintf所谓的第4个参数对应的“%n”,于是snprintf就成功的修改了变量x的值。

    而在实际网络环境中可利用的格式化字符串攻击也是很多的。下图就是一个实际网络攻击的截图。

    Screenshot

    文.RP.URL:http://blog.csdn.net/immcss/article/details/6267849

  • 相关阅读:
    maven surefire入门
    编译原理随笔4(自下而上的语法分析-递归法)
    编译原理随笔3(自上而下的语法分析-推导法)
    编译原理随笔1
    LeetCode刷题笔记-DP算法-取数问题
    算法刷题笔记-stack-四则运算
    LeetCode刷题笔记-递归-反转二叉树
    Beta里程碑总结
    评价cnblogs.com的用户体验
    我们的团队目标
  • 原文地址:https://www.cnblogs.com/Leroscox/p/6036291.html
Copyright © 2011-2022 走看看