
CVE-2022-42864 IOHIDFamily रेस कंडीशन के लिए प्रूफ-ऑफ-कॉन्सेप्ट
यह CVE-2022-42864 के लिए मेरा (अधूरा) proof-of-concept एक्सप्लॉइट है, जो IOHIDFamily में एक time-of-check-time-of-use (TOCTOU) भेद्यता है, जिसे iOS 16.2 / macOS Ventura 13.1 में ठीक किया गया था।
यह एक्सप्लॉइट वर्तमान में वही "arbitrary kfree" प्रिमिटिव प्राप्त करता है जो multicast_bytecopy एक्सप्लॉइट में उपयोग किया गया था। हालाँकि, multicast_bytecopy के आगे के एक्सप्लॉइट प्रवाह के विरुद्ध भारी शमन (mitigation) किया जा चुका है, इसलिए यह एक पूर्ण एक्सप्लॉइट नहीं है, यह केवल समस्या की गंभीरता को प्रदर्शित करता है।
अगर आपको पूछना पड़ रहा है, तो नहीं। यह कुछ उपयोगी नहीं करता, यह केवल kernel panic उत्पन्न करता है। इस कोड के कारण होने वाली किसी भी डेटा हानि या अस्थिरता की जिम्मेदारी मैं नहीं लेता।
जब यह समस्या ठीक की गई थी, तब स्रोत कोड में 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 आकार के साथ हीप (heap) में एक cookies बफर आवंटित किया जाता है (या यदि cookieCount पर्याप्त छोटा है तो एक स्टैक बफर उपयोग किया जाता है)।[4] पर एक दूसरा लूप बफर के माध्यम से दूसरी बार गुजरता है, IOHIDElementValue को फिर से पार्स करता है।[5] पर प्रत्येक तत्व का मान रखने के लिए OSData ऑब्जेक्ट बनाए जाते हैं, जो पहले लूप में मान्य किए गए length फ़ील्ड का उपयोग करते हैं।[6] पर, प्रत्येक तत्व का cookie [3] पर आवंटित cookies सरणी में लिखा जाता है।तो समस्या क्या है? यह फ़ंक्शन पूरी तरह से सही व्यवहार करता है जब elementData गैर-परिवर्तनशील (non-volatile) होता है, समस्या तब आती है जब विधि को साझा मेमोरी के साथ बुलाया जाता है। अब आती है 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() के साथ बुलाया जाता है, जो userspace के साथ साझा की गई मेमोरी है, जो इस धारणा का उल्लंघन करती है कि elementData गैर-परिवर्तनशील है। elementData बफर की सामग्री [1] पर लूप के बाद, लेकिन [4] पर लूप से पहले बदल सकती है, तो एक हमलावर के लिए इसका क्या अर्थ है?
एक हमलावर इसका दुरुपयोग दो तरीकों से कर सकता है:
IOHIDElementValueHeader के length को बहुत बड़े मान में बदलना। इसका मतलब है कि जब [5] पर OSData बनाया जाता है, तो यह elementData बफर की सीमा से कहीं बाहर तक फैल जाएगा, जिससे एक हमलावर IOHIDInterface::GetElementValues_Impl का उपयोग करके out-of-bounds डेटा पढ़ सकता है।IOHIDElementValueHeader के length को बहुत छोटे मान में बदलना। यह [4] पर लूप को मूल रूप से [1] पर लूप में गिने गए हेडरों की तुलना में कई अधिक हेडर पार्स करने के लिए बना देगा, इसलिए जब [6] पर cookies सरणी में कुकीज़ लिखी जाती हैं, तो वे सरणी से बाहर overflow हो जाएँगी क्योंकि index को कभी भी cookieCount के विरुद्ध मान्य नहीं किया जाता।व्यवहार में, यह एक हमलावर को मनमाने आकार का out-of-bounds kernel heap डेटा पढ़ने और मनमाना डेटा (फिर से मनमाने आकार का) out-of-bounds kernel heap में लिखने की अनुमति देता है। ये दो शक्तिशाली प्रिमिटिव हैं।
रेस कंडीशन के साथ, हम हमेशा यह निर्धारित करने के तरीकों की तलाश करते हैं कि रेस सफलतापूर्वक जीती गई या नहीं, इस तरह हम तब तक प्रयास करते रह सकते हैं जब तक हम सफल नहीं हो जाते, जिससे हमारा ट्रिगर नियतात्मक (deterministic) बन जाता है। सौभाग्य से इस रेस कंडीशन के मामले में, हम ठीक ऐसा ही कर सकते हैं।
OOB read वैरिएंट के लिए, मैं उस हेडर के बाद एक और IOHIDElementValueHeader रखता हूँ जिसका length मैं बदल रहा हूँ, जिसका value पहचाने जाने योग्य स्थिरांक 0xD1AB011CAC1DF00D पर सेट होता है। फिर, तत्व का मान वापस पढ़ते समय, मुझे पता चल जाता है कि मैंने रेस जीत ली है यदि मुझे लौटाए गए डेटा की शुरुआत में परिचित 0xD1AB011CAC1DF00D हेडर दिखाई देता है।
OOB write वैरिएंट के लिए, मैं उस हेडर के बाद एक IOHIDElementValueHeader रखता हूँ जिसका length मैं बदल रहा हूँ, लेकिन उन हेडरों से पहले जिनके cookie overflow होंगे, इस बार पहचाने जाने योग्य value 0xD15EA5ED के साथ। जिस स्थिति में हम रेस नहीं जीतते, यह हेडर बड़े तत्व के value के अंदर समाहित हो जाएगा, इसलिए हेडर केवल तभी पार्स होगा और तत्व का मान 0xD15EA5ED पर सेट होगा यदि हम रेस जीतते हैं। तत्व का मान वापस पढ़कर, मुझे पता चलता है कि मैं सफल रहा या नहीं।
समस्या को ठीक करने के लिए, Apple ने लूप [1] और लूप [4] के बीच एक तीसरा लूप जोड़ना चुना, जो प्रत्येक length फ़ील्ड को मान्य करता है, और फिर उसे एक नई dataLengths सरणी में कैश करता है, साथ ही यह सुनिश्चित करता है कि तत्वों की संख्या नहीं बदली है। अंतिम लूप फिर अपनी गणनाओं के लिए कैश की गई लंबाईयों का उपयोग करता है, जिससे बफर से दोबारा पढ़ने से बचा जा सके।
इस समस्या का दोहन करते समय दूर करने वाली मुख्य बाधा यह है कि जिस बफर से हम overflow कर रहे हैं वह KHEAP_DATA_BUFFERS से संबंधित है, इसलिए एक्सप्लॉइटेशन लक्ष्य सीमित हैं। इस proof-of-concept में मैंने kmsg हेडरों को लक्षित करना चुना, क्योंकि ये KHEAP_DATA_BUFFERS में उन बहुत कम संरचनाओं में से एक हैं जिनमें kernel पॉइंटर्स होते हैं। इस दृष्टिकोण का उपयोग करके मैंने जो "arbitrary kfree" प्रिमिटिव प्राप्त किया, वही प्रिमिटिव multicast_bytecopy एक्सप्लॉइट में उपयोग किया गया है, हालाँकि IOSurfaceClient सरणी अब PAC'd है और forged क्लाइंट्स के पास उन्हें बनाने वाले IOSurfaceRootUserClient के पास वापस एक मान्य पॉइंटर होना आवश्यक है, जिससे यह अब एक वांछनीय kernel r/w लक्ष्य नहीं रह गया है।
Apple ने कस्टम DriverKit एक्सटेंशन बनाना और स्थापित करना बहुत आसान नहीं बनाया है, विशेष रूप से भुगतान किए गए Apple Developer खाते के बिना, लेकिन यह संभव है।
शुरू करने से पहले, मैं अनुशंसा करता हूँ:
amfi_get_out_of_my_way=1 cs_enforcement_disable=1 सेट करनाsystemextensionsctl developer on चलानाउसके बाद, आपको Xcode प्रोजेक्ट सेटिंग्स में अपनी developer टीम का चयन करने में सक्षम होना चाहिए और प्रोजेक्ट सफलतापूर्वक बन जाएगा। फिर आप HIDDriverLoader चला सकते हैं, DriverKit एक्सटेंशन स्थापित करने के लिए "Install Dext" का उपयोग कर सकते हैं और, आपने सही अनुमान लगाया, एक्सप्लॉइट ट्रिगर करने के लिए "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 को अपने keychain में एक self-signed certificate के नाम से बदलें।