
تنفيذ لإثبات المفهوم لتقنية مراوغة تهدف إلى إنهاء الخيط الحالي واستعادته قبل استئناف التنفيذ، مع تنفيذ تغييرات في حماية الصفحات أثناء عدم التنفيذ.
██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗
██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗
██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝
██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝
██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝
تنفيذ إثبات المفهوم (PoC) لتقنية تهرب تهدف إلى إنهاء الخيط الحالي واستعادته قبل استئناف التنفيذ، مع تطبيق تغييرات حماية الصفحة أثناء عدم التنفيذ.

طرق السبات والإخفاء معروفة جيدًا في مجتمع تطوير البرامج الضارة (maldev)، مع تطبيقات مختلفة، تهدف إلى الاختباء من ماسحات الذاكرة أثناء السبات، عادة عن طريق تغيير حماية الصفحات وحتى إضافة ميزات رائعة مثل تشفير الشيل كود، ولكن هناك نقطة مهمة أخرى لإخفاء الشيل كود الخاص بنا، وهي إخفاء خيط التنفيذ الحالي.
انتحال المكدس (stack spoofing) أمر رائع، ولكن بعد التفكير قليلاً، خطر لي أنه ليست هناك حاجة لانتحال المكدس… إذا لم يكن هناك مكدس :)
يُترك تقييم قابلية استخدام هذه التقنية للقارئ، ولكن على أي حال، أعتقد أنها طريقة رائعة لمراجعة بعض المواضيع، وتعلم بعض تقنيات maldev لمن هم، مثلي، بدأوا للتو في هذا العالم.
التنفيذ الرئيسي المعروض هنا يحتفظ بكل ما نحتاج لإخراجه من المكدس في قسم البيانات، كمتغيرات عامة، ولكن سيتم نشر تنفيذ ينقل كل شيء إلى الكومة (heap) قريبًا. يهدف إلى إظهار بعض التعديلات الرئيسية التي يجب إجراؤها لجعل هذا الكود مستقلاً عن الموقع (PIC) وقابلاً للحقن.
هذا المستودع منعكس بين GitHub و GitLab.
كل ما ورد هنا يأتي من فهمي للمواضيع المختلفة المطروحة، سواء من القراءة أو الخبرة أثناء التطوير. أنا مدرك أنني لست خبيرًا وآخر ما أريده هو نشر معلومات خاطئة، لذا إذا كنت تعتقد أن هناك شيئًا غير صحيح، سأكون سعيدًا بإبلاغي بذلك، يمكنك الاتصال بي على تويتر، أو فتح مشكلة (issue) في هذا المستودع. شكرًا جزيلاً لتفهمك. :)
الهدف الرئيسي من هذه التقنية واضح، وهو إنهاء الخيط الحالي واستعادته قبل استئناف التنفيذ، ولكن ماذا يعني هذا بالضبط، وما القيود الجديدة التي يفرضها؟
لكي نتمكن من استعادة التنفيذ، نحتاج إلى حفظ شيئين قبل إنهاء الخيط: أولاً، حالة وحدة المعالجة المركزية (CPU state)، وثانيًا المكدس، ثم إعادة ضبطهما بشكل فعال بعد تشغيل الخيط الجديد.
تحدثت عن قيود جديدة ستظهر في هذه التقنية، وهناك اثنان رئيسيان: أولاً، نحتاج إلى تخزين أي شيء مطلوب خارج المكدس من لحظة إنهاء الخيط حتى استعادة المكدس، وكما سترى، هذا يخلق تحديات جديدة.
ثانيًا، نحتاج دائمًا إلى وجود خيط آخر على الأقل قيد التشغيل في عمليتنا، لأننا بإنهاء خيطنا، إذا لم تكن هناك خيوط أخرى، ستنتهي العملية. لا أعتقد أن هذه مشكلة كبيرة، نظرًا لأن معظم العوامل (agents) تُحقن في عمليات أخرى، يمكننا افتراض أن هذه العملية ستحتفظ بخيط واحد على الأقل قيد التشغيل.
يمكننا رؤية 4 وظائف أساسية في هذا الإثبات:
عندما نكون على وشك حفظ المكدس، يطرح سؤال: ما مقدار المكدس الذي يجب حفظه؟
دعنا نراجع أولاً ما هو موجود في المكدس بعد استدعائنا لوظيفة DeathSleep (هذه هي الوظيفة التي تحفظ السياق والمكدس وتعد كل شيء للإخفاء والاستعادة).

كما نرى، كل وظيفة تتكون من ثلاثة أجزاء:
الحد الأدنى من المكدس الذي نحتاج بالتأكيد لحفظه هو كل شيء داخل برنامجنا الرئيسي، وهذا يعني مساحة Shadow space الخاصة به، وعنوان العودة الخاص به، وكل شيء حتى وظيفة DeathSleep. أي شيء قبل ذلك ليس مطلوبًا حقًا (حفظ المكدس المستخدم بواسطة وظيفة الدخول له مزايا، لكننا سنناقش ذلك لاحقًا)، لأن هذا هو المكدس المستخدم بواسطة إجراءات Windows لتشغيل خيطنا الجديد. بالإضافة إلى ذلك، قررت أيضًا تخزين مساحة Shadow space الخاصة بوظيفة DeathSleep (ليس ضروريًا حقًا، لكنه يجعل حساب Rsp في لحظة الاستيقاظ أسهل).
لذا في النهاية، نحن نحفظ هذا:

كل وظيفة في الترجمة القياسية يجب أن تتكون من 3 أجزاء: المقدمة (prologue)، كود الوظيفة، والخاتمة (epilogue).
يجب تعديل Rsp (مؤشر المكدس) فقط في مقدمة وخاتمة الوظيفة. المقدمة تزيد مؤشر المكدس (تذكر أن زيادة المكدس تعني تقليل العناوين، لأنها تسير في اتجاهين متعاكسين)، لحفظ السجلات، لاحتواء جميع متغيراتها المحلية ثم لاحتواء Shadow space، والخاتمة تفعل العكس تمامًا.
هذا يعني أن مؤشر المكدس داخل كود الوظيفة يجب أن يشير دائمًا إلى نهاية Shadow space (الأرجواني في الصورة أعلاه)، ومجموع حجم مكدس الوظيفة و Shadow space يمكن العثور عليه عن طريق حساب مقدار زيادة مؤشر المكدس في المقدمة. يمكن حساب هذه القيمة بسهولة باستخدام المعلومات المحمولة في جداول فك اللف (unwind tables)، شرح استخدامها ليس شيئًا سنغطيه هنا، ولكن كملخص، تُستخدم هذه الجداول للسماح لأي خيط أو عملية أخرى بالتحرك بشكل صحيح عبر المكدس لرؤية محتوياته، أو للتعامل مع الاستثناءات أو تحليلها.
التقاط السياق هو على الأرجح أسهل شيء يمكن القيام به، حيث يمكننا ببساطة تنفيذ RtlCaptureContext() في أول سطر من DeathSleep، قبل أي تعديل على السجلات غير المتطايرة (non-volatile registers). لا يزال يتعين علينا إجراء تعديلين على السياق الذي سنستعيد فيه التنفيذ.
أول تعديل سيكون تعديل Rip، كما تتذكر، هذا هو السجل الذي يحمل التعليمات التالية للتشغيل، وإذا تركناه دون تعديل، فسيستأنف التنفيذ داخل وظيفة DeathSleep. ما سنفعله هو تغيير Rip ليحمل عنوان العودة الخاص بـ DeathSleep، والذي يشير إليه Rsp الحالي بعد إزاحة مقدار الزيادة في الخاتمة (المناطق الخضراء + الأرجوانية في الصور أعلاه).
التعديل الثاني سيتم عند استعادة الخيط، وهو يتضمن تعيين Rsp ليشير إلى أعلى المكدس المستعاد؛ سيتم ذلك خلال مرحلة الاستعادة، لأننا لا نعرف أين سيتم وضع مكدسنا الجديد. ستكون القيمة ببساطة هي عنوان نهاية المكدس المستعاد، لأنه كما ناقشنا لاحقًا، قمنا بنسخ أيضًا مساحة Shadow space المحجوزة بواسطة متصل DeathSleep، وهذه هي بالضبط قيمة RSP قبل استدعاء DeathSleep.