
PoC-эксплойт для состояния гонки CVE-2022-42864 в IOHIDFamily
Это мой (незавершённый) концепт-эксплойт для CVE-2022-42864, уязвимости типа «проверка-в-момент-использования» (TOCTOU) в IOHIDFamily, исправленной в iOS 16.2 / macOS Ventura 13.1.
Эксплойт в настоящее время достигает того же примитива «произвольный kfree», который используется в эксплойте multicast_bytecopy. Однако последующий эксплойт-флоу multicast_bytecopy был значительно затруднён мерами защиты, так что это не полный эксплойт, а лишь демонстрация серьёзности проблемы.
Если вам приходится спрашивать — нет. Этот код не делает ничего полезного, он просто вызывает панику ядра. Я не несу ответственности за потерю данных или нестабильность, которые может вызвать этот код.
Комментарий Apple из исходного кода, когда эта проблема была исправлена, хорошо подводит итог:
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
Давайте посмотрим на функцию до патча (я попытался пометить соответствующие строки):
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] подсчитывает количество IOHIDElementValue в буфере и сохраняет это значение в cookieCount.[2] (в сочетании с условием цикла while) гарантирует, что поле length каждого заголовка не выходит за границы буфера elementData (равно как и не может не доставать до конца буфера, хотя это менее важно).[3] в куче выделяется буфер cookies размером cookieCount * 4 (или используется стековый буфер, если cookieCount достаточно мал).[4] проходит по буферу повторно, снова разбирая IOHIDElementValue.OSData создаются для хранения значения каждого элемента на , используя поле , проверенное в первом цикле.Так в чём же проблема? Эта функция ведёт себя полностью корректно, когда elementData не является изменяемым; проблема возникает, когда метод вызывается с разделяемой памятью. Переходим к методу 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;
}
Здесь postElementTransaction вызывается с md->getBytesNoCopy() — памятью, разделяемой с пользовательским пространством, что нарушает предположение о неизменяемости elementData. Содержимое буфера elementData может измениться после цикла на [1], но до цикла на [4]. Что это значит для атакующего?
У атакующего есть два способа воспользоваться этим:
length небольшого IOHIDElementValueHeader в конце буфера на гораздо большее значение. Это означает, что при создании OSData на [5] он выйдет далеко за пределы буфера elementData, что позволит атакующему читать данные за границами буфера с помощью IOHIDInterface::GetElementValues_Impl.length большого IOHIDElementValueHeader в начале буфера на гораздо меньшее значение. Это заставит цикл на [4] разобрать намного больше заголовков, чем было подсчитано в цикле на [1], поэтому при записи cookies в массив cookies на [6] произойдёт переполнение массива, поскольку index никогда не проверяется на соответствие .На практике это позволяет атакующему читать данные кучи ядра за пределами границ произвольного размера, а также записывать произвольные данные (опять же произвольного размера) за пределы границ в кучу ядра. Это два мощных примитива.
В условиях состояния гонки мы всегда ищем способы определить, была ли гонка выиграна успешно, чтобы можно было продолжать попытки до успеха, делая наш триггер детерминированным. К счастью, в случае с этим состоянием гонки мы можем сделать именно это.
Для варианта чтения за пределами границ (OOB read) я помещаю ещё один IOHIDElementValueHeader после заголовка, у которого меняю length, с value, установленным в узнаваемую константу 0xD1AB011CAC1DF00D. Затем, читая значение элемента обратно, я понимаю, что выиграл гонку, если вижу знакомый заголовок 0xD1AB011CAC1DF00D в начале возвращённых данных.
Для варианта записи за пределами границ (OOB write) я помещаю один IOHIDElementValueHeader после заголовка, у которого меняю length, но перед заголовками, чьи cookie будут переполнены, на этот раз с узнаваемым value 0xD15EA5ED. Этот заголовок будет инкапсулирован внутри value более крупного элемента в случае, если мы не выиграем гонку, поэтому заголовок будет разобран и значение элемента будет установлено в 0xD15EA5ED, только если мы выиграем гонку. Считывая значение элемента, я узнаю, был ли я успешен.
Чтобы исправить проблему, Apple решила добавить третий цикл между циклом [1] и циклом [4], проверяющий каждое поле length и затем кэширующий его в новом массиве dataLengths, одновременно гарантируя, что количество элементов не изменилось. Затем финальный цикл использует закэшированные длины для своих вычислений, избегая повторного чтения из буфера.
Основное препятствие, которое нужно преодолеть при эксплуатации этой уязвимости, заключается в том, что буфер, из которого мы переполняемся, принадлежит зоне KHEAP_DATA_BUFFERS, поэтому цели для эксплуатации ограничены. В этом концепт-эксплойте я решил нацелиться на заголовки kmsg, так как это одна из немногих структур в KHEAP_DATA_BUFFERS, содержащих указатели ядра. Примитив «произвольный kfree», полученный таким подходом, — это тот же примитив, который используется в эксплойте multicast_bytecopy, однако массив IOSurfaceClient теперь защищён PAC, и поддельные клиенты должны иметь корректный указатель обратно на IOSurfaceRootUserClient, который их создал, из-за чего это больше не является желаемой целью для чтения/записи в ядре.
Apple не сделали сборку и установку пользовательских расширений DriverKit очень простой, особенно без платной учётной записи Apple Developer, но это возможно.
Перед началом я рекомендую:
amfi_get_out_of_my_way=1 cs_enforcement_disable=1systemextensionsctl developer onПосле этого вы сможете выбрать свою команду разработчика в настройках проекта Xcode, и проект успешно соберётся. Затем вы можете запустить HIDDriverLoader, использовать "Install Dext" для установки расширения DriverKit и "Trigger Exploit", чтобы — как вы догадались — запустить эксплойт.
Если это не сработает, вы также можете попробовать собрать без подписи, используя:
xcodebuild build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
А затем подписать вручную с помощью:
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
Замените self-sign-cert на имя самоподписанного сертификата в вашей связке ключей.
[5]length[6] cookie каждого элемента записывается в массив cookies, выделенный на [3].cookieCount