
Prova di concetto per la CVE-2022-42864 IOHIDFamily race condition
Questa è la mia (incompleta) proof-of-concept dell'exploit per CVE-2022-42864, una vulnerabilità di tipo time-of-check-time-of-use (TOCTOU) in IOHIDFamily che è stata corretta in iOS 16.2 / macOS Ventura 13.1.
L'exploit attualmente ottiene la stessa primitiva "arbitrary kfree" usata nell'exploit multicast_bytecopy. Tuttavia, il successivo flusso di exploit di multicast_bytecopy è stato pesantemente mitigato, quindi non si tratta di un exploit completo, ma dimostra semplicemente la gravità del problema.
Se devi chiedere, no. Non fa nulla di utile, provoca solo un kernel panic. Non mi assumo responsabilità per eventuali perdite di dati o instabilità causate da questo codice.
Il commento di Apple dal codice sorgente quando questo problema è stato risolto lo riassume perfettamente:
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
Diamo un'occhiata alla funzione prima della patch (ho cercato di etichettare le righe pertinenti):
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] conta il numero di IOHIDElementValue nel buffer e memorizza questo conteggio in cookieCount.[2] (combinato con la condizione del ciclo while) si assicurerà che il campo length di ogni intestazione non si estenda oltre i limiti del buffer elementData (né possa essere inferiore alla fine del buffer, anche se questo è meno rilevante).cookies viene allocato nell'heap in [3] con una dimensione di cookieCount * 4 (oppure viene usato un buffer nello stack se cookieCount è sufficientemente piccolo).[4] effettua quindi una seconda scansione del buffer, analizzando nuovamente gli IOHIDElementValue.OSData vengono creati per contenere il valore di ogni elemento in [5], utilizzando il campo length che è stato validato nel primo ciclo.[6], il cookie di ogni elemento viene scritto nell'array cookies allocato in [3].Qual è il problema? Questa funzione si comporta correttamente quando elementData è non volatile; il problema si presenta quando il metodo viene chiamato con memoria condivisa. Entra in gioco il metodo IOHIDInterface::SetElementValues_Impl di DriverKit:
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;
}
Qui, postElementTransaction viene chiamato con md->getBytesNoCopy(), memoria condivisa con lo spazio utente, violando l'assunzione che elementData sia non volatile. Il contenuto del buffer elementData può cambiare dopo il ciclo in [1], ma prima del ciclo in [4], quindi cosa significa questo per un attaccante?
Ci sono due modi in cui un attaccante può abusarne:
length di un piccolo IOHIDElementValueHeader alla fine del buffer con un valore molto più grande. Ciò significa che quando l'OSData in [5] viene creato, si estenderà ben oltre i limiti del buffer elementData, consentendo a un attaccante di leggere dati fuori dai limiti usando IOHIDInterface::GetElementValues_Impl.length di un grande IOHIDElementValueHeader all'inizio del buffer con un valore molto più piccolo. Ciò farà sì che il ciclo in [4] analizzi molte più intestazioni di quelle originariamente contate nel ciclo in [1], quindi quando i cookie vengono scritti nell'array cookies in [6], questi traboccheranno dall'array poiché index non viene mai validato rispetto a cookieCount.In pratica, ciò consente a un attaccante di leggere dati fuori dai limiti dell'heap del kernel di dimensione arbitraria e di scrivere dati arbitrari (ancora di dimensione arbitraria) fuori dai limiti nell'heap del kernel. Si tratta di due primitive potenti.