这是我为 CVE-2022-42864 编写的(不完整的)概念验证漏洞利用代码,这是 IOHIDFamily 中的一个时间检查-时间使用(TOCTOU)漏洞,已在 iOS 16.2 / macOS Ventura 13.1 中修复。
该漏洞利用目前实现了与 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 循环的条件)将确保每个 header 的 length 字段不会超出 elementData 缓冲区的边界(也不能少于缓冲区的末尾,尽管这不太相关)。[3] 处向堆分配一个 cookies 缓冲区,大小为 cookieCount * 4(如果 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] 处循环最初计数的更多 header,因此当 cookies 被写入 [6] 处的 cookies 数组时,由于 index 从未与 cookieCount 进行校验,它们将溢出数组。实际上,这允许攻击者读取任意大小的越界内核堆数据,并将任意数据(同样是任意大小)越界写入内核堆。这是两个强大的原语。
对于竞态条件,我们总是寻找方法来确定竞争是否成功,这样我们就可以不断尝试直到成功,使我们的触发变得确定性。幸运的是,在这种竞态条件下,我们完全可以做到这一点。
对于越界读取变体,我在正在切换 length 的 header 之后再放置一个 IOHIDElementValueHeader,将其 value 设置为可识别的常量 0xD1AB011CAC1DF00D。然后,当回读元素的值时,如果在返回数据的开头看到熟悉的 0xD1AB011CAC1DF00D header,我就知道赢得了竞争。
对于越界写入变体,我在正在切换 length 的 header 之后、其 cookie 将被溢出的那些 header 之前放置一个 IOHIDElementValueHeader,这次的可识别 value 为 0xD15EA5ED。在我们没有赢得竞争的情况下,这个 header 将被封装在较大元素的 value 内部,因此只有在我们赢得竞争时,该 header 才会被解析并且元素的值才会被设置为 0xD15EA5ED。通过回读元素的值,我就知道是否成功。
为了修复该问题,Apple 选择在循环 [1] 和循环 [4] 之间增加第三个循环,验证每个 length 字段,然后将其缓存到一个新的 dataLengths 数组中,同时确保元素数量没有改变。最后的循环使用缓存的长度进行计算,避免再次从缓冲区读取。
利用此问题时需要克服的主要障碍是,我们溢出的缓冲区属于 KHEAP_DATA_BUFFERS,因此可利用的目标有限。在这个概念验证中,我选择以 kmsg headers 为目标,因为它们是 KHEAP_DATA_BUFFERS 中极少数包含内核指针的结构之一。我通过这种方法获得的“任意 kfree”原语与 multicast_bytecopy 漏洞利用中使用的原语相同,不过 IOSurfaceClient 数组现在已被 PAC 保护,且伪造的 client 需要有一个指向创建它们的 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 替换为你钥匙串中自签名证书的名称。