
Proof-of-Concept für die CVE-2022-42864 IOHIDFamily Race-Condition
Dies ist mein (unvollständiger) Proof-of-Concept-Exploit für CVE-2022-42864, eine Time-of-Check-Time-of-Use-Schwachstelle in IOHIDFamily, die mit iOS 16.2 / macOS Ventura 13.1 behoben wurde.
Der Exploit erreicht derzeit die gleiche „arbitrary kfree“-Primitive, die auch im multicast_bytecopy-Exploit verwendet wird. Allerdings wurde der darauffolgende Exploit-Ablauf von multicast_bytecopy stark abgesichert, sodass es sich hierbei nicht um einen vollständigen Exploit handelt, sondern lediglich um eine Demonstration des Schweregrads des Problems.
Wenn Sie fragen müssen, nein. Dies tut nichts Nützliches, es führt lediglich zu einer Kernel-Panik. Ich übernehme keine Verantwortung für Datenverlust oder Instabilität, die durch diesen Code verursacht werden könnte.
Apple's Kommentar aus dem Quellcode, als dieses Problem behoben wurde, fasst es gut zusammen:
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
Schauen wir uns die Funktion vor dem Patch an (ich habe versucht, relevante Zeilen zu kennzeichnen):
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] zählt die Anzahl der IOHIDElementValues im Puffer und speichert diese Anzahl in cookieCount.[2] (zusammen mit der Bedingung der while-Schleife) stellt sicher, dass das length-Feld jedes Headers nicht über die Grenzen des elementData-Puffers hinausreicht (noch darf es vor dem Ende des Puffers enden, obwohl dies weniger relevant ist).[3] ein cookies-Puffer auf dem Heap mit einer Größe von cookieCount * 4 allokiert (oder ein Stack-Puffer, falls cookieCount klein genug ist).[4] durchläuft den Puffer dann ein zweites Mal und parst die IOHIDElementValues erneut.[5] werden OSData-Objekte erstellt, die den Wert jedes Elements halten, unter Verwendung des length-Feldes, das in der ersten Schleife validiert wurde.[6] wird das cookie jedes Elements in das bei [3] allokierte cookies-Array geschrieben.Was ist also das Problem? Diese Funktion verhält sich vollkommen korrekt, wenn elementData nicht flüchtig ist. Das Problem tritt auf, wenn die Methode mit gemeinsam genutztem Speicher aufgerufen wird. Betreten Sie die IOHIDInterface::SetElementValues_Impl DriverKit-Methode:
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;
}
Hier wird postElementTransaction mit md->getBytesNoCopy() aufgerufen, einem mit dem Userspace geteilten Speicher. Dies verletzt die Annahme, dass elementData nicht flüchtig ist. Der Inhalt des elementData-Puffers kann sich nach der Schleife bei [1], aber vor der Schleife bei [4] ändern. Was bedeutet das für einen Angreifer?
Ein Angreifer kann dies auf zwei Arten ausnutzen:
length eines kleinen IOHIDElementValueHeader am Ende des Puffers auf einen viel größeren Wert zu ändern. Wenn dann das OSData bei [5] erstellt wird, reicht es weit über die Grenzen des elementData-Puffers hinaus, sodass ein Angreifer mit IOHIDInterface::GetElementValues_Impl Daten außerhalb der Grenzen lesen kann.length eines großen IOHIDElementValueHeader am Anfang des Puffers auf einen viel kleineren Wert zu ändern. Dadurch werden in der Schleife bei [4] viele weitere Header geparst, als ursprünglich in der Schleife bei [1] gezählt wurden. Wenn dann die Cookies in das cookies-Array bei [6] geschrieben werden, kommt es zu einem Überlauf aus dem Array heraus, da index nie gegen cookieCount validiert wird.