
كاشف وضع المستخدم الذي يلتقط استدعاءات النظام غير المباشرة. يحاصر Hell's Hall وTartarus' Gate وRecycledGate وVEH syscalls والعديد غيرها.
حقوق الطبع والنشر (C) 2026 Adam Zypherion <[email protected]> — مرخص تحت GPL-3.0.
الاستدعاءات النظامية غير المباشرة (Indirect syscalls) بسيطة بمجرد رؤية واحدة. المُحمِّل (loader) يجتاز ntdll، ويقرأ رقم SSN من مقطع Nt* التمهيدي، ويجد البايتين 0F 05 بعد ذلك بقليل، ويضبط r10/rax/rdx/r8/r9 بنفسه، ثم يقفز مباشرةً إلى تلك البايتين. يحدث الاستدعاء النظامي من داخل ntdll. لم يتم لمس خطافك (hook) على تصدير kernel32. لم يتم لمس خطافك على تصدير ntdll. يشير [RSP] في لحظة SYSCALL إلى صفحة RWX الخاصة بالمُحمِّل، لكن لا شيء في الاستدعاء يبدو غير معتاد من الخارج.
HellHall proc
mov r10, rcx
mov eax, dwSSN
jmp qword ptr [qAddr]
ret
HellHall endp
للاختلافات أسماء. يستخدم Tartarus' Gate و RecycledGate تعليمة syscall الخاصة بأحد المؤشرات (stubs) لكن SSN لآخر، لذا حتى لو سجلت اسم المؤشر الذي يُبلغ عنه خطافك، فهو كذب. يستخدم VEH syscall انتهاك وصول (access violation) عمدًا ويستخدم VEH الخاص به لإعادة كتابة السياق بحيث يهبط RIP على تعليمة syscall الخاصة بـ ntdll مع إعداد السجلات مسبقًا. لا يستخدم Hell's Gate ntdll على الإطلاق. يكتب المُحمِّل 0F 05 في صفحة RWX الخاصة به ويعمل من هناك.
الجواب على مستوى وضع kernel (KM) هو برنامج تشغيل (driver)، قد أصل إليه بالفعل مع الوقت وأصدر مشروعًا، لكن هذا لن يحدث قريبًا :D
يبدو PAGE_GUARD كما لو كان يجب أن يعمل. قم بتعليم الصفحة التي تحتوي على بايتات syscall بـ PAGE_GUARD، والتقط استثناء STATUS_GUARD_PAGE_VIOLATION في VEH، وافحص، وأعد التوجيه إلى ترامبولين خاص (private trampoline)، واضبط علامة المصيدة (trap flag)، وتخطَّ بخطوة واحدة (single-step out)، وأعد تسليح الصفحة. مصيدة واحدة لكل استدعاء Nt، بغض النظر عن كيفية وصول المُحمِّل إلى هناك. يعمل ضد كل شيء باستثناء Hell's Gate.
المشكلة أن نظام التشغيل لا يتعاون. PAGE_GUARD هو طلقة واحدة، ماذا يعني ذلك؟ في كل مرة يتم إطلاقها، يتم مسح البت وعليك إعادته. معالجك (handler) يقوم باستدعاءات نظامية (NtProtect لإعادة تعيين الحارس، NtContinue للاستئناف) وهذه الاستدعاءات النظامية لها مؤشرات stubs وتلك المؤشرات موجودة على الصفحة التي قمت بحراستها للتو. لقد عملت حول معظم ذلك باستخدام مؤشرات استدعاء نظامي خاصة (private syscall stubs) مبنية على صفحة منفصلة نملكها (خصص RWX، اكتب mov r10,rcx; mov eax,SSN; syscall; ret، قفل RX، لا تلمس ntdll's)، لكنها كانت هشة. كل إصدار ثانوي من Windows غيَّر التوقيت. كل خيط (thread) ينتجه النموذج كان سباقًا آخر ضد عامل النزاهة (integrity worker) الذي كان يعيد بناء الحارس. كان نجاح العرض التوضيحي حوالي 30 إلى 50 بالمائة عبر عشر مرات تشغيل على نفس الجهاز.
في النهاية توقفت عن محاولة إقناع Windows بأن PAGE_GUARD يجب أن يتصرف بالطريقة التي أريدها، وحاولت استبدال البايت بدلاً من ذلك.
https://github.com/user-attachments/assets/0cb670fd-e51b-413e-bf00-08f9297888ed
البايت في تعليمة syscall هو 0F 05. البايت الأول وحده (0F) هو البادئة لمجموعة من ثنائيات البايت opcode بما في ذلك SYSCALL و CPUID و RDTSC. بمفرده ليس قابلاً للتنفيذ؛ تحتاج وحدة المعالجة المركزية (CPU) إلى البايت الثاني لفك التشفير. إذا استبدلت البايت الأول بـ 0xCC (INT3)، يصبح الزوج CC 05، والذي تفك تشفيره وحدة المعالجة المركزية على أنه INT3 متبوعًا ببايت عشوائي لا تصل إليه أبدًا. أي مسار كود يهبط على ذلك العنوان يتسبب في EXCEPTION_BREAKPOINT.
هذه هي الآلية بأكملها. يكتشف VEH الخاص بنا نقطة التوقف (breakpoint)، ويبحث عن المؤشر الذي ينتمي إليه العنوان (نبني الخريطة في وقت التهيئة عن طريق تعداد تصديرات ntdll)، ويجري ثلاث فحوصات على من ظهر، ويضبط Context->Rip على ترامبولين خاص يقوم بالاستدعاء النظامي الحقيقي ويعود. تظل البايت CC. يضربها المستدعي التالي بنفس الطريقة، لذلك لا يوجد تبديل لحراسة الصفحة ذهابًا وإيابًا.
للتوضيح: نعم، هذا هو التعليق (hooking) عن طريق استبدال البايت. لكنه ليس في البايت الذي يعنيه الناس عادةً بـ "خطاف ntdll". الخطاف الكلاسيكي لـ EDR يستبدل أول بايتات المؤشر بـ JMP <my_func>:
ntdll!NtAllocateVirtualMemory:
E9 ?? ?? ?? ?? jmp my_hook ; overwrites "mov r10, rcx"
...
0F 05 syscall
C3 ret
هذا هو بالضبط ما تتجنبه الاستدعاءات النظامية غير المباشرة. يقرأ المُحمِّل SSN بنفسه ويقفز مباشرة إلى 0F 05، ولا يتم تشغيل المقطع التمهيدي (ولا قفزتك) أبدًا، ولا يتم تشغيل الخطاف أبدًا. ما نفعله هو استبدال بايت syscall نفسه:
ntdll!NtAllocateVirtualMemory:
4C 8B D1 mov r10, rcx ; untouched
B8 18 00 00 00 mov eax, 18h ; untouched
CC 05 int3 / 05 ; was 0F 05, we wrote CC over the 0F
C3 ret
الآن لا يهم كيف وصلت إلى هذا العنوان. من خلال المقطع التمهيدي، أو من خلال قفزة غير مباشرة تتجاوز المقطع التمهيدي، أو من خلال إعادة كتابة السياق VEH-syscall التي تضع RIP هناك، أيًا كان. إذا نفذت وحدة المعالجة المركزية تلك البايتة، فإنها تتعثر. نفس عائلة التقنية مثل الخطاف الكلاسيكي، موقع مختلف، تغطية مختلفة تمامًا.
يبدو الترامبولين كما يلي:
F3 0F 1E FA endbr64
49 89 CA mov r10, rcx
B8 <stub SSN> mov eax, ssn
0F 05 syscall
C3 ret
الفحوصات الثلاثة لم تتغير من نسخة PAGE_GUARD لأنها كانت الفحوصات الصحيحة الثلاثة؛ لقد احتاجت فقط إلى مكان موثوق للعيش فيه.
عنوان العودة. [RSP] هو المكان الذي كان سيعود إليه الاستدعاء النظامي. بالنسبة لاستدعاء حقيقي فهو داخل ntdll أو kernel32 أو kernelbase أو أحد مكتبات وقت التشغيل ذات الصلة. بالنسبة لـ Hell's Hall فهو داخل صفحة RWX التي يعمل منها المُحمِّل. لدينا قائمة قصيرة من أهداف العودة الموثوقة المبنية في وقت التهيئة عن طريق استدعاء GetModuleHandle لتلك الأسماء وقراءة نطاقات .text من رؤوس PE الخاصة بهم.
SSN. لقد تم تشغيل المقطع التمهيدي للمؤشر بالفعل قبل أن يصل إلى بايت syscall، لذلك يحمل eax أي قيمة تم تحميلها فيه. إذا قام المُحمِّل بتبديل Tartarus، فلن تتطابق تلك القيمة مع SSN الذي قرأناه من نفس هذا المؤشر في وقت التعداد. نسجل عدم التطابق، ويكتب الترامبولين SSN الصحيح قبل استدعائه النظامي الخاص، لذلك فإن وظيفة kernel التي يتم تشغيلها هي التي تنتمي إلى البايت بدلاً من تلك التي أرادها المُحمِّل. يتم تسجيل التقنية وتحييدها في نفس الخطوة.
تجوال المكدس (Stack walk). RtlVirtualUnwind من السياق الحالي، خمس إطارات لأعلى. يجب أن يكون RIP لكل إطار داخل وحدة معروفة ويجب أن يحتوي على إدخال RUNTIME_FUNCTION. يفشل الكود البرمجي (shellcode) وأدوات ROP في هذا حتى عندما يبدو [RSP] نفسه موثوقًا به، وهو ما يمكن للمُحمِّل تزويره (يمكنه التنبؤ تقريبًا بمكان وجود المستدعي الخاص به في الذاكرة وإنشاء عنوان عودة مزيف معقول هناك).
إذا فشل أي فحص، فإننا ندفع بنية صغيرة إلى حلقة خالية من القفل (lock free ring) ويقوم خيط التصريف (drain thread) بطباعتها في المرة التالية التي يستيقظ فيها
[!! hallwatch !!] استدعاء نظامي غير مباشر (مستدع غير موثوق، SSN خاطئ لهذا المؤشر)
syscall : NtAllocateVirtualMemory
syscall rip : 0x00007FF827660372
return addr : 0x00007FF7EED719D6
rax (ssn) : 0x0000000F (stub encodes 0x00000018)
thread : 26388
Hell's Gate هو المكان الذي يتوقف فيه INT3 عن المساعدة. لم نقم أبدًا بتصحيح صفحة RWX الخاصة بالمُحمِّل لأننا لم نكن نعرف بوجودها أبدًا.