
CVE-2022-42864 IOHIDFamily 레이스 컨디션에 대한 개념 증명
이것은 CVE-2022-42864에 대한 (불완전한) 개념 증명 익스플로잇입니다. 이 취약점은 iOS 16.2 / macOS Ventura 13.1에서 수정된 IOHIDFamily의 검사 시점-사용 시점(time-of-check-time-of-use) 취약점입니다.
현재 이 익스플로잇은 multicast_bytecopy 익스플로잇에서 사용된 것과 동일한 "임의 kfree" 프리미티브를 달성합니다. 그러나 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]에서 cookieCount * 4 크기의 cookies 버퍼가 힙에 할당됩니다(또는 cookieCount가 충분히 작으면 스택 버퍼가 사용됨).[4]의 두 번째 루프는 버퍼를 두 번째로 통과하며 IOHIDElementValue를 다시 파싱합니다.[5]에서 각 요소의 값을 저장하기 위해 OSData 객체가 생성되며, 첫 번째 루프에서 검증된 length 필드를 사용합니다.[6]에서 각 요소의 cookie가 [3]에서 할당된 cookies 배열에 기록됩니다.문제는 무엇일까요? 이 함수는 elementData가 비휘발성일 때 완전히 올바르게 동작합니다. 문제는 이 메서드가 공유 메모리로 호출될 때 발생합니다. IOHIDInterface::SetElementValues_Impl 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;
}
여기서 postElementTransaction은 md->getBytesNoCopy()와 함께 호출되는데, 이 메모리는 사용자 공간과 공유되므로 elementData가 비휘발성이라는 가정을 위반합니다. elementData 버퍼의 내용은 [1] 루프 이후 [4] 루프 이전에 변경될 수 있습니다. 이것이 공격자에게 어떤 의미일까요?
공격자가 남용할 수 있는 두 가지 방법이 있습니다:
IOHIDElementValueHeader의 length를 훨씬 큰 값으로 바꾸는 것입니다. 이렇게 하면 [5]에서 OSData가 생성될 때 elementData 버퍼의 경계를 훨씬 넘어 확장되어, 공격자가 IOHIDInterface::GetElementValues_Impl을 사용하여 경계 밖 데이터를 읽을 수 있습니다.IOHIDElementValueHeader의 length를 훨씬 작은 값으로 바꾸는 것입니다. 이렇게 하면 [4] 루프가 [1] 루프에서 원래 계산된 것보다 더 많은 헤더를 파싱하게 되므로, [6]에서 cookies 배열에 쿠키가 기록될 때 index가 cookieCount에 대해 검증되지 않아 배열을 오버플로우하게 됩니다.실제로 이를 통해 공격자는 임의 크기의 커널 힙 데이터를 경계 밖에서 읽고, 임의의 데이터(역시 임의 크기)를 커널 힙에 경계 밖으로 쓸 수 있습니다. 이는 두 가지 강력한 프리미티브입니다.
레이스 조건에서는 항상 레이스를 성공적으로 이겼는지 확인할 방법을 찾습니다. 그래야 성공할 때까지 계속 시도하여 트리거를 결정적으로 만들 수 있습니다. 다행히 이 레이스 조건의 경우 정확히 그렇게 할 수 있습니다.
OOB 읽기 변형에서는 length를 바꾸려는 헤더 뒤에 하나 이상의 IOHIDElementValueHeader를 배치하고, 그 value를 인식 가능한 상수 0xD1AB011CAC1DF00D로 설정합니다. 그런 다음 요소의 값을 다시 읽을 때 반환된 데이터의 시작 부분에서 익숙한 0xD1AB011CAC1DF00D 헤더가 보이면 레이스를 이긴 것입니다.
OOB 쓰기 변형에서는 length를 바꾸려는 헤더 뒤에, cookie가 오버플로우될 헤더들 앞에 하나의 IOHIDElementValueHeader를 배치하고, 이번에는 인식 가능한 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 개발자 계정이 없으면 더 어렵습니다. 하지만 가능합니다.
시작하기 전에 다음을 권장합니다:
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를 키체인의 자체 서명 인증서 이름으로 바꾸세요.