缓冲区溢出——安全漏洞的发源地

高质量程序设计艺术》样章连载——3.2 缓冲区溢出

<o:p> </o:p>

原书名:Code Quality: The Open Source Perspective

<o:p></o:p>

<!---->1.         <!---->深入剖析著名开源软件的质量问题<o:p></o:p>

<!---->2.         <!---->全面阐述CC++Java代码中的常见编程错误<o:p></o:p>

<!---->3.         <!---->指导你编写优秀代码的圣经<o:p></o:p>

<o:p> 缓冲区溢出——安全漏洞的发源地</o:p>

更多详细信息http://www.china-pub.com/37661<o:p></o:p>

----------------------------------------------------------------------------------------------------------------------<o:p></o:p>

3.2  缓冲区溢出

<!----><!----><!---->在检查C/C++代码时,最常见到的安全漏洞就是缓冲区溢出buffer overflowbuffer overrun)。对已经被利用的安全漏洞所做的分析表明,多达50%的漏洞源于缓冲区溢出。缓冲区溢出产生的原因是粗心的代码在特定数据结构的范围之外进行写入操作。不管使用何种语言,不小心的话都会出现这种问题:一段错误的代码序列对程序维护的数据结构的错误位置进行写入操作,导致程序工作不正常。在C/C++程序中,这个问题尤其严重,原因是当前大多数C/C++编译器与运行时环境的设计,允许使用指针和数组下标对应用的数据做不受限制的访问,并且还允许执行位于应用数据区与栈区的代码。因此,在一般的C/C++程序中,一个指向数组范围之外的数组下标将会存取对应内存位置处的数据,而且是可读可写的。同样的情况,如果是Java程序的话,会导致ArrayIndexOutOfBoundsException异常;如果是Perl程序的话,会自动增加数组的大小。

攻击者利用程序中的缓冲区溢出,在执行该程序的计算机上执行任意代码的方法有很多种。最简单的办法是直接将一个位于栈上的(函数局部的)缓冲区末端的函数返回地址覆盖,让它指向该缓冲区中特别编制的代码。还有许多其他方法也可以(而且已经)被用来执行攻击者的代码,比如利用储存在堆中的缓冲区以及储存在静态存储中的缓冲区。基于本书的宗旨——以真正在使用的代码为例——现在让我们来看一下NETBSDOpenBSD版本的FTP服务器ftpd<!---->[1]<!---->的一个单字节偏移(off-by-one)错误是如何被利用,使得攻击者可以在宿主机上获得命令行访问的。我们的目的不是要详细解释缓冲区溢出的工作原理—如果你确实感兴趣的话,请参阅ViegaMcGraw的著作[VM01]的第7章,而是想要通过一个详尽的例子来告诉读者,即使是一个很小的错误也能够让攻击者获得对整个系统的控制。

与这个漏洞相关的代码如图3-1所示。函数replydirname将会把name复制到npath,将所有的双引号使用一个额外的双引号进行转义。问题在于,尽管代码在基本的for循环里面很小心地进行处理,保证写入的范围不超出npath缓冲区的范围,但是在添加作为转义符的双引号时(图3-1:1)却没有进行边界检查:写入位置是否已经超出了缓冲区的大小。其结果就是,如果一个1 024字符(MAXPATHLEN)的目录名以一个双引号字符结束,用来表示字符串结束的’\0’字符会被写入到缓冲区外去,这也就是如下的攻击代码的工作原理:

/*

h0h0h0 O-day k0d3z

Scrippiedvorakjimjones的帮助下开发

<!----> 缓冲区溢出——安全漏洞的发源地


<!---->

<!---->[1]<!----> netbsdsrc/libexec/ftpd

为了有助于理解为什么这个看起来没什么大不了的单字节偏移错误能够让攻击者在你的系统上执行他们的代码,我们需要研究一下在程序执行时其栈的布局(另请参见5.6节)。表3-3给出了在replydirname即将返回调用者pwd时,栈的布局。如你所见,函数被调用的时候,其栈框架上储存着传入的参数、保存的上一个栈框架的框架指针(通常是一个寄存器——这里是%ebp——用于访问框架的内容),以及局部变量(也请参阅5.6.1节)。针对ftpd的攻击包括构造并使用一个包含以下各个部分的1 024字符的目录名:

<!----> <!---->一个本地可写的目录(例如/pub/incoming)的绝对路径名,这保证了目录名可以通过一系列的MKDIRCHDIR命令逐步构造,而且不会一开始就触发单字节偏移错误。

<!----> <!---->填充(AAAA/AAAA…)字节,以确保目录名有1 024字节长。

<!----> <!---->攻击者想要在被攻击的计算机上执行的代码(\x31\xc0\x89\xc1\x80\xc1"…),该段代码的功能是通过FTP连接提供命令行访问。

<!----><!---->同一个返回地址的多个实例,该返回地址用于将程序流程改变到执行攻击代码(\x4f\x09\x00\xd0\x4f\x09\x00\xd0"…——请看后面的解释)。

<!----> <!---->填充更多字节,以一个双引号结尾(AAA"),用来触发偏差为一错误。

攻击所使用的路径的完整结构如表3-3所示,从内存地址0xd0000710开始。攻击代码通过一个FTP连接创建具有如前所述名字的目录,(通过一系列的CHDIR命令)改变当前目录为所生成的目录,然后调用PWD命令,使得pwdreplydirname函数被调用。请注意,在pwdpath变量与replydirnamenpath变量中储存的是相同的目录名。

pwd函数试图返回其调用者(yyparse)时,攻击者的代码就会被执行到。在i386体系结构上,从一个函数返回时需要对展开其栈框架,其操作如下:

<o:p> </o:p>

<o:p>       </o:p>

3-3  ftpd缓冲区攻击发生时的栈<o:p></o:p>

    <o:p></o:p>

    <o:p></o:p>

<o:p></o:p>

pwd的栈框架<o:p></o:p>

0xd0000b14<o:p></o:p>

0xd0000b10<o:p></o:p>

0xd0000b0f<o:p></o:p>

0xd0000b0e<o:p></o:p>

0xd0000b0d<o:p></o:p>

0xd0000b0c<o:p></o:p>

返回地址(yyparse + 3 583<o:p></o:p>

保存的栈框架指针(%ebp<o:p></o:p>

path[]的最后一个字节<o:p></o:p>

填充字节<o:p></o:p>

<o:p> </o:p>

目录路径分隔符<o:p></o:p>

0x8052000<o:p></o:p>

<o:p> </o:p>

A<o:p></o:p>

A<o:p></o:p>

A<o:p></o:p>

/<o:p></o:p>

pwd的栈框架<o:p></o:p>

0xd0000b08<o:p></o:p>

0xd0000b04<o:p></o:p>

0xd0000b00<o:p></o:p>

<o:p> </o:p>

0xd0000a10<o:p></o:p>

0xd0000a0c<o:p></o:p>

0xd0000a0b<o:p></o:p>

<o:p> </o:p>

0xd000094f<o:p></o:p>

0xd000094e<o:p></o:p>

<o:p> </o:p>

0xd0000710<o:p></o:p>

攻击者放置的返回地址<o:p></o:p>

攻击者放置的返回地址<o:p></o:p>

攻击者放置的返回地址<o:p></o:p>

…<o:p></o:p>

攻击者放置的返回地址<o:p></o:p>

目录路径分隔符<o:p></o:p>

攻击代码的最后一个字节<o:p></o:p>

…<o:p></o:p>

攻击代码的第一个字节<o:p></o:p>

填充字节<o:p></o:p>

…<o:p></o:p>

path[]的第一个字节<o:p></o:p>

0xd000094f <o:p></o:p>

0xd000094f<o:p></o:p>

0xd000094f<o:p></o:p>

0xd000094f<o:p></o:p>

0xd000094f<o:p></o:p>

/<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

xor %eax, %eax<o:p></o:p>

A<o:p></o:p>

pub/incoming/AA…<o:p></o:p>

/<o:p></o:p>

replydirname的栈框架<o:p></o:p>

0xd00006fc<o:p></o:p>

0xd00006f8<o:p></o:p>

0xd00006f4<o:p></o:p>

0xd00006f0<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

0xd00006ef<o:p></o:p>

0xd00002f0<o:p></o:p>

0xd00002ec<o:p></o:p>

压栈的message参数<o:p></o:p>

压栈的name参数<o:p></o:p>

返回地址(pwd+116<o:p></o:p>

保存的栈框架指针(%ebp<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

npath[]的最后一个合法字节<o:p></o:p>

npath[]的第一个合法字节<o:p></o:p>

i<o:p></o:p>

“is the …”<o:p></o:p>

0xd0000710<o:p></o:p>

0x8048674<o:p></o:p>

0xd0000b10<o:p></o:p>

(正常情况)<o:p></o:p>

0xd0000b00<o:p></o:p>

(遭受攻击时)<o:p></o:p>

A<o:p></o:p>

/<o:p></o:p>

在表3-4中,可以看到从replydirname返回pwd再从pwd返回其调用者yyparse时,这个操作是怎么进行的。现在看看当replydirname的代码将npath[]缓冲区后面的字节用0覆写时的情况。这个值为0的一个字节将被写入的地址就是replydirname储存其调用者(pwd)的栈框架指针的地方:0xd00006f0。其结果就是,保存的栈框架指针的最低字节从0x10变成了0x00,也就是对应的指针从0xd0000b10变成了0xd0000b00。现在让我们看看表3-5列出的新的返回序列。当replydirname返回到pwd的时候,它从栈上取出的栈框架指针的值0xd0000b00是错误的,而且pwd会使用这个值来恢复其栈指针。这个错误的栈指针值(0xd0000b00)指向path[]的内存区域,其位置恰好包含着可以给攻击者提供(他们垂涎已久的)对我们的机器的命令行访问能力的代码。故事结束了。

3-4  展开栈(默认情况)<o:p></o:p>

    <o:p></o:p>

    <o:p></o:p>

    <o:p></o:p>

replydirname + 183展开:<o:p></o:p>

0x80485f7<o:p></o:p>

0x80485f9<o:p></o:p>

0x80485fa<o:p></o:p>

mov %ebp, %esp<o:p></o:p>

pop %ebp<o:p></o:p>

ret<o:p></o:p>

此时%esp0xd00006f0<o:p></o:p>

%ebp成为0xd0000b10<o:p></o:p>

返回到pwd0x8048674<o:p></o:p>

pwd + 116展开:<o:p></o:p>

0x8048674<o:p></o:p>

0x8048677<o:p></o:p>

0x8048679<o:p></o:p>

0x804867a<o:p></o:p>

add $0x10, %esp<o:p></o:p>

mov %ebp, %esp<o:p></o:p>

pop %ebp<o:p></o:p>

ret<o:p></o:p>

<o:p> </o:p>

此时%esp0xd0000b10<o:p></o:p>

%esp成为0xd0000b14<o:p></o:p>

返回到yyparse0x8052000<o:p></o:p>

yyparse + 3 583继续:<o:p></o:p>

0x8052000<o:p></o:p>

jmp 0x8052123<o:p></o:p>

<o:p> </o:p>

3-5  在遭受攻击时展开栈<o:p></o:p>

<t>

你可能感兴趣的:(数据结构,C++,c,框架,C#)