
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.
Con le race condition, cerchiamo sempre modi per determinare se la race è stata vinta con successo, in modo da poter continuare a provare fino a riuscirci, rendendo il nostro trigger deterministico. Fortunatamente, nel caso di questa race condition, possiamo farlo esattamente.
Per la variante di lettura OOB, posiziono un altro IOHIDElementValueHeader dopo l'intestazione di cui sto cambiando la length, con il suo value impostato sulla costante riconoscibile 0xD1AB011CAC1DF00D. Quindi, quando leggo il valore dell'elemento, so di aver vinto la race se vedo l'intestazione familiare 0xD1AB011CAC1DF00D all'inizio dei dati restituiti.
Per la variante di scrittura OOB, posiziono un IOHIDElementValueHeader dopo l'intestazione di cui sto cambiando la length, ma prima delle intestazioni i cui cookie saranno sovrascritti, questa volta con il value riconoscibile 0xD15EA5ED. Questa intestazione sarà incapsulata all'interno del value dell'elemento più grande nel caso in cui non vinciamo la race, quindi l'intestazione verrà analizzata e il valore dell'elemento verrà impostato su 0xD15EA5ED solo se vinciamo la race. Leggendo il valore dell'elemento, so se ho avuto successo.
Per risolvere il problema, Apple ha scelto di aggiungere un terzo ciclo tra il ciclo [1] e il ciclo [4], validando ogni campo length e poi memorizzandolo in un nuovo array dataLengths, assicurando al contempo che il numero di elementi non fosse cambiato. Il ciclo finale utilizza quindi le lunghezze memorizzate nella cache per i suoi calcoli, evitando di leggere dal buffer un'altra volta.
L'ostacolo principale da superare nello sfruttamento di questo problema è che il buffer da cui stiamo traboccando appartiene a KHEAP_DATA_BUFFERS, quindi gli obiettivi di sfruttamento sono limitati. In questa proof-of-concept ho scelto di prendere di mira le intestazioni kmsg, poiché sono una delle poche strutture in KHEAP_DATA_BUFFERS che contengono puntatori al kernel. La primitiva "arbitrary kfree" che ho ottenuto usando questo approccio è la stessa primitiva usata nell'exploit multicast_bytecopy, tuttavia l'array IOSurfaceClient è ora protetto da PAC e i client contraffatti devono avere un puntatore valido all'IOSurfaceRootUserClient che li ha creati, rendendo questo non più un bersaglio desiderabile per lettura/scrittura del kernel.
Apple non ha reso molto semplice la compilazione e l'installazione di estensioni DriverKit personalizzate, specialmente senza un account sviluppatore Apple a pagamento, ma è possibile.
Prima di iniziare, raccomando:
amfi_get_out_of_my_way=1 cs_enforcement_disable=1systemextensionsctl developer onDopo di che, dovresti essere in grado di selezionare il tuo team di sviluppo nelle impostazioni del progetto Xcode e il progetto verrà compilato con successo. Puoi quindi eseguire HIDDriverLoader, usare "Install Dext" per installare l'estensione DriverKit e "Trigger Exploit" per, hai indovinato, attivare l'exploit.
Se questo fallisce, puoi anche provare a compilare senza firmare usando:
xcodebuild build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
E poi firmare manualmente usando:
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
Sostituendo self-sign-cert con il nome di un certificato auto-firmato nel tuo portachiavi.