
Preuve de concept pour la condition de concurrence IOHIDFamily CVE-2022-42864
Ceci est mon exploit proof-of-concept (incomplet) pour CVE-2022-42864, une vulnérabilité de type temps de vérification/temps d'utilisation (TOCTOU) dans IOHIDFamily qui a été corrigée dans iOS 16.2 / macOS Ventura 13.1.
L'exploit atteint actuellement la même primitive « arbitrary kfree » (libération arbitraire) que celle utilisée dans l'exploit multicast_bytecopy. Cependant, le déroulement ultérieur de l'exploit de multicast_bytecopy a été fortement atténué, donc il ne s'agit pas d'un exploit complet, mais simplement d'une démonstration de la gravité du problème.
Si vous devez poser la question, non. Cela ne fait rien d'utile, cela provoque simplement un kernel panic. Je ne peux être tenu responsable d'aucune perte de données ni instabilité que ce code pourrait causer.
Le commentaire d'Apple dans le code source lors de la correction de ce problème résume bien la situation :
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
Examinons la fonction avant le patch (j'ai essayé d'annoter les lignes pertinentes) :
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] compte le nombre de IOHIDElementValues dans le tampon et stocke ce compte dans cookieCount.[2] (combinée à la condition de la boucle while) garantit que le champ length de chaque en-tête ne dépasse pas les limites du tampon elementData (et ne s'arrête pas non plus avant la fin du tampon, bien que cela soit moins pertinent).cookies est alloué sur le tas en [3] avec une taille de cookieCount * 4 (ou un tampon sur la pile est utilisé si cookieCount est suffisamment petit).[4] effectue ensuite un deuxième passage dans le tampon, analysant à nouveau les IOHIDElementValues.OSData sont créés pour contenir la valeur de chaque élément en [5], utilisant le champ length validé dans la première boucle.[6], le cookie de chaque élément est écrit dans le tableau cookies alloué en [3].Quel est donc le problème ? Cette fonction se comporte parfaitement correctement lorsque elementData n'est pas volatil ; le problème survient lorsque la méthode est appelée avec de la mémoire partagée. Voici la méthode DriverKit IOHIDInterface::SetElementValues_Impl :
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;
}
Ici, postElementTransaction est appelée avec md->getBytesNoCopy(), une mémoire partagée avec l'espace utilisateur, ce qui viole l'hypothèse selon laquelle elementData n'est pas volatil. Le contenu du tampon elementData peut changer après la boucle [1], mais avant la boucle [4]. Qu'est-ce que cela signifie pour un attaquant ?
Un attaquant peut abuser de cela de deux manières :
length d'un petit IOHIDElementValueHeader à la fin du tampon par une valeur beaucoup plus grande. Cela signifie que lorsque OSData est créé en [5], il s'étendra bien au-delà des limites du tampon elementData, permettant à un attaquant de lire des données hors limites à l'aide de IOHIDInterface::GetElementValues_Impl.length d'un grand IOHIDElementValueHeader au début du tampon par une valeur beaucoup plus petite. Cela fera analyser par la boucle [4] beaucoup plus d'en-têtes que ceux comptés initialement dans la boucle [1], de sorte que lorsque les cookies sont écrits dans le tableau cookies en [6], ils déborderont du tableau car index n'est jamais validé par rapport à cookieCount.