
(1) IQVW32.sys قبل الإصدار 1.3.1.0 و(2) IQVW64.sys قبل الإصدار 1.3.1.0 في برنامج تشغيل تشخيص إيثرنت إنتل لنظام ويندوز يسمح للمستخدمين المحليين بالتسبب في رفض الخدمة أو ربما تنفيذ كود تعسفي بامتيازات النواة عبر استدعاء IOCTL مخصص (a) 0x80862013، (b) 0x8086200B، (c) 0x8086200F، أو (d) 0x80862007.
(1) IQVW32.sys قبل الإصدار 1.3.1.0 و (2) IQVW64.sys قبل الإصدار 1.3.1.0 في برنامج تشغيل تشخيص إيثرنت من إنتل لنظام ويندوز يسمح للمستخدمين المحليين بالتسبب في رفض الخدمة أو ربما تنفيذ كود عشوائي بصلاحيات النواة عبر استدعاء IOCTL مُصمم (أ) 0x80862013، (ب) 0x8086200B، (ج) 0x8086200F، أو (د) 0x80862007.
يحتوي هذا المستودع على تقرير حول الثغرة المذكورة، بالإضافة إلى أدوات إثبات المفهوم التي تعمل على نظام ويندوز 7 64 بت مع حزمة الخدمة 1 ونظام ويندوز 10 20H2. يمكن العثور على ملف برنامج التشغيل في دليل ملفات برنامج التشغيل. إذا اكتشفت أي أخطاء إملائية في التقرير/الورقة، أو إذا كنت ترغب في رؤية تفاصيل معينة مع شرح أكثر تفصيلاً، يرجى إنشاء مشكلة في المستودع! سأقوم بإصلاحها في أقرب وقت ممكن.
الدافع وراء كتابة استغلال لبرنامج التشغيل هذا على وجه الخصوص هو فقط لأنه يُستخدم حالياً في البرية لتحميل روت-كيت غير موقعة من المهاجم. باستخدام طريقة BYOVD (إحضار برنامج التشغيل الضعيف الخاص بك)، يمكن للبرنامج الضار التحقق مما إذا كان يعمل بصلاحيات مرتفعة، وإسقاط نسخة من برنامج التشغيل الضعيف، وتحميل برنامج التشغيل، ثم استغلاله للحصول على تنفيذ كود النواة لتحميل الروت-كيت. لم أتمكن من هندسة عكسية ناجحة لعينة البرنامج الضار، لذا أخذت على عاتقي إنشاء الاستغلال.
عينات تم رصدها في البرية: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
برنامج التشغيل، المسمى iqvw64e.sys، هو برنامج تشغيل مصمم لإجراء تشخيصات لمحول الشبكة. يسمح للمكون في وضع المستخدم بالتفاعل مع برنامج التشغيل لتنفيذ العديد من إجراءات النواة عن طريق كشف عدد من رموز التحكم بالإدخال/الإخراج (المعروفة أيضاً باسم IOCTLs)، مع تقديم رمز تحكم IOCTL "فرعي" في مخزن الإدخال الخاص بالمستخدم أثناء التفاعل. رمز التحكم IOCTL الذي سيُستخدم للوصول إلى مسار الكود الضعيف هو 0x80862007. بالإضافة إلى رمز التحكم الرئيسي، فإن رموز التحكم IOCTL "الفرعية" المذكورة أعلاه التي سيتم تغطيتها في هذا التحليل ستكون الرمز 0x33 للوصول إلى استدعاء الدالة memmove، والرمز 0x30 للوصول إلى مسار الكود الخاص باستدعاء الدالة memset. لن يغطي هذا التقرير أي تفاصيل تتعلق بإجراء DriverEntry، حيث توجد وثائق كافية على صفحة توثيق مايكروسوفت لتقديم شرح شامل.
للبدء، نريد معرفة كيف يمكننا التفاعل مع برنامج التشغيل هذا في المقام الأول. الطريقة الأكثر شيوعاً للتواصل مع برنامج التشغيل هي من خلال استخدام دالة تسمى DeviceIoControl. الفكرة العامة وراء هذه الدالة هي أنه يمكننا تمرير مقبض برنامج تشغيل صالح تم إنشاؤه بواسطة CreateFileA، وتمرير رمز تحكم IOCTL يتوافق مع إجراء النواة الذي نريده، وتمرير بنية (أو مخزن) يتوقعها، وستقوم بإرجاع البيانات في مخزن الإخراج الخاص بنا. بينما يمكن أن تكون إجراءات مثل هذه ضرورية في بعض الأحيان (مثل الوصول إلى السجلات الخاصة بالنموذج لأغراض رفع تردد التشغيل)، إلا أنها تشكل أيضاً خطراً أمنياً خطيراً. ولكن... كيف؟
في حالة CVE-2015-2291، يمكن تفعيل الثغرة بواسطة مستخدم غير مميز. نظراً لعدم وجود فحوصات تنقية، و لا تتطلب امتيازات المسؤول لاستغلال الثغرة، فإن هذا يشكل خطراً أمنياً. ما يكمن تحت هذين العيبين هو القدرة على التحكم الكامل في استدعاءات الدالتين memset و memmove المكشوفة بواسطة واجهة رمز التحكم IOCTL. تذكر الدالة DeviceIoControl المذكورة سابقاً، كيف يمكننا تمرير بنية سيتم استخدامها في إجراء النواة؟ هكذا يتجمع كل شيء.
دعنا نتراجع خطوة. نريد أولاً الحصول على مقبض برنامج التشغيل المرتبط ببرنامج التشغيل الضعيف. ولكن حتى قبل ذلك، نحتاج إلى تحديد كائن الجهاز المسمى المرتبط. يتم كشف هذه الكائنات للمستخدم عبر رابط رمزي (مضمن في الكود عادةً)، يمكن العثور عليه باستخدام WinObj، جزء من [مجموعة SysInternals]. بينما يمكننا استخدام أداة تفريغ السلاسل النصية لتفريغ الرابط الرمزي، أو بدلاً من ذلك هندسة عكسية لبرنامج التشغيل، قمت ببساطة بتحميل برنامج التشغيل وتحديده باستخدام WinObj. الرابط الرمزي الموجود والمرتبط ببرنامج التشغيل هو \\.\GLOBALROOT\Device\Nal. للحصول على مقبض برنامج التشغيل، نحتاج إلى استدعاء دالة CreateFileA وجعلها تُعيد مقبض برنامج تشغيل صالحاً لاستخدامه لاحقاً في العملية. الكود لهذه العملية هو كما يلي:```C
if (h_nal == (HANDLE)-1)
{
printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);
سنستخدم مقبض برنامج التشغيل لاحقًا في عملية الاستغلال. في الوقت الحالي، سنبدأ في إعداد استغلالنا. الخطوة التالية ستكون تحميل مكتبة `ntdll.dll` باستخدام دالة [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) لإرجاع [مقبض وحدة](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types)، حتى نتمكن من تحديد موقع الوظائف التي نحتاجها ديناميكيًا. على الرغم من أن مكتبة `ntdll.dll` قد تكون محملة بالفعل في عمليتنا، إلا أننا مع ذلك نحتاج إلى الحصول على مقبض للمكتبة يمكننا استخدامه. الوظائف التي نحتاجها للاستغلال هي [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) لتسريب العنوان الأساسي لنواة NT (مع سلامة عملية متوسطة) لاستخدامه لاحقًا في عملية الاستغلال، ووظيفة [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) لتحفيز الثغرة. أما بالنسبة للكود الخاص بتحميل مكتبة `ntdll.dll`، فهو كما يلي:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);
الآن بعد أن حصلنا على مقبض (handle) للمكتبة، سنبدأ بتحديد موقع الدالة NtQueryIntervalProfile. للبدء، سنحتاج إلى تعريف نوع (type definition) لهذه الدالة، لأنها غير موثقة. بينما يمكنك العثور على تعريف النوع عبر الإنترنت، فقد وفرته هنا لسهولة الوصول:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
لاستخدام هذه الوظيفة، سنحتاج أيضًا إلى تعريف متغير (محلي أو عام، حسب اختيارك) باستخدام نوع `NtQueryIntervalProfile`. الآن، كيف نحول هذا المتغير إلى وظيفة فعلية؟ للقيام بذلك، سنستخدم وظيفة تُسمى [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). من خلال تمرير مقبض للوحدة التي نريد البحث فيها (المعامل الأول) وتمرير اسم الوظيفة (المعامل الثاني)، يمكننا تحديد موقع أي وظيفة نريدها في الوحدة واسترداد مؤشر لتلك الوظيفة! يتم توفير الكود لمساعدتك في معالجة هذه المعلومات.```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);
السبب في نجاح تحميل الدوال ديناميكيًا والقدرة على استخدامها هو أن الدوال نفسها عبارة عن مؤشرات إلى كود قابل للتنفيذ. النص الفعلي للدالة هو الكود الذي سيتم تنفيذه.