When looking for vulnerabilities of interest, it’s always a good option to look for vulnerabilities used at pwn2own. Indeed, these vulnerabilities are exploited during the competition, meaning they have a practical impact.

  • Thomas is recognized as one of the several highly skilled researchers in the French exploit scene, and is certainly knowledgeable
  • The vulnerability, teased in the HITB’s upcoming conference, is described as: “a logical bug that defeats most mitigations by allowing direct read and write access to kernel virtual memory.

Finding The Root Cause 

Starting with the ZDI’s advisory of the vulnerability, it is possible to get enough details to look for the root cause. Typically, the important information is:

  • The vulnerability is present in the mskssrv driver
  • The issue results from the lack of proper validation of a user-supplied value prior to dereferencing it as a pointer

The next step is to patch-diff the driver for the update, correcting the vulnerability. Only one function—named FsAllocAndLockMdl—was modified.

Within that function, the AccessMode parameter of the call to MmProbeAndLockPages was changed from KernelMode to UserMode, as shown in the following

Screenshot:

With the root cause found, let’s analyze the vulnerability.

Understanding The Vulnerability 

MDL, What is that? 

An I/O buffer that occupies a contiguous virtual memory range can be non-contiguously distributed over several physical pages in memory.

The Windows OS utilizes memory descriptor list (MDL) structures at the kernel level to describe a single virtual memory buffer’s physical page layout.

MDLs vary in size, are semi-opaque, and are composed of a header that describes the MDL’s properties and a variable-size array of pointers—called the page frame number (PFN) array—describing the physical addresses used by the MDL.

Inside the header, the virtual address (VA) of the memory buffer that is physically described by the MDL is present, as well as its length.

The following schematics illustrates the concept of MDL:

MDL creation and interaction are reserved for the components operating in kernel mode, like drivers, even though a user-facing process may be able to do so via a communication channel with a driver.

It should be noted that an address that is already mapped and potentially in use by the OS can have its physical page layout described by a new MDL.

In this case, the component accessing this MDL will be able (overly simplified) to directly access the physical memory pointed to by this buffer VA.

This creates a communication channel between this component and the OS component that is already interacting with the VA.

As a consequence, MDLs can allow drivers for the Windows OS to implement Direct Memory Access (DMA) operations and permit memory-copy operations between the user land and the kernel land (Direct I/O).

Inner Workings of IoAllocateMdl and MmProbeAndLockPages 

In the vulnerable functionFsAllocAndLockMdl, two APIs permitting interaction with MDLs are used: IoAllocateMdl and MmProbeAndLockPages.

The first API—IoAllocateMdl—allocates the MDL structure’s storage to the virtual memory, sets the buffer VA that the MDL describes in its header, but does not initialize the PFN array describing the physical memory that will be used for the buffer.

In fact, it should be coupled with a second API call that is responsible for establishing this array, thus acquiring the correct physical memory to describe it.

The second API—MmProbeAndLockPages— first probes the buffer VA described by the MDL—i.e., it will check if this buffer VA can be accessed—in case the the AccessMode parameter is set to UserMode.

Next, this function locks the physical pages, making them unable to be paged, reallocated, or freed while setting the access operation (read and/or write).

Let’s describe what the probing consists of:

As already described, new MDLs can be created to get the physical description of a given virtual address’s buffer already in use by the OS and potentially to directly interact with the physical memory associated with this buffer.

In particular, it can be used against various data already in use at the kernel level.

This is a problem when the MDL parameters come from the user-land (for instance, because that user-land process aims to perform a DMA operation), for example, through a DeviceIO control message made to a driver.

Indeed, if the user-land process passes in kernel pointers for the creation of the MDL, and is then able to interact with it, that means this user-land process would be able to interact with kernel data.

As a consequence, the user/kernel barrier is broken. To avoid this problem, the probing simply checks that the buffer VA in the MDL is not in the land, by checking that the address is not superior to 0x7FFFFFFF0000.

Explaining The Root Cause 

The FsAllocAndLockMdl function is reachable through a DeviceIO control message with code 0x2f0408 . In particular, the parameters for the MDL creation are directly taken from the user-supplied SystemBuffer.

As the AccessMode parameter of MmProbeAndLockPages was not correctly set to UserMode, no probing of the MDL occurs. As a consequence, the user can create a MDL pointing to critical kernel data.

As CVE-2023-29360 was exploited, it means there is a way for the user to interact later with the arbitrarily created MDL, especially to directly modify the kernel data pointed at by it.

In particular, it appears that a second DeviceIO control message with code 0x2f0410, permits to map the previously created MDL’s physical memory directly in the user-land process’s memory, inside a variable with read and write access.

This mapping is realized through the MmMapLockedPagesSpecifyCache API. As a consequence, accessing this variable as a pointer allows the physical pages used in the MDL to be accessed and modified directly.

Exploiting The Vulnerability 

One approach to exploit CVE-2023-29360 is to first obtain a MDL describing the kernel VA where the current process privileges are defined (the kernel VA being simply obtained through a NtQuery leak), using the first DeviceIO control message. Subsequently, this MDL is mapped through the second DeviceIO control message.

As a consequence, the values located at the kernel VA, where the current process privileges are defined are now directly accessible and modifiable in the current process’s virtual memory.

The process can now freely modify its own privileges and achieve privilege escalation, for example by getting the SeDebugPrivilege.

The following steps highlight how the identified exploit approach works:

  1. The exploit process is launched. After launching, the memory layout is as follows:

2. The exploit process sends the first DeviceIO control message to mskssrv, inducing the creation of a MDL pointing to its own privileges in the kernel address space, thanks to the absence of check. The memory layout is now the following:

3. The exploit process sends the second DeviceIO control message to mskssrv, mapping the physical memory pointed by the MDL in its own virtual address space. The memory layout becomes:

4. The exploit process can now directly modify the physical memory values tied to its own privileges, to make itself highly privileged.

Conclusion 

This logical vulnerability is really powerful as it may allow for direct kernel read/write. As stated by Thomas, no mitigation currently halts a similar exploit for it.

This might change soon with the modification of the NtQuery leaks in-the-works, as such exploits will necessitate a first vulnerability to leak the kernel address of interest.

Finally, I would like to thanks Thomas Imbert one more time for having found it as I learnt a lot while analyzing it.

By the way, this attack surface might have been underlooked. Setting up the following bad yara rule for variants leads to a few results, eheh:

rule search_cve_2023_29360_variant
{
    meta:
        version = "102947593"

    strings:
        $api1 = "MmProbeAndLockPages"
        $api2 = "MmMapLockedPagesSpecifyCache"
        $s2 = { 33 D2 44 8D 42 01 } //xor edx, edx, lea r8d, [rdx+1]

    condition:
        uint16(0) == 0x5a4d and all of them
}