通透讲解缓冲区溢出

 
 
 
缓冲区溢出 在英文中可以解释为: buffer overflow buffer overrun smash the
stack
trash the stack scribble the stack mangle the stack memory
leak
overrun screw
我们通常所说的 溢出 指的是缓冲区溢出,(废话,不然要从那里溢出呀!)先解释一下什么是缓冲区 ―― 缓冲区是内存中存放数据的地方,是程序运行时计算机内存中的一个连续的块,它保存了给定类型的数据。问题随着动态分配变量而出现。为了不用太多的内存,一个有动态分配变量的程序在程序运行时才决定给他们分配多少内存。当程序试图将数据放到计算机内存中的某一位置,但没有足够空间时会发生缓冲区溢出。另一种说法,就是说程序在动态分配缓冲区放入太多的数据会有什么现象?它会溢出,会漏到了别的地方。

一个缓冲区溢出应用程序使用这个溢出的数据将汇编语言代码放到计算机的内存中,通常是产生 root 权限的地方。单单的缓冲区溢出,并不会产生安全问题。只有将溢出送到能够以 root 权限运行命令的区域才行。这样,一个缓冲区利用程序将能运行的指令放在了有 root 权限的内存中,从而一旦运行这些指令,就是以 root 权限控制了计算机。

所以我们更多的时候把缓冲区溢出指的是一种系统攻击的手段,通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。据统计,通过缓冲区溢出进行的攻击占所有系统攻击总数的 80% 以上。

世界上第一个缓冲区溢出攻击 ―― 著名的 Morris 蠕虫,发生在十年前,它曾造成了全世界 6000 多台网络服务器瘫痪。我这是我所知的最早的缓冲区溢出攻击程序。

记住,造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数!!!
下面举一个最为常见的,也是最简单的溢出。
void function(char *str){
char buffer[16];
strcpy(buffer,str);
}
在这个例子中上面的 buffer 的长度被限制在 16 ,而 strcpy() 将直接把 str 中的内容 copy buffer 中。这样只要 str 的长度大于 16 ,就会造成 buffer 的溢出,使程序运行出错。 So ,我们说这个程序溢出了。

在C语言中,静态变量是分配在数据段中的,动态变量是分配在堆栈段的。缓冲区溢出是利用堆栈段的溢出的。一个正常的程序在内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程序的机器码和只读数据,这个段通常是只读,对它的写操作是非法的。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们的位置如下:

  
  / �D�D�D�D�D�D�D�D\   内存低端
  程序段
   �D�D�D�D�D�D�D�D�D
  数据段
   �D�D�D�D�D�D�D�D�D
  堆栈
   \�D�D�D�D�D�D�D�D�D /内存高端
堆栈是内存中的一个连续的块。一个叫堆栈指针的寄存器( SP )指向堆栈的栈顶。堆栈的底部是一个固定地址。堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。它支持两个操作, PUSH POP PUSH 是将数据放到栈的顶端, POP 是将栈顶的数据取出。

在高级语言中,程序函数调用和函数中的临时变量都用到堆栈。为什么呢?因为在调用一个函数时,我们需要对当前的操作进行保护,也为了函数执行后,程序可以正确的找到地方继续执行,所以参数的传递和返回值也用到了堆栈。通常对局部变量的引用是通过给出它们对 SP 的偏移量来实现的。另外还有一个基址指针( FP ,在 Intel 芯片中是 BP ),许多编译器实际上是用它来引用本地变量和参数的。通常,参数的相对 FP 的偏移是正的,局部变量是负的。

当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器 (IP) 中的内容,做为返回地址 (RET) ;第三个放入堆栈的是基址寄存器 (FP) ;然后把当前的栈指针 (SP) 拷贝到 FP ,做为新的基地址;最后为本地变量留出一定空间,把 SP 减去适当的数值。

比如说下面这个程序:
void function(int a, int b, int c){
char buffer1[10];
char buffer2[15];
}
void main(){
function(1,2,3);
}
假设我们在 Linux 下,用 gcc 对这段源码进行编译,产生汇编代码输出:
   $ gcc -S -o example1.s example1.c
  看看输出文件中调用函数的那部分:
   pushl $3
   pushl $2
   pushl $1
   call function
  这就将 3 个参数推入堆栈里了,并调用 function() 。指令 call 会将指令指针 IP 压入堆栈。在返回时, RET 要用到这个保存的 IP 。在函数中,第一要做的事是进行一些必要的处理。每个函数都必须有这些过程(为了保护呀,不然就找不到了。):

  
   pushl %ebp
   movl %esp,%ebp
   subl $20,%esp
这几条指令将 EBP ,基址指针放入堆栈。然后将当前 SP 拷贝到 EBP 。然后,为本地变量分配空间,并将它们的大小从 SP 里减掉。由于内存分配是以字为单位的,因此,这里的 buffer1 用了 8 字节( 2 个字,一个字 4 字节)。 Buffer2 用了 12 字节( 3 个字)。所以这里将 ESP 减了 20 。这样,现在,堆栈看起来应该是这样的。


  低端内存 高端内存
   buffer2 buffer1 sfp ret a b 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
   < ------ [ ] [ ] [ ] [ ]
  栈顶 栈底
很明显,程序执行的结果是 &quot;Segmentation fault (core
dumped)&quot;
或类似的出错信息。因为从 buffer 开始的 256 个字节都将被 *str 的内容 'A' 覆盖,包括 sfp,
ret,
甚至 *str 'A' 的十六进值为 0x41 ,所以函数的返回地址变成了 0x41414141,
这超出了程序的地址空间,所以出现段错误。可见,缓冲区溢出允许我们改变一个函数的返回地址,在这个例子中,我们可以通过修改 0x41414141 来改变程序返回后的入口地址。同样通过这种方式,就可以改变程序的执行顺序了。

存在象 strcpy 这样的问题的标准函数还有 strcat(),sprintf(),vsprintf(),gets(),scanf(), 以及在循环内的 getc(),fgetc(),getchar() 等。

 


缓冲区溢出技术基础
为了提高大家的技术水平,为了更了解我们讨论的这种技术,为了把这个论坛建成一个更更好的论坛,下面我为大家推出一系列完整的有关溢出,溢出攻击的文章。让大家更能了解到这个天天说但又不太清楚怎么回事的东西。我想,看了这个以后大家也不再会问,为什么我用了这个工具,怎么没有用呀什么的问题?
在这里强调一下,想完全看的懂这篇文章,至少需要具备一定的汇编语言, C 语言和 LINUX 的基础。
缓冲区溢出 在英文中可以解释为: buffer overflow buffer overrun smash the
stack
trash the stack scribble the stack mangle the stack memory
leak
overrun screw
我们通常所说的 溢出 指的是缓冲区溢出,(废话,不然要从那里溢出呀!)先解释一下什么是缓冲区 ―― 缓冲区是内存中存放数据的地方,是程序运行时计算机内存中的一个连续的块,它保存了给定类型的数据。问题随着动态分配变量而出现。为了不用太多的内存,一个有动态分配变量的程序在程序运行时才决定给他们分配多少内存。当程序试图将数据放到计算机内存中的某一位置,但没有足够空间时会发生缓冲区溢出。另一种说法,就是说程序在动态分配缓冲区放入太多的数据会有什么现象?它会溢出,会漏到了别的地方。

一个缓冲区溢出应用程序使用这个溢出的数据将汇编语言代码放到计算机的内存中,通常是产生 root 权限的地方。单单的缓冲区溢出,并不会产生安全问题。只有将溢出送到能够以 root 权限运行命令的区域才行。这样,一个缓冲区利用程序将能运行的指令放在了有 root 权限的内存中,从而一旦运行这些指令,就是以 root 权限控制了计算机。

所以我们更多的时候把缓冲区溢出指的是一种系统攻击的手段,通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。据统计,通过缓冲区溢出进行的攻击占所有系统攻击总数的 80% 以上。

世界上第一个缓冲区溢出攻击 ―― 著名的 Morris 蠕虫,发生在十年前,它曾造成了全世界 6000 多台网络服务器瘫痪。我这是我所知的最早的缓冲区溢出攻击程序。

记住,造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数!!!
下面举一个最为常见的,也是最简单的溢出。
void function(char *str){
char buffer[16];
strcpy(buffer,str);
}
在这个例子中上面的 buffer 的长度被限制在 16 ,而 strcpy() 将直接把 str 中的内容 copy buffer 中。这样只要 str 的长度大于 16 ,就会造成 buffer 的溢出,使程序运行出错。 So ,我们说这个程序溢出了。

在C语言中,静态变量是分配在数据段中的,动态变量是分配在堆栈段的。缓冲区溢出是利用堆栈段的溢出的。一个正常的程序在内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程序的机器码和只读数据,这个段通常是只读,对它的写操作是非法的。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们的位置如下:

  
  / �D�D�D�D�D�D�D�D\   内存低端
  程序段
   �D�D�D�D�D�D�D�D�D
  数据段
   �D�D�D�D�D�D�D�D�D
  堆栈
   \�D�D�D�D�D�D�D�D�D /内存高端
堆栈是内存中的一个连续的块。一个叫堆栈指针的寄存器( SP )指向堆栈的栈顶。堆栈的底部是一个固定地址。堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。它支持两个操作, PUSH POP PUSH 是将数据放到栈的顶端, POP 是将栈顶的数据取出。

在高级语言中,程序函数调用和函数中的临时变量都用到堆栈。为什么呢?因为在调用一个函数时,我们需要对当前的操作进行保护,也为了函数执行后,程序可以正确的找到地方继续执行,所以参数的传递和返回值也用到了堆栈。通常对局部变量的引用是通过给出它们对 SP 的偏移量来实现的。另外还有一个基址指针( FP ,在 Intel 芯片中是 BP ),许多编译器实际上是用它来引用本地变量和参数的。通常,参数的相对 FP 的偏移是正的,局部变量是负的。

当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器 (IP) 中的内容,做为返回地址 (RET) ;第三个放入堆栈的是基址寄存器 (FP) ;然后把当前的栈指针 (SP) 拷贝到 FP ,做为新的基地址;最后为本地变量留出一定空间,把 SP 减去适当的数值。

比如说下面这个程序:
void function(int a, int b, int c){
char buffer1[10];
char buffer2[15];
}
void main(){
function(1,2,3);
}
假设我们在 Linux 下,用 gcc 对这段源码进行编译,产生汇编代码输出:
   $ gcc -S -o example1.s example1.c
  看看输出文件中调用函数的那部分:
   pushl $3
   pushl $2
   pushl $1
   call function
  这就将 3 个参数推入堆栈里了,并调用 function() 。指令 call 会将指令指针 IP 压入堆栈。在返回时, RET 要用到这个保存的 IP 。在函数中,第一要做的事是进行一些必要的处理。每个函数都必须有这些过程(为了保护呀,不然就找不到了。):

  
   pushl %ebp
   movl %esp,%ebp
   subl $20,%esp
这几条指令将 EBP ,基址指针放入堆栈。然后将当前 SP 拷贝到 EBP 。然后,为本地变量分配空间,并将它们的大小从 SP 里减掉。由于内存分配是以字为单位的,因此,这里的 buffer1 用了 8 字节( 2 个字,一个字 4 字节)。 Buffer2 用了 12 字节( 3 个字)。所以这里将 ESP 减了 20 。这样,现在,堆栈看起来应该是这样的。


  低端内存 高端内存
   buffer2 buffer1 sfp ret a b 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
   < ------ [ ] [ ] [ ] [ ]
  栈顶 栈底
很明显,程序执行的结果是 &quot;Segmentation fault (core
dumped)&quot;
或类似的出错信息。因为从 buffer 开始的 256 个字节都将被 *str 的内容 'A' 覆盖,包括 sfp,
ret,
甚至 *str 'A' 的十六进值为 0x41 ,所以函数的返回地址变成了 0x41414141,
这超出了程序的地址空间,所以出现段错误。可见,缓冲区溢出允许我们改变一个函数的返回地址,在这个例子中,我们可以通过修改 0x41414141 来改变程序返回后的入口地址。同样通过这种方式,就可以改变程序的执行顺序了。

存在象 strcpy 这样的问题的标准函数还有 strcat(),sprintf(),vsprintf(),gets(),scanf(), 以及在循环内的 getc(),fgetc(),getchar() 等。

你可能感兴趣的:(职场,溢出,休闲)