当前位置:首页 期刊杂志

利用Metasploit 破解栈缓存溢出漏洞的一个例子

时间:2024-09-03

◆李维峰

(中国飞行试验研究院 陕西 710089)

栈缓存溢出漏洞自1988年出现至今已过去30多年了[1],它以其危害性及广泛性早已引起了广大信息安全领域研究人员的重视。多年以来,随着攻防双方技术的交替进步,对于此类漏洞的控制已经不像最初阶段那么无力,但是不得不说,由于栈缓存溢出漏洞的产生根源是程序设计不严谨所导致,换个更直接的说法就是该漏洞的产生可认为是人为的编码疏忽,因此,在未来很长的一段时间内它还将与我们共存。本文将利用Metasploit框架开发一个针对软件bof-server栈缓存溢出漏洞的破解模块,以此为例,讲解此类漏洞的危害及预防措施。由于Metasploit框架是进行渗透测试的优秀工具,它提供了大量的漏洞渗透模块库,集成了优秀的模块开发环境,所以,我们选择它作为本文的实验工具。

1 栈缓存溢出漏洞的原理

在微软的定义里,缓存溢出攻击是一种攻击者用自己的代码覆盖程序原有代码的行为,如果被覆盖的恶意代码是一段受攻击者控制的可执行代码,那么攻击者就可以在目标系统中进行意料之外的操作。

栈是一种数据结构,栈中数据的写入和读取只能从栈顶进行操作,它遵循后进先出的原则。栈支持两种操作:push和pop。push是将数据添加到栈顶。pop是将数据从栈顶弹出。让我们看一下C程序的内存布局、它的内容以及它在函数调用和返回期间是如何工作的。如图1所示。

其中,Text:包含要执行的程序代码。Data:包含程序的全局信息。Stack:包含函数参数,返回地址和函数的局部变量。它是后进先出的数据结构。随着新函数的调用,它在内存中向下增长(从较高的地址空间到较低的地址空间)。Heap:容纳所有动态分配的内存。每当我们使用malloc动态获取内存时,它都是从堆中分配的。随着需要越来越多的内存,堆在内存中的增长(从低到高)。

对于基于栈的缓存溢出,我们把注意力集中在寄存器EBP、EⅠP和ESP上。EBP指向堆栈底部的较高内存地址,ESP则指向堆栈顶部的较低内存位置。EⅠP中存储的是下一条指令的地址。我们主要关注EⅠP寄存器,因为我们需要劫持程序的执行顺序。由于EⅠP只是一个寄存器,所以我们无法为其分配要执行指令的内存地址。

图1 C程序中的内存结构

图2 栈的内存结构

当函数执行时,一个包含有函数信息的栈帧(stack frame)会被压入栈中。一旦函数执行完毕,栈帧会被弹出栈,函数完成执行后,将从栈中弹出相关的栈帧,并在中断的调用的函数中继续执行。CPU知道必须从何处继续执行程序,它是从调用函数时压入栈的返回地址获得此信息。

为了方便理解,我们举一个例子:在主函数中调用func()。因此,当程序开始时,将调用main(),并为其分配一个栈帧并将其压入栈。接下来main()调用func(),同样是分配栈帧,将其压入栈并将执行移交给func(),main()通过将这个值(返回地址)压入栈,来指出当func()返回(通常是在调用func()之后的代码行)时,需要继续执行的地方。

图3 栈帧结构

在func()函数执行完后,它的栈帧被弹出,其中存储的返回地址被加载到EⅠP寄存器中,继续执行main()。如果我们能够控制返回地址,我们就能在func()返回时劫持将要执行的指令。

2 渗透思路

首先,我们下载并运行bof-server。可以看到这个程序在端口200上提供TCP服务。如图4所示:

图4 程序在端口200上提供TCP服务

然后,我们向TCP 200端口发起TELNET连接,建立连接后向其发送若干随机数据。如图5所示:

图5 建立连接后向其发送若干随机数据

我们发现,当提供一定数量的随机数据之后,连接就断开了,这是因为目标服务器已经崩溃。来看一看目标服务器上的报错信息,如图6所示:

图6 目标服务器上的报错信息

点击“click here”,查看详细情况,发现程序是由于无法在地址41414141处找到下一条要执行的指令,从而导致了程序的崩溃。因为我们随机输入的是若干个字母A,而值41就是字母A的十六进制表示,这说明我们输入的数据已经超出了缓存的范围,而且覆盖了EⅠP寄存器。接下来,程序试图执行41414141这个地址上指令,显然这不是一个有效的地址,因此,程序崩溃了。

由于我们的输入数据超出了程序栈的缓存范围,引发了程序的栈缓存溢出漏洞,导致程序崩溃。如果我们控制好输入的数据,使得覆盖EⅠP寄存器的内容恰好是我们想要执行的代码地址,那么我们就控制了服务器,从而完成了漏洞利用。

3 渗透的步骤

根据上一节的思路,我们将利用Metasploit框架开发一个破解模块,触发漏洞并运行我们想要执行的其他代码。

模块开发的第一个步骤是找出偏移量,在这个过程中将用到Metasploit中的两款工具,分别是pattern_create和pattern_offset。工具pattern_create用来按一定规律生成字符,例如:#./pattern_create.rb 1000,表示生成1000个字符。将这些字符发送给目标服务器后如果程序崩溃,就能得到一个地址的值,我们得到的地址值是72413372,将该值作为参数,使用工具pattern_offset就能得到具体的偏移量,例如:#./pattern_offset.rb 72413372 1000,表示EⅠP中的地址为72413372,填充1000个字符。最后,我们得出的偏移量是520,在520个字节后面的4个字节的数据就会覆盖EⅠP寄存器。

接下来,我们还将使用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指令的地址。只需要用这个地址来重写EⅠP寄存器中的内容,就可以执行我们的代码。

到目前为止,我们已经掌握了开发Metasploit程序破解模块的主要内容,让我们看看代码是怎样的。如下所示:

在分析代码之前,我们先看看模块中用到的库,请看表1。

表1 模块中用到的库

破解模块开头就是包含各种必要的路径和文件。我们把模块类型定义为Msf::Exploit::Remote,意味着它是一个远程破解模块。接下来,我们在initialize方法中定义name,description,author等基本信息。另外,我们还看到大量的其他声明。请看表2:

表2 破解模块

避免程序崩溃或payload 不执行

让我们看看上面代码中用到的一些重要函数,如表3。

表3 重要函数

在我们之前编写的模块中,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之后。

图7 步骤1

接下来,使用函数sock.put建立与目标的连接,参数是buf。使用handler方法检查目标是否被成功破解,如果成功破解则会建立连接。最后,用disconnect断开连接。我们来看看使用的效果:我们设置必要的参数,payload设置为Windows/meterpreter/bind_tcp,这意味着到目标的直接连接。让我们看看使用exploit命令后会发生什么(图8)。

图8 步骤2

显然,我们编写的破解模块成功了,获得一个meterpreter会话,通过该会话可以在目标服务器上进行非授权操作。

4 总结

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

免责声明

我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自各大过期杂志,内容仅供学习参考,不准确地方联系删除处理!