# CVE-2022-42864: Diabolical Cookies

## What is this repo?
This is my (incomplete) proof-of-concept exploit for CVE-2022-42864, a time-of-check-time-of-use vulnerability in IOHIDFamily that was fixed in iOS 16.2 / macOS Ventura 13.1. 

## What is the status of the proof-of-concept?
The exploit currently achieves the same "arbitrary kfree" primitive used in the multicast_bytecopy exploit. However, the subsequent exploit flow of multicast_bytecopy has been heavily mitigated against, so this is not a complete exploit, it merely demonstrates the severity of the issue.

## Should I run this?
If you have to ask, no. This does not do anything useful, it just causes a kernel panic. I do not take responsibility for any data loss or instability this code may cause.

## The bug
[Apple's comment](https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDFamily/IOHIDDevice.cpp#L1601) from the source code when this issue was fixed sums this up nicely:

```cpp
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
```

Let us have a look at the function before the patch (I have tried to label relevant lines):
```cpp
IOReturn IOHIDDevice::postElementTransaction(const void* elementData, UInt32 dataSize, UInt32 completionTimeout, IOHIDCompletion * completion)
{
    IOReturn ret = kIOReturnError;
    uint32_t   cookies_[kMaxLocalCookieArrayLength];
    uint32_t   *cookies = cookies_;
    uint32_t   cookieCount = 0;
    uint32_t   cookieSize = 0;
    uint32_t   dataOffset = 0;
    uint8_t    *data = (uint8_t*)elementData;
    IOMemoryDescriptor *elementDesc = getMemoryWithCurrentElementValues();
    require(_elementArray && elementDesc, fail);

    WORKLOOP_LOCK;

    // Find the number of cookies in the data. Check that all cookies are valid elements.                   [1]
    while (dataOffset < dataSize) {
        const IOHIDElementValueHeader *headerPtr = (const IOHIDElementValueHeader *)(data + dataOffset);
        IOHIDElementPrivate *element = GetElement(headerPtr->cookie);
        if (!element) {
            HIDDeviceLogError("Could not find element for cookie: %d", headerPtr->cookie);
            ret = kIOReturnAborted;
            goto fail;
        }
        cookieCount++;

        require_noerr_action(os_add3_overflow(dataOffset, headerPtr->length, sizeof(IOHIDElementValueHeader), &dataOffset), fail, HIDDeviceLogError("Overflow iterating cookie data buffer %u %u", dataOffset, headerPtr->length));
    }
    // Data isn't as large as expected, don't overrun, just abort
    if (dataOffset != dataSize) {   //                                                                      [2]
        HIDDeviceLogError("Cookie data buffer is smaller than expected. %u vs. %u",
                        (unsigned int)dataSize, (unsigned int)dataOffset);
        ret = kIOReturnAborted;
        goto fail;
    }
    dataOffset = 0;

    require_noerr_action(os_mul_overflow(cookieCount, sizeof(uint32_t), &cookieSize),
                        fail,
                        HIDDeviceLogError("Overflow calculating cookieSize"));

    cookies = (cookieCount <= kMaxLocalCookieArrayLength) ? cookies : (uint32_t*)IOMallocData(cookieSize); // [3]

    if (cookies == NULL) {
        ret = kIOReturnNoMemory;
        goto fail;
    }

    // Update the elements, this replaced the shared kernel-user shared memory.
    for (size_t index = 0; dataOffset < dataSize; ++index) {    //                                          [4]
        const IOHIDElementValueHeader *headerPtr;
        IOHIDElementPrivate *element;
        OSData *elementVal;

        headerPtr = (const IOHIDElementValueHeader *)(data + dataOffset);
        element = GetElement(headerPtr->cookie);
        dataOffset += headerPtr->length + sizeof(IOHIDElementValueHeader);

        elementVal = OSData::withBytesNoCopy((void*)headerPtr->value,
                                            headerPtr->length); //                                          [5]
        require_action(elementVal, fail, ret = kIOReturnNoMemory);
        element->setDataBits(elementVal);
        elementVal->release();

        cookies[index] = headerPtr->cookie; //                                                              [6]
    }

    // Actually post elements
    ret = postElementValues((IOHIDElementCookie *)cookies, (UInt32)cookieCount, 0, completionTimeout, completion);

fail:
    WORKLOOP_UNLOCK;
    if (cookies != &cookies_[0]) {
        IOFreeData(cookies, cookieSize);
    }

    return ret;
}
```

- The loop at `[1]` counts the number of `IOHIDElementValue`s in the buffer, and stores this count in `cookieCount`.
- The check at `[2]` (combined with the condition of the while loop) will make sure that the `length` field of each header does not extend out of the bounds of the `elementData` buffer (nor can it fall short of the end of the buffer, although this is less relevant).
- Once all the elements have been counted and sanity-checked by this loop, a `cookies` buffer is allocated to the heap at `[3]` with a size of `cookieCount * 4` (or a stack buffer is used if `cookieCount` is sufficiently small).
- A second loop at `[4]` then makes a second pass through the buffer, parsing the `IOHIDElementValue`s again.
- `OSData` objects are created to hold each element's value at `[5]`, using the `length` field that was validated in the first loop.
- At `[6]`, each element's `cookie` is written into the `cookies` array allocated at `[3]`.

So what's the issue? This function behaves entirely correctly when `elementData` is non-volatile, the issue comes when the method is called with shared memory. Enter the `IOHIDInterface::SetElementValues_Impl` DriverKit method:

```cpp
kern_return_t
IMPL(IOHIDInterface, SetElementValues)
{
    IOReturn ret = kIOReturnError;
    UInt8 *values = NULL;
    IOBufferMemoryDescriptor *md = NULL;
    
    md = OSDynamicCast(IOBufferMemoryDescriptor, elementValues);
    require_action(md && count, exit, ret = kIOReturnBadArgument);

    values = (UInt8 *)md->getBytesNoCopy();
    
    // Post the data to the device
    ret = _owner->postElementTransaction(values, (UInt32)md->getLength());
    require_noerr_action(ret, exit, HIDServiceLogError("postElementValues failed: 0x%x", ret));

exit:
    return ret;
}
```

Here, `postElementTransaction` is called with `md->getBytesNoCopy()`, memory shared with userspace, violating the assumption that `elementData` is non-volatile. The content of the `elementData` buffer can change after the loop at `[1]`, but before the loop at `[4]`, so what does this mean for an attacker?

There are two ways an attacker can abuse this:
- The first is to swap the `length` of a small `IOHIDElementValueHeader` at the end of the buffer to a much larger value. This means that when the `OSData` at `[5]` is created, it will extend far outside the bounds of the `elementData` buffer, allowing an attacker to read out-of-bounds data using `IOHIDInterface::GetElementValues_Impl`.
- The second is to swap the `length` of a large `IOHIDElementValueHeader` at the beginning of the buffer to a much smaller value. This will make the loop at `[4]` parse many more headers than were originally counted in the loop at `[1]`, so when cookies are written to the `cookies` array at `[6]`, they will overflow out of the array as `index` is never validated against `cookieCount`.

In practice, this allows an attacker to read out-of-bounds kernel heap data of an arbitrary size, and to write arbitrary data (again of an arbitrary size) out-of-bounds to the kernel heap. These are two powerful primitives.

## Winning the race
With race conditions, we always look for ways to determine whether the race was won successfully, that way we can keep trying until we succeed, making our trigger deterministic. Luckily in the case of this race condition, we can do exactly that.

For the OOB read variant, I place one more `IOHIDElementValueHeader` after the header that I'm switching the `length` of, with its `value` set to the recognisable constant of `0xD1AB011CAC1DF00D`. Then, when reading the value of the element back, I know I have won the race if I see the familiar `0xD1AB011CAC1DF00D` header at the start of the returned data.

For the OOB write variant, I place one `IOHIDElementValueHeader` after the header that I'm switching the `length` of, but before the headers whose `cookie`s will be overflowed, this time with the recognisable `value` of `0xD15EA5ED`. This header will be encapsulated inside the `value` of the larger element in the case where we do not win the race, so the header will only be parsed and the element's value be set to `0xD15EA5ED` if we win the race. By reading back the element's value, I know whether I was successful.

## Apple's fix
To fix the issue, Apple chose to add a third loop in between loop `[1]` and loop `[4]`, validating each `length` field, and then caching it in a new `dataLengths` array, while ensuring the number of elements had not changed. The final loop then uses the cached lengths for its calculations, avoiding reading from the buffer another time.

## Issues with exploitation
The main obstacle to overcome when exploiting this issue is that the buffer we are overflowing out of belongs to `KHEAP_DATA_BUFFERS`, so exploitation targets are limited. In this proof-of-concept I chose to target kmsg headers, as these are one of very few structures in `KHEAP_DATA_BUFFERS` that contain kernel pointers. The "arbitrary kfree" primitive I obtained using this approach is the same primitive used in the [multicast_bytecopy](https://github.com/potmdehex/multicast_bytecopy) exploit, however the `IOSurfaceClient` array is now PAC'd and forged clients need to have a valid pointer back to the `IOSurfaceRootUserClient` that created them, rendering this no longer a desirable kernel r/w target.

## Building and installing
Apple have not made building and installing custom DriverKit extensions very easy, especially without a paid Apple Developer account, but it is possible. 

Before you start, I recommend:
- Disabling SIP
- Setting the boot-args `amfi_get_out_of_my_way=1 cs_enforcement_disable=1`
- Running `systemextensionsctl developer on`
- Restarting your computer

After that, you should be able to select your developer team in the Xcode project settings and the project will build successfully. You can the run HIDDriverLoader, use "Install Dext" to install the DriverKit extension and "Trigger Exploit" to, you guessed it, trigger the exploit.

If this fails, you can also try building without signing using:

    xcodebuild build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO

And then manually signing using:

    codesign -fs self-sign-cert --entitlements HIDDriverLoader/HIDDriverLoader.entitlements build/Release/HIDDriverLoader.app/Contents/MacOS/HIDDriverLoader
    codesign -fs self-sign-cert --entitlements HIDDriver/HIDDriver.entitlements build/Release/HIDDriverLoader.app/Contents/Library/SystemExtensions/*.dext/*.driver

Replacing `self-sign-cert` with the name of a [self-signed certificate](https://support.apple.com/en-gb/guide/keychain-access/kyca8916/mac) in your keychain.

### Thank you for reading :)
