
Proof-of-concept for the CVE-2022-42864 IOHIDFamily race condition
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.
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.
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.
Apple's comment from the source code when this issue was fixed sums this up nicely:
// 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):
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;
}
[1] counts the number of IOHIDElementValues in the buffer, and stores this count in cookieCount.[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).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).[4] then makes a second pass through the buffer, parsing the IOHIDElementValues again.OSData objects are created to hold each element's value at [5], using the length field that was validated in the first loop.[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:
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:
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.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.
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.