The Curious Case of LenovoRuntimePassword

2022-05-26

كل حاسبة لمن تشتغل يكون عندها boot order يخليها تحدد ترتيب الاجهزة اللي تحاول تقلع منها، مثلا اول شي تجرب تقلع من الهارد، واذا مابي نظام تروح للDVD drive، واذا هم مابي تجرب تسوي network booting وهكذا.

ممكن ملاحظ قبل انه انت تگدر تغير الboot order من داخل النظام، بدون ما تروح لاعدادات الUEFI. بالوندوز مثلا تگدر عن طريق bcdedit، وبلينكس عن طريق efibootmgr. هالشي يتم عن طريق شي اسمه UEFI variables.

كل وحدة من الUEFI variables عندها اسم وGUID وattributes ومحتوى. الattributes مالتها تحدد اذا كانت Non-volatile، يعني اذا تكون باقية حتى بعد ما "يطفى" الجهاز (مكانها الفيزيائي مو مهم - غالبا بالSPI chip اللي محطوط بيها الUEFI)، وايضا تحدد اذا تكون متوفرة اثناء الاقلاع بس، لو مستمرة حتى بعد ما "يشتغل" النظام.

ولو ميحتاج اگول، بس دا ابسط كلشي هوايه حتى اگدر اوصل الزبدة بدون ملل.

لينكس يوفر الvariables على شكل ملفات، غالبًا تلگيها بهالمسار: /sys/firmware/efi/efivars/

فالمهم، چنت داگلب بيهن ووحدة من الvariables اللي لگيتها بداخل لابتوب Lenovo قديم اسمها LenovoRuntimePassword، وهذا محتواها:

اول اربع بايتات (00 00 00 07) تمثل الattributes مالتها. طبعا الattributes مو جزء من المحتوى بس لينكس هيچ يعرضها، وراها اكو string مگطوم (LenovoRuntimePasswo)، بين كل حرف من حروفه اكو 00. ذني الصفاره وجودها مو اعتباطي، وانما بسبب الencoding المستخدم هنا: UCS-2، وهو نفس السبب اللي يخلي عدد الحروف المتوفرة برسالة SMS وحدة تقل من 160 الى 70 باللحظة اللي تخلي بيها حرف عربي، حتى لو باقي رسالتك كله انگليزي.

الUnicode هو مجرد character set، او بمعنى ثاني، هو مجرد تمثيل للcharacters على هيئة رموز. الUnicode هو امتداد للASCII، فيعني مثلا الرمز 65 بالASCII والUnicode هو A. الUnicode اكو اله عدة encodings، اشهرها UTF-8 وUTF-16. الUTF-16 هو امتداد للUCS-2، ثنيناتهم نفس الشي عند اول 65,536 رمز، بس الUCS-2 تخلص الرموز مالته هنا (لأن حجم الcharacters مالته ثابت، بايتين بس)، بينما الUTF-16 وراها يبدي يسوي ملاعيب تفاصيلها خارج سياق هالمقال.

فبما انه الUCS-2 حجمه ثابت، اذا ردنا نسوي encoding لحرف h، اللي رمزه 104، او 0x68، راح يصير عندنا 0x0068، او 0x6800 اذا چان الترميز little-endian. مرات تنحط byte-order mark بالبداية حتى تعرف اذا النص little او big-endian.

فاللي ديصير ليفوگ انه الhex viewer ديتعرف على الحروف لأن اماكنها بالUnicode نفسها بالASCII، بس الصفاره مديعرف معناها فديحط نقاط.

الUEFI مال اللابتوب بي نوعين من الپاسوردات: Supervisor وUser. الSupervisor اذا تدَخله حيخليك تعدل كل الاعدادات، بينما الUser ميخليك تعدل اغلب الاعدادات.

جربت احط Supervisor password (1234)، وذني الفروقات الصارت بالملف:

وراها جربت اخلي 1233 للSupervisor والUser:

نگدر نشوف انه اكو flag مكون من بايتين (00 01) يحدد وجود الSupervisor password. وراها تجي 16 بايت شكلها هاش1 (لأن تغيرت كليًا بس غيرنا حرف) وراها تجي 16 بايت كلها صفاره، وراها تتكرر نفس السوالف بس للUser وراها فد بايتين شكلها checksum.

بما انه الهاش حجمه 16 بايت، اول شي اجه ببالي هو MD5. اخذت الMD5 مال الپاسورد وجربت كل الencodings اللي موجودة بالصورة الفوگ، بس ولا وحدة طابقت، فيعني اما ديصير salting او ديتم استخدام غير hashing algorithm.

فقررت اسوي شوية هندسة عكسية، نزلت firmware dump لللابتوب من النت، وحطيته بأداة اسمها UEFITool وبحثت عن LenovoRuntimePassword وحصلت على مطابقة بملف اسمه SystemUserManagerDxe.

الDXE معناها Driver eXecution Environment، وهي مرحلة من مراحل اقلاع الUEFI اللي خلالها يتم تنفيذ عدة binaries اسمها DXE drivers (مثل SystemUserManagerDxe) على وفق Dependency order محدد.

فاستخرجت الbinary، حطيتها بGhidra وگعدت اباوع على الstrings اللي گدرت Ghidra تكتشتفها. اول شي شفته هو هالformat string:

اذا نروح للمكان اللي ديتم استخدامه بي ونباع على الdecompilation حنشوف الاتي:

الchecksum اللي بآخر الvariable شكله حرفيا مجرد الsum مال كل الvariable لحد 0x6a (يعني الى بداية الchecksum، اذا نستثني اول اربع بايتات مال الattributes اللي حاطها لينكس).

فگعدت اتتبع الfunction، مشفت شي مهم ديصير للformat string بس لگيت هالfunction الغريبة اللي دتاخذ pointer للchecksum وشكم شغله لخ (اتوقع الvariable من ضمنها):

بالبداية اعتقدت انه هي هاي الfunction اللي ديصير بيها الhashing للپاسورد، لسببين:

گعدت اگلب الcode على السريع، بس حسيت اكو شي غريب هنا. مثلًا هي دتاخذ pointer للchecksum، بس مدتغير القيمة الديوجهلها، فيعني ايا كان الديصير، فهو مديأثر على محتوى الvariable بشكل مباشر. الأغرب من هيچ لگيت hex lookup table (يعني string محتواه 0123456789ABCDEF).

زين لعد شنو هاي؟ مادري الصراحة، يمكن نوع من الobfuscation، او يمكن مدتسوي شي اصلا؛ مجرد شي حاطيه حتى يضيع وكت الفضوليين امثالي. مكلفت نفسي باكتشاف الديصير.

زين ضروري اني اكتشف بنفسي؟ هوايه من الcryptographic algorithms تعتمد على ثوابت معينة، فإذا گدرت اكشف وجود هاي الثوابت بالملف هذا، راح اعرف الخوارزمية اللي ديتم استعمالها للhashing. گعدت ادور على هيچ اداة بالنت، وبعد كم دقيقة من الgoogling، ملگيت هيچ اداة... بس لگيت شي احسن: Ghidra extension اسمه FindCrypt يعلملك اماكن الثوابت.

نزلت الpre-built release، بس طلع مبني لGhidra 10.1.12 ومالتي 10.1.13 فGhidra مقبلت تنصبه... جربت ابنيه بنفسي وشمر خطأ غريب كلشي مفتهمت منه، بعد البحث، طلع الJDK مالتي 17 وهو يتوقع 11، نصبت ال11، هالمره gradle اشتكى انه نسخته قديمة...

وره ما بنيته شغلته على الملف وملگه شي. شكيت انه الخوارزمية مموجودة بهالملف اصلا، خصوصا انه الUEFI يحتاج يسوي hashing بغير اماكن، مثل الHDD password (اللي سالفته سالفة)، فيعني من المنطقي انه يكون اكو abstraction لهلسوالف. گعدت ادور بالDXE drivers لحد ما لگيت واحد اسمه كلش مثير للاهتمام: SystemCryptSvcRt:

حطيته بGhidra وذني الstrings اللي اكتشْفَتها:

زين شلون اعرف اذا هذا الdriver ديستخدمه SystemUserManagerDxe لو لا؟

الطريقة الاساسية للتواصل بين فد UEFI driver وباقي الdrivers والبرامج هي الprotocols. كل protocol يتكون من GUID خاص بي وinterface، الinterface هي مجرد struct. كل protocol instance لازم يكون موجود بhandle معين. الhandle هو مجموعة من الprotocol instances اللي متعلقات ببعض بطريقة ما. ممكن يكون اكو اكثر من protocol instance بس ميصير يكونن موجودات بنفس الhandle.

فاذا ردت تستخدم protocol معين منصبه فد driver معين، راح تستخدم function مثل LocateProtocol اللي تنطيها GUID وتنطيك pointer للinterface مال اول protocol instance موجود لهلGUID. فأنت تگدر تستخدم هاي الinterface طالما تعرف الهيكل مال الstruct (مثلا عندكياه بheader file)، او على الاقل تعرف الoffsets والtypes مال الشغلات اللي تريدها.

نصبت extension لGhidra اسمه ghidra-firmware-utils، من مميزاته انه يكتشفلك هوايه شغلات تخص الUEFI، بدون ما نتعمق بالتفاصيل، راح يلگيلنا functions مثل LocateProtocol وInstallMultipleProtocolInterfaces. فنصبته، وراها حطيت SystemCryptSvcRt بGhidra وخليته يفحصه... ولگيت هالسطر بالentry function:

بالنسبة لل_LAB، فGhidra عبالها هالمكان بي instructions. هالشي طبيعي، شغل الdisassemblers اصعب مما نتوقع بهوايه:

اذا نباع على الUEFI spec، راح نشوف انه InstallMultipleProtocolInterfaces هي variadic function، يعني تاخذ عدد غير محدد من الarguments، اول شي تاخذ handle، وراها تاخذ ازواج من الGUIDs وinterface pointers، وبالاخير تاخذ NULL. خلي نرتب شوية:

فهسه نريد نشوف الاماكن اللي ذني الGUIDs موجودة بيها (احتمال نلگي شغلات ثانية تهمنا)، واذا SystemUserManagerDxe من ضمن ذني الاماكن لو لا.

اداة UEFITool بيها GUID search، فيعني نحتاج نلگي طريقة نحول بيها الbytes الى GUIDs. الuuid module بpython تخلينا نحول الbyte strings الى GUIDs. بقه بس نلگي طريقة نحول الbytes الى python byte strings. نگدر نسوي هالشي بGhidra بسهولة:

وراها:

وراها:

لگيت هوايه matches مثيرة للاهتمام، منها SystemPasswordCredentialDxe وSystemHddPwdDxe واكيد، SystemUserManagerDxe.

مرات الGUIDs مجرد تكون موجودة بالbinary بدون ما يتم استعمالها، فحتى نتاكد، نبحث عن اول كم byte منها ونشوف اذا اكو XREFs. اول كم GUID ممستعملات هنا:

بس لگيت واحد مستعمل:

فهسه شلون نعرف شنو الخوارزميات الموجودة بSystemCryptSvcRt؟ نشغل FindCrypt! ذني الثوابت اللي لگاهن:

لمن شفت SHA256 طخت عندي، تتذكرون شلون قبل شوية شفنا انه اكو 16 بايت مصفره وره الهاش؟ بديت اشك انه ذني هي تكملة الهاش بس ديتم تصفيرها. فإذا هالنظرية صح، معناها الهاش حجمه الحقيقي 32 بايت، واحزروا الهاشات مال SHA256 شگد حجمها...

لابتوبي (B470e) متاكد انه بي، اكو لابتوبات ثانية شبه متأكد انه بيها لأن بحثت عن اسم الvariable بالfirmware dump مالها، مثل B570e وB470. ماتوقع انه موجود بهوايه موديلات والا چان واحد اكتشفه قبلي، لمن بحثت عن اسم الvariable بگوگل وYandex النتائج الوحيدة اللي لگيتها هي مال ناس ناشرين كل الvariables اللي بجهازهم.

المكان الوحيد اللي مذكور بي LenovoRuntimePassword هو SystemUserManagerDxe، وجربت اغير قيم الvariable وكلشي مصار، مجرد رجعت القيم مثل ما هيه وره ما رستت، فعالاكثر لا.

عالاكثر احتاجوه اثناء الtesting ونسوه، هالخبالات مو غريبة على Lenovo.


بلينكس، هوايه برامج تعتمد على PAM، اللي يفصل الauthentication logic عن البرنامج ويخليك تكتب authentication modules وتسويلها chaining وهوايه شغلات ثانية بدون ما تعدل على الcode مال البرنامج.

بالاضافة للشغلات المعروفة، اكو مثلا module اسمها pam-duress تخليك تحط پاسورد تستخدمه عند الطوارئ. مثلا اذا واحد جبرك تنطيه پاسورد حاسبتك، راح تنطيه پاسورد الطوارئ وراح يگدر يدْخُل بس الmodule راح تسوي شغلات من اختيارك، مثلا تراسل الشرطة او تمسح ملفات معينة، اكو module ثانية اسمها howdy، توفرلك facial recognition كوسيلة login، الخ...

فسويت module شلون مچان اسمها pam_biospw، شغلها واضح من اسمها. طبعا بما انه احنا عدنا نص الهاش بس، فاحتمالية الcollisions اعلى بهواية، بس هالشي مو هلگد مهم بهالسياق.

تگدر تستخدم الmodule ويه ssh او sudo او ويه اي برنامج يعجبك طالما يستخدم PAM ولابتوبك بي هالvariable.

حتحتاج module اسمها pam-python، بDebian وجماعته اسم الpackage هو libpam-python.

حط pam_biospw.py ويه باقي الmodules للترتيب، وتأكد من الpermissions مالتها.

وراها، حتى تستعملها بssh ك2FA مثلا، حتحتاج تروح لsshd_config وتخلي yes يم UsePam وChallengeResponseAuthentication.

وراها روح ل⁦/etc/pam.d/sshd⁩ وخلي ذني بالأخير:

auth required pam_python.so /path/to/pam_biospw.py

وممكن همين تغير مسار الvariable اذا مسويله caching او هيچ شي:

auth required pam_python.so /path/to/pam_biospw.py /path/to/variable

If a programmer writes software for a living, he had better be specialized in one or two problem domains outside of software if he does not want his job taken by domain experts who learn programming in their spare time.

— Tu Do, Operating Systems: From 0 to 1