حظي معالجو الاستثناءات المتجهة (VEH) باهتمام كبير من صناعة الأمن الهجومي في السنوات الأخيرة، لكن تم استخدام VEH في البرامج الضارة لأكثر من عقد من الزمن الأن. يوفر VEH للمطوّرين طريقة سهلة لاصطياد الاستثناءات وتعديل سياقات التسجيل، لذا من الطبيعي أن تكون هدفًا مناسبًا لمطوّري البرامج الضارة. ورغم كل الاهتمام الذي تلقاه، لم يعلن أحد عن طريقة لإضافة معالج الاستثناءات المتجهة يدويًا دون الاعتماد على واجهات برمجة التطبيقات المدمجة في Windows التي غالبًا ما تكون مرتبطة بمنتجات كشف نقطة النهاية والاستجابة لها.
في عام 2015، نشر مستخدم UnKnoWnCheaTsuser مقتطفات رموز لمعالجة قائمة VEH، ومؤخرًا في عام 2024 نشر باحث يُدعى mannyfreddy مدونة تتناول بالتفصيل كيفية عمل معالجات الاستثناء الموجهة. كما تناولت المدونة mannyfreddy كيفية معالجة قائمة VEH، بل واستعرضت كيفية استخدام معالجات الاستثناء الموجهة لإدخال رمز داخل عملية أخرى عن بُعد
في عام 2022، أصبحتُ مهتمًا بمعالجات الاستثناءات الموجهة بعد أن نشر rad9800 إثبات مفهوم لاستعراض قائمة معالجات الاستثناء المتجهة واستدعاء واجهة برمجة تطبيقات تُدعى RemoveVectoredExceptionHandler على كل معالج مسجل لمسح القائمة. وقد قادني ذلك إلى تطوير طريقة للمعالجة يدويًا لقائمة VEH وطريقة لاستخدام VEH لتنفيذ إدخال رمز داخل عملية بدون إنشاء خيوط. وبما أن المعلومات المتعلقة بهذه التقنيات بدأت تنتشر علنًا، رأيت أن الوقت قد حان لنشر أبحاثي في هذا المجال.
في هذا المنشور، سنلقي نظرة على كيفية التعامل مع قائمة معالج الاستثناء المتجه في نظام التشغيل Windows، وكيف يمكن استخدام معالجات استثناءات الموجهة للتهرب من الدفاعات وتنفيذ إدخال رمز داخل عملية. يمكنك العثور على الرمز المصاحب لهذا المنشور هنا.
تُعد معالجات الاستثناء الموجهة هي إحدى آليات Windows التي توسع معالجة الاستثناء المنظمة (SEH). باختصار، تسمح هذه الآلية للمطورين بتسجيل دالة سيتم استدعاؤها عند إنشاء استثناء في العملية. وستتلقى هذه الدالة معلومات حول الاستثناء وحالة السجلات عند حدوث الاستثناء.
يتم تخزين معالجات الاستثناءات الموجهة في قائمة وعندما يتم إنشاء استثناء، فإنه سيتم استدعاء معالج الاستثناء الأول في القائمة. عادةً، تكتب VEH للبحث عن أنواع معيّنة من الاستثناءات التي تتوقّع التعامل معها. إذا تم استدعاء المعالج الخاص بك ولم يكن رمز الخطأ أحد الرموز التي تهمك، فيمكنك السماح للعملية بالاستمرار في السير في القائمة للعثور على معالج يمكنه التعامل مع الخطأ. وإذا كان رمز الخطأ هو الرمز الذي تريد معالجته، ثم يمكنك القيام بأي احتياجات تم تنفيذها وإخبار العملية بأنه قد تم التعامل مع الخطأ، وسيتم استئناف التنفيذ. إذا تم استعراض قائمة VEH بالكامل ولم يسمح أي معالج للعملية بالتوقف عن التنفيذ، فسيتم إنهاء العملية.
يوضح الرسم البياني أدناه كيف يبدو VEH. سيبدأ معالج الاستثناءات من بداية القائمة ثم يتنقل بين كل عنصر بحثًا عن معالج مناسب. إذا عاد إلى بداية القائمة، فسيتم إنهاء العملية.
يمكنك العثور على بعض الأمثلة على الرمز من Microsoft هنا. وباختصار، يمكنك إنشاء معالج استثناء موجه عن طريق إنشاء دالة تأخذ مؤشرًا إلى بنية _EXCEPTion_POINTERS كوسيطة، ثم استدعاء واجهة برمجة التطبيقات AddVectoredExceptionHandler في نظام التشغيل Windows لتسجيل معالج الاستثناء. وفيما يلي وسيطات دالة AddVectoredExptionHandler.
تخبر الوسيطة الأولى الدالة بما إذا كان سيتم إدراج المعالج الجديد في بداية قائمة معالج الاستثناء. وإذا لم تقم بإدراجه باعتباره المعالج الأول، فسيتم إدراجه في نهاية القائمة. الوسيطة الثانية هي مؤشر إلى معالج الاستثناء الخاص بك ليتم استدعاؤه.
لاحظ أنه على الرغم من أن دالة المعالج الخاصة بك من المفترض أن تأخذ بنية _EXCEPTION_POINTERS كوسيط، إلا أنه ليس من الضروري الالتزام بهذا النموذج الأولي إذا كان معالجك لا يحتاج إلى أي وسيطات. وهذا يعني أنه يمكنك الحصول على عناوين ذاكرة تسمى معالجات الاستثناءات الموجهة. وسنرى الآثار المترتبة على ذلك لاحقًا.
بعض منتجات EDR تسجل معالجات الاستثناءات الموجهة الخاصة بها. من حالات الاستخدام الشائعة لهذا الأمر وضع اعتراضات PAGE_GUARD على مناطق معينة من الذاكرة. عندما يتم الوصول إلى منطقة من الذاكرة بحماية PAGE_GUARD، سيتم إنشاء استثناء، ويمكن لاكتشاف نقاط النهاية والاستجابة لها بعد ذلك فحص ما أدى إلى إنشاء الاستثناء لتحديد ما إذا كان ضارًا أم لا.
على سبيل المثال، سيصل الرمز shellcode إلى جدول عناوين التصدير (EAT) لـ Kernel32.dll لحل عناوين الدوال. ومع ذلك، فإن الدالة GetProcAddress الشرعية تفعل ذلك أيضًا. عن طريق وضع اعتراض PAG_UARD على Kernel32.DLL، يمكن أن يقوم EDR بتحليل ما إذا كان الوصول يتم إجراؤه بواسطة وحدة شرعية أم لا، أو من منطقة ذاكرة غير مدعومة. وإذا كان الخيار الثاني، فهذا مؤشر على وجود برنامج ضار محتمل. ناقشت Yarden Shafir سيناريو مشابه في منشور المدونة الممتاز هذا.
نظرًا لأن موردي اكتشاف نقاط النهاية والاستجابة لها يستخدمون معالجات الاستثناءات الموجهة، فمن مصلحتهم التأكد من عدم العبث بقائمة VEH. وإذا تمكنت من إضافة معالج استثناء إلى مقدمة القائمة، فلا يمكنك ببساطة تمرير التنفيذ إلى معالج اكتشاف نقاط النهاية والاستجابة لها. وفي منتج شائع واحد على الأقل من المنتجات التي اختبرناها، سيؤدي استدعاء AddVectoredExceptionHandler دائمًا إلى إضافة VEH في نهاية القائمة، بغض النظر عما إذا كنت قد طلبت من Windows بإضافته في مقدمة القائمة أم لا.
ابقَ على اطلاع دائم على أبرز الاتجاهات في مجالات الذكاء الاصطناعي، والأتمتة، والبيانات، وغيرها الكثير من خلال رسالة Think الإخبارية. راجع بيان الخصوصية لشركة IBM.
ونظرًا لأن استدعاء واجهة برمجة التطبيقات AddVectoredExptionHandler (التي تستدعي بدورها RtlAddVectoredExptionHandler) ليس خيارًا، فإنه يمكننا ببساطة (وهذا تبسيط مبالغ فيه) إعادة تنفيذه.
كما هو موضح في الرسم البياني السابق، يتم تخزين قائمة معالج الاستثناء الموجه كقائمة ثنائية الارتباط. وتُعد القائمة ثنائية الارتباط بنية بيانات تحتوي فيها كل إدخال على مؤشر إلى الإدخال التالي، ومؤشر إلى الإدخال السابق، ثم بعض البيانات. وفي هذه الحالة، تكون البيانات عبارة عن بنية أخرى تحتوي على معلومات لمعالج الاستثناء الموجه.
المصدر الرسومي: https://www.osronline.com/article.cfm%5Earticle=499.htm
يبدو كل معالج استثناء موجه على النحو التالي.
يحتوي عنصر LIS_ENTRY على مؤشرات Flink/Blink الخاصة بنا، وعدادًا مرجعيًا، وقيمة محجوزة لا تهم حقًا، وأخيرًا مؤشرًا للدالة التي يجب استدعائها. باستثناء أن هذا المؤشر ليس في الواقع مؤشرًا، ولكنه مؤشر مشفر. يمكن تشفير / فك تشفير المؤشرات باستخدام دالتي EncodePointer /DecodePointer Windows API.
هناك طريقتان لتحديد موقع قائمة معالج الاستثناء الموجه. يعتمد أحدها على استخدام طرق استدلالية مثل تحديد الدالة التي تشير إلى متغير LdrpVectorHandlerList وقراءة البايتات للعثور على العنوان. أما الطريقة الثانية هي تسجيل معالج استثناء موجه جديد والانتقال عبر القائمة المرتبطة بشكل مضاعف حتى نتعرف على مؤشر إلى .data قسم من NTDLL، والذي يجب أن يكون عنوان القائمة المرتبطة. وهذه الطريقة الأخيرة هي الطريقة التي وثقها Rad9800، وهي الطريقة التي أفضلها، حيث لا داعي للقلق بشأن تغير الإزاحات أو أنماط البايت عبر إصدارات Windows.
ويكمن الخطر في هذا النهج في أنه إذا ظهر استثناء لا يستطيع معالج الاستثناء الخاص بك معالجته، فسيتم إنهاء عمليتك. كما تستخدم العمليات الشرعية أيضًا معالجات الاستثناءات الموجهة لاكتشاف الأخطاء التي تتوقع حدوثها، لذا ربما لا يكون تقصير القائمة هو النهج الأفضل. وبدلاً من ذلك، يمكننا تحديث القائمة بشكل صحيح لإدراج معالج الاستثناء الخاص بنا أولاً.
ومن خلال هذا النهج، يمكننا التعامل مع الأخطاء التي تهمنا، وتمرير أي شيء آخر إلى معالج الاستثناء التالي.
كما رأينا، فإن تنفيذ إصدارنا الخاص من واجهة برمجة التطبيقات AddVectoredExptionHandler ليس أمرًا معقدًا للغاية. ولكن الأهم من ذلك، أنه لم يتطلب منا حقًا التفاعل مع النواة، بصرف النظر عن استدعاء NtProtectVirtualMemory لتغيير حماية الذاكرة في .mrdata قسم من NTDLL. ونظرًا لأنه يتم تخزين جميع المعلومات التي تستخدمها العملية عند استدعاء معالجات الاستثناءات الموجهة داخل العملية، فإنها تمثل هدفًا رائعًا كتقنية حقن عملية بدون ترابط.
ما هي عملية الحقن بدون خيط؟ لقد غطتCeri Coburn ذلك في حديثها لعام 2023 في Bsides Cymru، "إبر بدون خيط". ومن الطريف أن هذه المحاضرة صدرت قبل أن ألقي محاضرة في مؤتمر داخلي لشركة IBM لعرض تقنيتي الجديدة في الحقن التي لا تتطلب آلية تنفيذ مباشرة.
للتلخيص، تتطلب تقنيات العمليات التقليدية للحقن طريقة من أجل:
يمكننا مزج هذه العناصر الأولية ومطابقتها للحصول على تقنيات مختلفة، وبعض التقنيات لا تحتاج إلى كل الخطوات. وعلى سبيل المثال، إذا قمت بتخصيص ذاكرة في العملية البعيدة مثل RWX، فلن تحتاج إلى تغيير الحماية لاحقًا. أو إذا قمت باستدعاء NtMapViewOfSection، فسيتم تخصيص ذاكرتك وكتابتها في العملية البعيدة في نفس الخطوة. ولكن الشيء الوحيد الذي تتطلبه كل تقنيات حقن العمليات التقليدية هو العناصر الأولية للتنفيذ. وهذا عادةً ما يكون CreateRemoteThread/QueueUserAPC/SetThreadContext (أو دالة Nt المكافئة لها). ونتيجةً لذلك، تخضع العناصر الأولية للتنفيذ هذه لفحص دقيق من قبل منتجات الأمان للاستخدام الخبيث. ويُعد استدعاء إجراء تنفيذ عنصر أولي يستهدف ذاكرة غير مدعومة داخل عملية عن بُعد وسيلة فعّالة جدًا لكشف الإشارة الخاصة بك.
فلماذا لا نتخطى العناصر الأولية للتنفيذ تمامًا؟" باستخدام معالجات الاستثناءات الموجهة، تعمل على النحو التالي:
الخطوة الأخيرة هي الخطوة الحساسة التي تسمح لنا بتجاوز الحاجة إلى تنفيذ عنصر أولي عن طريق تشغيل استثناء في العملية البعيدة. وهناك بعض الطرق للقيام بذلك، ولكن يُعد فخ PAGGUARD أفضل طريقة، في رأيي. لقد نفذت تقنيات الحقن لكل من العمليات الجديدة والحالية باستخدام فخاخ PAGE_GUARD.
إذا كنت تُنشئ عملية جديدة، فيمكنك نشر العملية في حالة تعليق ووضع فخ عند نقطة الدخول للعملية. وعادةً ما يؤدي وضع العملية في حالة تعليق ومعالجتها إلى وضع علامة بأنك تقوم بسلوك تفريغ العملية. ومع ذلك، بما أننا لا نكتب إلى أي أقسام .text أو باستخدام أي بدائل للتنفيذ، يجب ألا نتأثر بهذا الكشف. ولكن كما هو الحال دائمًا، اختبر هذا في مختبرك.
يُعد الحقن في عملية التشغيل أكثر تعقيدًا، لكنني وجدت أن أسهل طريقة هي:
يمكن أن تكون هذه التقنية غير مستقرة بعض الشيء إذا كنت تقوم بتنفيذ كود shellcode مباشرةً نظرًا لأنها تستولي على مسار التنفيذ، مما قد يؤدي إلى تعطل العملية. وجدتُ أنه من الأكثر موثوقية إضافة shellcode تمهيدي ينفّذ معالج استثناء موجه مناسب يقوم بإنشاء مسار تنفيذ جديد لكود shellcode، ثم يعيد تدفّق تنفيذ الكود إلى المسار الأصلي بشكل طبيعي. لن يخضع إنشاء مسار التنفيذ المحلي هذا لنفس الفحص الذي يخضع له إنشاء مسار تنفيذ بعيد.
الاعتبار الأخير لكلا الطريقتين هو أنه كلما حدث خطأ أثناء العملية، سيتم استدعاء VEH وسيتم تنفيذ shellcode الخاص بك. وقد يؤدي ذلك إلى إنشاء عدد كبير جدًا من الإشارات داخل عملية واحدة، مما يؤدي في النهاية إلى تعطلها. وجدتُ أن حل هذه المشكلة يكون إما بتشغيل كود shellcode المذكور أعلاه، والذي يمكنه التحقق من أن الاستثناء هو فخ PAGE_GUARD، أو عن طريق إزالة معالج الاستثناء المتجه من الإشارة التي تم إنشاؤها حديثًا. يمكن تنفيذ ذلك عن طريق تشغيل BOF لاستعراض قائمة VEH، وتحديد المعالج الخاص بك (وهو مؤشر مُشفّر إلى ذاكرة غير مدعومة)، ثم إزالته من خلال المعالجة اليدوية، أو ببساطة من خلال استدعاء RemoveVectoredExceptionHandler عليه.
أعتقد أن أفخاخ PAGE_GUARD هي أفضل طريقة لإنشاء استثناءات عن بُعد نظرًا لأنها استدعاء NtProtectVirtualMemory واضح ومباشر، وتتم إزالة الفخ بعد إنشاء الاستثناء، ولا تتطلب كتابة أو عنصرًا أوليًا. ومع ذلك، هناك طرق أخرى يمكنك من خلالها تشغيل استثناء عن بُعد، من أجل التنوع:
لا أعتقد أن أيًا من هذه الأفكار جيدة بشكل خاص (ربما باستثناء الفكرة الأولى، والتي فكرت فيها بنجاح)، ولكن النقطة المهمة هي أنك لا تحتاج بالضرورة إلى استخدام فخ PAGE_GUARD.
كما هو الحال دائمًا، لا يعمل Windows Server 2012 بشكل جيد مع التقنيات الموضحة أعلاه، ولكن ليس من الصعب تشغيله. في Windows Server 2012، تفقد بنية VEH أحد الإدخالين المحجوزين الموجودين في الإصدارات الأخرى من Windows. بالإضافة إلى ذلك، فإن قائمة VEH غير موجودة في قسم .mrdata ولكن في قسم البيانات .data .
يمكن إجراء الكشف عن التلاعب في VEH باستخدام نفس الأساليب الموضحة في هذا المنشور للتجول في قائمة VEH. وعادةً ما يتم تكوين منتجات الأمان التي تستخدم VEH لضمان أنها الإدخال الأول في VEH. وإذا لم يكن الأمر كذلك، فربما يكون هناك شيء ضار قد حدث. ومع ذلك، قد يتسبب هذا في مشكلات إذا كان هناك منتجان يعملان بالتوازي ويتوقع كلاهما أن يكون الإدخال الأول في القائمة.
أجرت مجموعة NCC بحثًا ممتازًا حول تعداد معالجات الاستثناء المتجهة عبر جميع العمليات وتحديد أي معالجات تشير إلى ذاكرة غير مدعومة. وكما هو الحال دائمًا، تُعد الذاكرة التنفيذية غير المدعومة مؤشرًا جيدًا على السلوك الخبيث. ويمكن استخدام أداة تتبع الأحداث لاستعلامات التهديدات لنظام التشغيل Windows (ETWTi) لتحديد تخصيص وكتابة وحماية shellcode في الذاكرة غير المدعومة. وبالمثل، ينبغي أن تكون أحداث ETWTi الخاصة بكتابة الذاكرة عن بُعد إلى قسم .mrdata الخاص بعملية ما مؤشرًا عالي الدلالة ومنخفض الضوضاء.