
استغلال إثبات المفهوم لـ PyInstaller CVE-2019-16783
هذا هو إثبات المفهوم الخاص بي لثغرة Windows PyInstarter في الإصدارات < 3.6 الموجودة في الخيار --onefile. يمكن للمهاجم تحقيق تنفيذ الأوامر وربما تصعيد الامتيازات المحلية (LPE) عن طريق اختطاف مكتبة DLL يتم استيرادها بواسطة مكتبة DLL الخاصة بمفسر بايثون المستخدمة بواسطة ملف PyInstaller الثنائي. سيتبع شرح قصير للثغرة وعملية الاستغلال. شكري لـ Alter Solutions لاكتشاف الثغرة والكتابة عنها في PagedOut #3 (الصفحة 55 في ملف PDF).
نشأت الثغرة بسبب إنشاء ضعيف لدليل يستخدمه PyInstaller في وقت تشغيل الملف الثنائي المنفذ. يقوم PyInstaller بإنشاء دليل _MEIPIDX في مجلد temp الخاص بالمستخدم، ويضع فيه محتويات مختلفة، مثل مكتبة DLL الخاصة بمفسر بايثون المستخدمة لتشغيل كود بايثون المعبأ في ملف PE قابل للتنفيذ.
المشكلة في هذه العملية هي أن الدليل الذي تم إنشاؤه لـ NT AUTHORITY\SYSTEM كان C:\Windows\Temp، مما سمح لأي شخص بتخمينه والكتابة داخله. لذا، كان اختطاف DLL ممكنًا على سبيل المثال عند تشغيل مفسر بايثون. هنا هو الـ commit الذي أصلح الثغرة. بدلاً من الاعتماد فقط على بعض دوال API القياسية لإنشاء الدليل، طبق المطورون دالتهم الخاصة للحصول على تحكم أكبر في إنشاء الدليل.
نظرًا لأن هذا كان أول إثبات مفهوم لي، سأناقش بعض المشاكل التي واجهتها على طول الطريق.
أولاً، إعداد بيئة لهذا الإثبات لم يكن صعبًا بشكل خاص، لأن كل ما احتجته هو الإصدار الصحيح من الحزمة. ومع ذلك، عندما قمت بتثبيته، تعطل:
Traceback (most recent call last):
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
...
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\site-packages\PyInstaller\building\utils.py", line 653, in <genexpr>
strip_paths_in_code(const_co, new_filename)
File "c:\users\ckrielle\appdata\local\programs\python\python38\lib\site-packages\PyInstaller\building\utils.py", line 660, in strip_paths_in_code
return code_func(co.co_argcount, co.co_kwonlyargcount, co.co_nlocals, co.co_stacksize,
TypeError: an integer is required (got type bytes)
بعد البحث قليلاً، رأيت أن إصدار بايثون الخاص بي هو السبب (أستخدم 3.8.10). جعل إصدار بايثون الخاص بي من الصعب تثبيت أي إصدار آخر 3.8.x، لأنه عند التثبيت سيجد المثبت إصدار Python38 الخاص بي ويعيد خطأ. حاولت أيضًا بناء إصدار آخر 3.8، لكنني لم أستطع لأنه كان بحاجة إلى Visual Studio 2015 بينما لدي 2022. في النهاية، اخترت تنزيل Python 3.7.5، والذي عمل بشكل مثالي. لذا قمت بإنشاء بيئة افتراضية للإصدار 3.7.
أدركت أن إعداد البيئة يمكن أن يتراوح بين أن تكون جاهزة بالفعل، إلى أن تستغرق وقتًا طويلاً لإعدادها بشكل صحيح. على الرغم من أهميته، إلا أنه قد ينتقص من متعة استغلال هدفنا فعليًا.
هناك خطوتان لعملية الاستغلال لدينا: العثور على دليل العملية المعبأة بواسطة PyInstaller، وكتابة مكتبة DLL الخاصة بنا للاستغلال هناك. بالنسبة للجزء الأول، يمكننا العثور على PID من خلال دوال WINAPI القياسية (CreateToolhelp32Snapshot، Process32First، Process32Next). عمل هذا بسلاسة. بعد ذلك، نحتاج إلى العثور على الرقم الأخير من دليل _MEI. يقترح الإكسبلويت الأصلي استخدام دالة GetFileAttributesA فقط، والتحقق مما إذا كان رمز الحالة المرتجع هو FILE_ATTRIBUTE_DIRECTORY. لكن هذا لم يعمل معي. كان حلي هو إنشاء ملف في كل دليل مرشح، وإذا تم إنشاء الملف بنجاح، فهذا يعني وجود الدليل. لسبب ما، كان الحالة المرتجعة للملف من GetFileAttributesA هي FILE_ATTRIBUTE_ARCHIVE، لذا أتحقق من ذلك. قبل المتابعة، الحلقة أثناء البحث عن PID هي لأننا نريد حقن مكتبات DLL الخاصة بنا عند تشغيل العملية المستهدفة، ليكونوا جاهزين قبل حدوث التحميل.
بالنسبة لمكتبة DLL، نحتاج إلى اختطاف مكتبة DLL يستوردها مفسر بايثون (python37.dll). للحصول على مكتبات DLL التي يستوردها المفسر، يمكننا فتح DLL باستخدام PE-Bear. إحدى مكتبات النظام المستوردة هي version.dll. لذا يمكننا إنشاء DLL الخاصة بنا، وإساءة استخدام ترتيب البحث لمحمل Windows عن طريق وضعها في دليل temp للعملية المعبأة بواسطة PyInstaller. بهذه الطريقة، عندما تريد استيراد DLL، ستقوم باستيراد DLL الخبيثة بدلاً من الصحيحة.

لكن القيام بذلك لن يعمل. السبب هو أن استيراد الدالة التي يستدعيها مفسر بايثون من version.dll (تحديدًا VerQueryValueW من الصورة) لا يتم حله. لذا يتعطل البرنامج. لحل هذه المشكلة، نحتاج إلى عمل وكالة DLL (DLL proxying). باختصار، نقوم بتكوين DLL الخبيثة لتصدير دوال DLL الأصلية، ونحضر DLL الأصلية معادة التسمية، بحيث يمكنها تحميلها واستدعاء الدالة منها. لذا بالنسبة لإكسبلويتنا، سنقوم بتجميع DLL خبيثة تحتوي على DllMain والذي سيسمح لنا بتحقيق تنفيذ الكود. سنقوم بتصدير جميع دوال version.dll، ونسخ version2.dll الأصلي من النظام ووضعه في نفس الدليل. بهذه الطريقة، يمكن لـ version.dll توجيه استدعاءات الدوال إلى version2.dll. وبمجرد أن نحصل على هذه الـ DLLs، كل ما نحتاجه هو نسخها في دليل العملية، والانتظار للحصول على تنفيذ الكود. لشرح أفضل لـ وكالة/اختصار DLL، اقرأ المقال المرتبط.

على الرغم من أن هذه الخطوات سهلة ومباشرة في النظر إلى الوراء، إلا أنها لم تكن كذلك أثناء تطوير الإثبات. قبل فترة، استغرق الأمر بعض الوقت لفهم عملية الإكسبلويت، وبمجرد أن بدأت البرمجة بدأت أفهمها أكثر. بالنسبة لـ DLL، حاولت تجميعها بنفسي بالطريقة التي تمت الإشارة إليها في مستودع إثبات Alter Solutions. لديهم DLL منفصلة لتنفيذ الكود، وأخرى مختلفة للوكالة، والتي تقوم بتحميل payload.dll في وقت التشغيل (يتم استدعاء DllMain عند التحميل). لكنني لم أستطع تجميعها بشكل صحيح. في النهاية بعد قراءة المقال أعلاه، قررت استخدام مستودع DLLProxyProject، الذي قام بتجميع DLL التي أردت، ويمكن استخدامه عمومًا لإنتاج DLL لأغراض الوكالة. حاولت نسخ ملف DLLMain.cpp الخاص به وتجميعه مع ملف الرأس exports.h. وعلى الرغم من أنه عمل، إلا أنه أنتج خطأ:
Fatal Python error: init_sys_streams: can't initialize sys standard streams
OSError: [WinError 6] The handle is invalid
Current thread 0x00002ea8 (most recent call first):
قد يتم تجنب هذا الخطأ باستخدام ملف Utils.cpp، لذا قررت الاحتفاظ بذلك المشروع لتجميع DLL الخاص بي (على الرغم من أنه كان سيكون أجمل لو قمت بذلك بنفسي بالكامل).
بعد إعداد البيئة (استخدمت psexec للحصول على شل NT AUTHORITY\SYSTEM)، قمت بتشغيل الإكسبلويت، وتشغيل الملف الثنائي المستهدف، وحصلت على تنفيذ كود كمسؤول.

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