
Prueba de concepto para el CVE-2022-42864 de condición de carrera en IOHIDFamily
Esta es mi prueba de concepto (incompleta) del exploit para CVE-2022-42864, una vulnerabilidad de tiempo-de-verificación a tiempo-de-uso en IOHIDFamily que fue corregida en iOS 16.2 / macOS Ventura 13.1.
El exploit actualmente logra la misma primitiva "kfree arbitrario" utilizada en el exploit multicast_bytecopy. Sin embargo, el flujo posterior de exploit de multicast_bytecopy ha sido fuertemente mitigado, por lo que esto no es un exploit completo, solo demuestra la severidad del problema.
Si tienes que preguntarlo, no. Esto no hace nada útil, solo provoca un pánico del kernel. No me hago responsable por cualquier pérdida de datos o inestabilidad que este código pueda causar.
El comentario de Apple del código fuente cuando se corrigió este problema lo resume muy bien:
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
Echemos un vistazo a la función antes del parche (he intentado etiquetar las líneas relevantes):
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] cuenta el número de IOHIDElementValues en el buffer y almacena esta cuenta en cookieCount.[2] (combinada con la condición del bucle while) se asegura de que el campo length de cada cabecera no se extienda más allá de los límites del buffer elementData (ni que quede corto respecto al final del buffer, aunque esto es menos relevante).cookies al heap en [3] con un tamaño de cookieCount * 4 (o se usa un buffer de pila si cookieCount es suficientemente pequeño).[4] realiza una segunda pasada por el buffer, analizando nuevamente los IOHIDElementValues.OSData para almacenar el valor de cada elemento en [5], utilizando el campo length que fue validado en el primer bucle.[6], el cookie de cada elemento se escribe en el array cookies asignado en [3].Entonces, ¿cuál es el problema? Esta función se comporta correctamente cuando elementData no es volátil, el problema surge cuando el método se llama con memoria compartida. Aquí entra el método IOHIDInterface::SetElementValues_Impl de 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;
}
Aquí, postElementTransaction se llama con md->getBytesNoCopy(), memoria compartida con el espacio de usuario, violando la suposición de que elementData es no volátil. El contenido del buffer elementData puede cambiar después del bucle en [1], pero antes del bucle en [4], entonces, ¿qué significa esto para un atacante?
Hay dos formas en que un atacante puede abusar de esto:
length de un IOHIDElementValueHeader pequeño al final del buffer por un valor mucho más grande. Esto significa que cuando se crea el OSData en [5], se extenderá mucho más allá de los límites del buffer elementData, permitiendo a un atacante leer datos fuera de los límites usando IOHIDInterface::GetElementValues_Impl.length de un IOHIDElementValueHeader grande al principio del buffer por un valor mucho más pequeño. Esto hará que el bucle en [4] analice muchos más encabezados de los que fueron contados originalmente en el bucle en [1], por lo que cuando se escriban las cookies en el array cookies en [6], desbordarán el array ya que index nunca se valida contra cookieCount.En la práctica, esto permite a un atacante leer datos del heap del kernel fuera de los límites de un tamaño arbitrario, y escribir datos arbitrarios (también de tamaño arbitrario) fuera de los límites en el heap del kernel. Estas son dos primitivas poderosas.