
CVE-2020-15368، المعروف أيضًا باسم "كيفية استغلال برنامج تشغيل ضعيف"
استغلال وإثبات المفهوم (PoC) لـ CVE-2020-15368. أعدت Asrock تغليف برنامج تشغيل rweverything لأداة التحكم في إضاءة RGB الخاصة بها ووقّعته. لقد "حمّوه" عن طريق تشفير استدعاءات ioctl الخاصة بهم... لول. عثرنا على هذا CVE بالصدفة الصيف الماضي، وعلى حد علمي لا يزال برنامج التشغيل غير مُصحح. التأثير بالطبع هو تنفيذ تعليمات برمجية عشوائية في النواة، إلخ. لذا استمتعوا بهذا "0day" لول.
إذا كنت تريد أن تجادلني حول ما إذا كانت هذه CVE حقيقية وصادقة، فلا تتردد في التواصل معي على تويتر، يمكننا أن نخوض معركة كبيرة على وسائل التواصل الاجتماعي العامة وسيكون الأمر مثيرًا حقًا لكل المعنيين! سأشتري حتى نطاقًا لهذا الخطأ إذا كنت ترغب في ذلك. الأمر كله يتعلق بالتسويق!!!!
على أي حال، هذا الخطأ سيئ للغاية، لذا سأستخدمه كدرس تعليمي حول كيفية اختراق برنامج تشغيل ضعيف نموذجي. لهذا، هذا المنشور موجه للمبتدئين. ستتعلم كيفية استغلال برنامج تشغيل ضعيف. هناك الكثير من برامج التشغيل السيئة الأخرى مثل هذا في الخارج. العالم بحوزتك. استمتع
إخلاء مسؤولية: هذا المنشور مقدم لأغراض تعليمية فقط. تقع على عاتق القارئ مسؤولية الامتثال لجميع القوانين المحلية والولائية والاتحادية المعمول بها. لا يتحمل مؤلفو هذا المنشور أي مسؤولية ولا يكونون مسؤولين عن أي إساءة استخدام أو ضرر ناتج عن البرنامج الوارد في هذا المنشور.
عالقون في الحجر الصحي، أنا ورفاقي في السكن (Pear0، Codetector) كنا نعبث على اللوحة الأم الجديدة من Asrock الخاصة بـ Pear0. كانت مصابيح LED الحمراء الساطعة مزعجة للغاية ولم يكن من الممكن تكوينها على لينكس. لذلك كانت خطتنا هي تفكيك برنامج تشغيل ويندوز الذي يتحكم فيها وإعادة إنتاج عمليات الإدخال/الإخراج على لينكس.
باختصار، لم يمر وقت طويل حتى أدركنا أن برنامج التشغيل هو حرفيًا مجرد برنامج تشغيل عام يمنح وصولًا عشوائيًا للقراءة/الكتابة إلى أي شيء. يشمل ذلك سجلات التحكم مثل CR3 وCR4 والذاكرة الفعلية، إلخ. برامج التشغيل من هذا القبيل مخصصة للاستخدام كأداة تصحيح أخطاء وموقع البائع يوضح ذلك بوضوح.

اعتقدنا أن هذا مضحك للغاية. إنه أمر مثير للغاية في المرة الأولى التي تجعل فيها جهاز الكمبيوتر يتعطل ثلاثيًا (triple fault) ويعيد التشغيل بقوة من مساحة المستخدم. (ربما أقل إثارة في المرة العشرين.) على أي حال، أبلغنا عن الخطأ ثم نسينا أمره لمدة عام.
كمبتدئ في النواة، كنت أتساءل كيف يمكنني فعليًا تحميل برنامج التشغيل والتفاعل معه. اتضح أن الأمر سهل للغاية.
يمكنك فقط إنشاء خدمة لبرنامج التشغيل في Process Hacker (من الواضح أنه يتطلب صلاحيات المسؤول لتحميل برامج التشغيل). ثم يمكنك النقر بزر الماوس الأيمن وتشغيله. نعم، الأمر بهذه البساطة حقًا.

يمكننا عرض كائن الجهاز الخاص بنا في WinObjEx64.

يمكننا حتى اللعب مع الجهاز في FileTest.


كل هذه الأدوات الثلاثة مذهلة، خاصة PH و FileTest. إنها مثل سكين الجيش السويسري، وينبغي أن تكون في صندوق أدوات كل محلل عكسي على ويندوز. على سبيل المثال، على حد فهمي، وجد Jonas L عددًا لا يحصى من ثغرات تصعيد الامتيازات المحلية (LPE) في ويندوز بمجرد العبث في FileTest. لذا فإن ويندوز لديه حقًا بعض الأدوات الرائعة للعبث. أتمنى لو كان هناك مثل هذا الشيء على لينكس.
Rweverything لديه ioctl يأخذ ioctl كمعامل، والذي يتحكم في العملية التي سيتم تنفيذها (قراءة ذاكرة، كتابة ذاكرة، قراءة MSR، إلخ)، وunion لبعض المعاملات الخاصة بالعملية مثل عنوان المصدر، عنوان الوجهة، إلخ. عندما قارنّا كود برنامجي التشغيل:

🤔🤔🤔🤔🤔🤔🤔🤔🤔
ومع ذلك، يقوم برنامج التشغيل بمحاولة رديئة لتأمين نفسه من خلال الغموض (security by obscurity) عبر اشتراط أن تكون جميع استدعاءات ioctl مشفرة بشكل مناسب بمفتاح AES مضمّن في الكود. الكود (بعد بعض التنظيف) يبدو هكذا:
if ( IoControlCode == 0x22EC00 )
{
char enc_key[32];
memset(enc_key, 0, sizeof(enc_key));
memmove(enc_key, "C110DD4FE9434147B92A5A1E3FDBF29A", 32ui64);
memcpy(enc_key + 13, ioctl_args->key, 16);
size_t cb_decrypted = 0;
my_decrypted_cmd* decryptedCmd = NULL;
DWORD iv_size = ioctl_args->iv_size;
DWORD input_size = *(DWORD*)(ioctl_args + bufferLen - 6);
// really just calls BCrypt API to get an AES implementation
if ( (unsigned int)decrypt_ioctl_params(enc_key, 32, ioctl_args->iv, iv_size, ioctl_args + bufferLen - input_size - 6, input_size, &decryptedCmd, &cb_decrypted) )
{
// Decryption failed
if ( decryptedCmd )
ExFreePoolWithTag(decryptedCmd, 0);
irp->IoStatus.Status = 0xC000000D;
goto Fail_Out;
}
IoControlCode = decryptedCmd->opcode;
Rweverything_Args = &decryptedCmd->args;
}
else // not 0x22EC00
{
if ( IoControlCode != 0x22E858 &&
IoControlCode != 0x22E860 &&
IoControlCode != 0x22E800 &&
IoControlCode != 0x22E804 )// whitelisted control codes
IoControlCode = 0; // block everything else
}
يسمح برنامج التشغيل ببعض العمليات المملة التي تقوم ببعض عمليات PMIO، لكن جميع رموز التحكم "الممتعة" محجوبة خلف روتين فك التشفير هذا. على الرغم من وجود قائمة السماح الصريحة هذه، إلا أنها لا تزال تتضمن جميع وظائف Rweverything الخطيرة. بدلاً من إخفاء هذه الميزات الخطيرة، كان ينبغي عليهم على الأرجح إزالتها تمامًا.
أيضًا، المثير للاهتمام، أنه يسمح للمستخدم بتحديد جزء من المفتاح (؟؟؟)، ولست أدري لأي سبب. الكود ببساطة مكتوب بشكل سيئ للغاية.
على أي حال، من السهل نسبيًا كتابة كود العميل لاستخدام هذه الواجهة البرمجية المشفرة الغريبة وتمرير أي استدعاءات ioctl نريدها. لن أملّكم بالتفاصيل.
نفتح مقبضًا (handle) لبرنامج التشغيل ونستخدم DeviceIoControl لاستدعاء ioctl، أمر قياسي جدًا.
HANDLE hDevice = CreateFileA("\\\\.\\GlobalRoot\\Device\\AsrDrv104", GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// ... set up the encrypted ioctl data
BOOL result = DeviceIoControl(hDevice, 0x22EC00, ioctl_data, in_buf_size, out_buf, sizeof(out_buf), &bytes_returned, NULL);
الآن بعد أن أصبحنا قادرين على التواصل مع الجزء المخفي من Rweverything في برنامج التشغيل، كان أول شيء أردت فعله هو إحداث انهيار (crash) لأعرف أن عميل برنامج التشغيل الخاص بي يعمل.
الطريقة الأكثر مباشرة لتحقيق ذلك هي الكتابة فوق CR3 ببيانات عشوائية. أعلم أن بعضكم من المبتدئين وهذا لا بأس به، لذا سأشرح بالتفصيل. أنا غبي أيضًا، لذا ربما سيساعدك هذا على التعلم. إذا كنت تعرف ما تفعله، فيمكنك تخطي هذا.
في معمارية x86، عندما يكون الترحيل (paging) مفعلًا (في معظم الأوقات في أي نظام تشغيل حديث)، يشير CR3 إلى العنوان الأساسي الفعلي لدليل جدول الصفحات العلوي. إذا كنت لا تعرف ما يعنيه ذلك، فاقرأ مقال ويكيبيديا عن الذاكرة الافتراضية.
عندما نستبدل CR3 ببيانات عشوائية، مثل 0x0000000000000000، يتم مسح الـ TLB، وعند محاولة تنفيذ التعليمة التالية، سيحاول المعالج (وتحديدًا وحدة إدارة الذاكرة MMU) ترجمة مؤشر التعليمات إلى عنوان فعلي. يمكن اعتبار ترجمة العنوان سلسلة من عمليات التنقل في جدول الصفحات تبدأ من CR3. الآن يشير CR3 إلى الذاكرة الفعلية عند العنوان 0، وهي موجودة فعليًا ويمكن الوصول إليها؛ ومع ذلك، فمن غير المرجح للغاية أن تكون جدول صفحات صالحًا. (إدخالات جدول الصفحات، أو PTEs باختصار، يجب أن تتبع بنية محددة.)
عندما يحدث هذا، سنحصل على خطأ صفحة (page fault) أثناء ترجمة العنوان. الآن، عادةً ما ينقلنا المعالج إلى عنوان معالج خطأ الصفحة. لكن كيف يعرف مكان دالة معالج خطأ الصفحة؟ يتم تخزين هذا في بنية بيانات في الذاكرة تُعرف باسم جدول واصفات المقاطعة (Interrupt Descriptor Table - IDT). يمتلك المعالج سجلًا (يُقرأ ويُكتب بواسطة تعليمتي sidt و lidt) يحمل العنوان الافتراضي لجدول IDT. هل ترى المشكلة الآن؟ للتعامل مع خطأ الصفحة، نحتاج أولاً إلى الوصول إلى ذاكرة افتراضية أخرى، وبالتالي ترجمة عنوان أخرى.
بالطبع، عملية ترجمة العنوان الثانية ستتسبب أيضًا في خطأ. الآن لدينا خطأ مزدوج (Double Fault): خطأ يحدث أثناء معالجة خطأ الصفحة الأول. هذا خطير جدًا ولكن لا يزال قابلاً للاسترداد---سيمنحنا المعالج فرصة أخيرة للتعافي. بالطبع، تُقطع هذه المحاولة أيضًا بشكل وحشي مع خطأ صفحة ثالث ونهائي، وهو الخطأ الثلاثي (Triple Fault). عند هذه النقطة، يستسلم المعالج ويقوم بإعادة تشغيل الجهاز بشكل قسري. إذا نفذت هذا الإجراء على جهاز فعلي، فمن المحتمل أن ترى شاشة BIOS الآن.
الآن إذا كان لديك أي أسئلة، سأعطيك نفس الإجابة التي كانت تُعطى لي دائمًا. وهي أن تذهب لقراءة الدليل Intel Manual Volume 3A. (المعروف أيضًا باسم الكتاب المقدس).
حسنًا، كيف نستغل برنامج التشغيل فعليًا؟ بالنظر حولنا، نجد بدائية قراءة/كتابة عشوائية حرة للذاكرة الفعلية. إنها ببساطة تعيّن أي عنوان فعلي تريده باستخدام MmMapIoSpace، وتنسخ المخزن المؤقت إليه (أو العكس)، ثم تلغي تعيين العنوان.
ملاحظة صغيرة: عندما تحاول استدعاء
MmMapIoSpaceببعض الوسائط الغبية مثل ما فعلنا بينما يكون مصحح أخطاء النواة متصلاً، ستحدث مشكلة في النظام (bugcheck). يمكنك تجاوز ذلك بكتابة بايت سحري في WinDbg. ابحث عن تعليق يشير إلىMiShowBadMapperفي exploit.cpp لمعرفة المزيد. لا أعرف حقًا ما علاقة هذا الأمر ولا أهتم حقًا بمعرفته.
كيف يمكننا الاستفادة من هذه البدائية للحصول على تنفيذ تعليمات برمجية في النواة؟ المشكلة الرئيسية في هذه البدائية هي أنها تعمل على الذاكرة الفيزيائية. كبرنامج في وضع المستخدم، ليس لدينا أي فكرة تقريبًا عن شكل تخطيط الذاكرة الفيزيائية ---نظام التشغيل يتولى كل ذلك عنا. حتى لو تمكنا من الحصول على العناوين الافتراضية لبعض هياكل بيانات النواة أو مؤشرات دوال النواة، فليس لدينا أي فكرة عن مكانها في مساحة العناوين الفعلية.
إحدى الأفكار هي قراءة CR3، وقراءة جداول الصفحات، والقيام بترجمة العنوان الافتراضي بأنفسنا. هذه فكرة رائعة. لكنها لا تعمل. وذلك لأن ويندوز لم يعد يسمح لك بتعيين جداول الصفحات باستخدام MmMapIoSpace. لذا نحتاج إلى أن نكون أكثر ذكاءً.
استخدمت تقنية xeroxz من VDM. إنها بسيطة جدًا، لكن التقنية ذكية جدًا. على الرغم من أننا لا نعرف تخطيط الذاكرة الفعلية، إلا أنه لا يزال بإمكاننا فحص جميع الذاكرة الفعلية حتى نجد ما نبحث عنه. الشيء الذي يمكننا الاستفادة منه هو أن محتويات الصفحات هي نفسها دائمًا فيزيائيًا وافتراضيًا: أي إزاحات (offsets) نسبية لحدود الصفحات يتم الحفاظ عليها دائمًا. على سبيل المثال، إذا كانت لدي صفحة 0x7fff000000000XXX معينة إلى إطار فعلي 0x0000000123456XXX، فإن XXX في جميع العناوين هو نفسه في كل من العنوان الفعلي والافتراضي. يتم الحفاظ على جميع البنية داخل الصفحة؛ وبالتالي يمكننا فحص بعض الصفحات المثيرة للاهتمام التي نرغب في الكتابة فوقها.
أسهل شيء يمكننا الكتابة فوقه هو على الأرجح بعض معالجات استدعاءات النظام (syscall) أو ioctl التي يسهل الوصول إليها. على ويندوز، هناك دالة قياسية Beep() تجعل جهاز الكمبيوتر يصدر صوت تنبيه. صدق أو لا تصدق، هذه الدالة منفذة في برنامج تشغيل، Beep.sys، والذي يوفر جهاز Beep. (في الواقع، يمكنك رؤيته في لقطة شاشة WinObjEx64 السابقة.) يمكن لأي شخص استخدام جهاز Beep، ونادرًا ما يُستدعى. لذا دعونا نستبدل معالج ioctl الخاص بـ Beep.
يمكننا فتح Beep.sys في IDA وإلقاء نظرة على معالج DeviceIoControl.

عند إزاحة الصفحة 0x270، لدينا هذا الكود بالبايتات 40 53 48 .... لا تتم إعادة توطين أي من هذه البايتات، لذا فحص هذه الدالة سهل جدًا. إذا كانت هناك بايتات معاد توطينها، لكنا بحاجة إلى وضع أحرف بدل (wildcards) مكانها. إنها نفس فكرة فحص التوقيعات عندما تكتب اختراقًا للعبة.
بعد فحص الذاكرة الفعلية لتحديد موقع هذا الكود، يمكننا ببساطة الكتابة فوقه بالشيل كود (shellcode) الخاص بنا. يجب أيضًا أن تكون حذرًا، فقد تكون هناك نسخ متعددة من هذه الصفحة متناثرة في الذاكرة الفعلية (!) لذا ابحث عن جميع النسخ.
عند هذه النقطة، يمكننا بسهولة تصعيد الامتيازات عن طريق استبدال رمز الأمان (security token) الخاص بعمليتنا برمز عملية نظام للحصول على صلاحيات nt authority\system. لسوء الحظ، يتطلب برنامج تشغيل Asrock صلاحيات المسؤول لفتحه على أي حال، لذا فإن هذا ليس مثيرًا للاهتمام.
بالنسبة لنا، نكتب شيل كود أساسي يخصص وينسخ حمولة المرحلة الثانية (stage 2 payload)، ثم ينشئ مؤشر ترابط نواة جديد. لا يمكننا فعل كل شيء في معالج Beep المستبدل لدينا للأسباب التالية: 1) نحن مقيدون بصفحة واحدة، و2) سنتسبب في انهيار النظام عند محاولة إغلاق مقبضنا لجهاز Beep، لأننا أتلفنا أيضًا بقية الكود في جهاز Beep. أما بالنسبة للحصول على مؤشرات النواة، فهذا سهل في الواقع لأن NtQuerySystemInformation سيعطينا إياها مجانًا إذا طلبنا ذلك بلطف.
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp)
{
MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer;
void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao');
user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size);
HANDLE hThread;
user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL);
user_data->nt_IofCompleteRequest(irp, 0);
return 0;
}
لذا نقوم بترقيع Beep بسرعة، ونستدعي معالج ioctl المستبدل، ثم نزيل الترقيع عن Beep. الآن أنشأنا بأمان مؤشر ترابط نواة ينفذ كودنا دون إتلاف أي شيء آخر على النظام. عند هذه النقطة يمكننا تعيين برامج التشغيل الخاصة بنا أو أي شيء آخر.
أنا باحث أمني سيئ وأجد فقط أخطاءً بلا قيمة، بالصدفة. شكرًا لقراءتكم جميعًا. يرجى الاشتراك في OnlyFans