
إثبات المفهوم لحالة السباق CVE-2022-42864 في IOHIDFamily
هذا هو استغلال إثبات المفهوم (غير المكتمل) الخاص بي لثغرة CVE-2022-42864، وهي ثغرة من نوع زمن الفحص-زمن الاستخدام (TOCTOU) في IOHIDFamily تم إصلاحها في iOS 16.2 / macOS Ventura 13.1.
يحقق الاستغلال حاليًا نفس أولية "التحرير التعسفي (arbitrary kfree)" المستخدمة في استغلال multicast_bytecopy. ومع ذلك، فقد تم تحصين مسار الاستغلال اللاحق الخاص بـ multicast_bytecopy بشدة، لذا فهذا ليس استغلالًا كاملًا، بل يوضح فقط مدى خطورة المشكلة.
إذا كنت بحاجة إلى السؤال، فالإجابة لا. هذا لا يفعل أي شيء مفيد، بل يسبب فقط انهيار النواة (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] تعدّ عدد IOHIDElementValues في المخزن المؤقت، وتخزّن هذا العدد في cookieCount.[2] (بالاشتراك مع شرط حلقة while) يضمن أن حقل length لكل ترويسة لا يمتد خارج حدود مخزن elementData المؤقت (كما لا يمكن أن يقصر عن نهاية المخزن، على الرغم من أن هذا أقل أهمية).cookies مؤقت في الكومة عند [3] بحجم cookieCount * 4 (أو يُستخدم مخزن مكدس إذا كان cookieCount صغيرًا بما يكفي).[4] بتمريرة ثانية عبر المخزن، لتحليل IOHIDElementValues مرة أخرى.OSData لاحتواء قيمة كل عنصر عند [5]، باستخدام حقل length الذي تم التحقق منه في الحلقة الأولى.[6]، تتم كتابة cookie لكل عنصر في مصفوفة cookies المخصصة عند [3].إذن ما هي المشكلة؟ تتصرف هذه الدالة بشكل صحيح تمامًا عندما تكون 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 عند [6]، سوف تفيض خارج المصفوفة لأن index لا يتم التحقق منه أبدًا مقابل cookieCount.عمليًا، يسمح هذا للمهاجم بقراءة بيانات كومة النواة خارج الحدود بحجم تعسفي، وكتابة بيانات تعسفية (مرة أخرى بحجم تعسفي) خارج الحدود إلى كومة النواة. هاتان أوليتان قويتان.
مع حالات التسابق، نبحث دائمًا عن طرق لتحديد ما إذا تم الفوز بالتسابق بنجاح، وبهذه الطريقة يمكننا مواصلة المحاولة حتى ننجح، مما يجعل مشغل الهجوم لدينا حتميًا. لحسن الحظ، في حالة هذا التسابق تحديدًا، يمكننا فعل ذلك بالضبط.
بالنسبة لنسخة القراءة خارج الحدود (OOB read)، أضع ترويسة IOHIDElementValueHeader إضافية بعد الترويسة التي أقوم بتبديل length الخاصة بها، مع ضبط value على الثابت المعروف 0xD1AB011CAC1DF00D. بعد ذلك، عند قراءة قيمة العنصر مرة أخرى، أعلم أنني فزت بالتسابق إذا رأيت الترويسة المألوفة 0xD1AB011CAC1DF00D في بداية البيانات المعادة.