
مكتبة مضادة لتصحيح الأخطاء (anti-debugging) خفية في مساحة المستخدم (userland) بلغة C/C++ تعتمد بالكامل على استدعاءات النظام (syscalls) لنظام Windows، مصممة لحماية البرمجيات من الهندسة العكسية
antidbg هي مكتبة مضادة للتنقيح (anti-debugging) تعمل في وضع المستخدم (user-mode) على نظام Windows بمعمارية x64، مصممة لحماية البرمجيات من التنقيح.
اعتبر المكتبة أساسًا لحماية برنامجك المضادة للتنقيح، وليست دفاعك الوحيد.
المكتبة:
يفترض نموذج التهديد أن المنقّح (debugger) قد يعترض البرمجيات المحمية بهذا النظام من أي مستوى صلاحيات، وتكون الكشوفات أقل فعالية كلما ارتفع مستوى الصلاحيات.
تم تحصين هذا البرنامج لتجاوز الاعتراض البسيط عند CPL > 0، من خلال:
RAX عبر كشف و/أو الكتابة فوق ردود نداء الأدوات غير الشرعية..text المراقَبة عبر إجراء مقارنة على القرص مقابل في الذاكرة.VEH، SEH، LPTOP_LEVEL_EXCEPTION_FILTER) ونقاط التوقف البرمجية.PAGE_GUARD.TLS callbacks من ارتباط المنقّح، وإجراء فحص لعنوان بداية الخيط.DbgBreakPoint وDbgUiRemoteBreakin، لتحطيم العملية أو العودة.إذا كانت الطريقة الوحيدة لإجراء فحص ما هي استخدام دالة مُصدَّرة غير قابلة للاستدعاء عبر syscall، فإن الدالة المعنية تُهندَس عكسيًا يدويًا وتُعاد بناؤها لتعمل في مساحة عنوان الوحدة النمطية لخيط الحماية. تُستعرض جميع بنى الذاكرة في وضع المستخدم عبر استبطان الذاكرة المباشر بدلًا من استخدام واجهات API.
تترك روتينات الحماية صراحةً بعض مسارات التنفيذ دون حماية ضد خطافات وضع المستخدم، لتكون بمثابة مصائد ذاكرة (memory honeypots). تُستخدم لمقارنة الحالة ولإرباك المهاجمين.
تعمل روتينات الحماية بشكل شبه عشوائي. تُحدَّد العشوائية عبر ASLR المعتمد على العتاد الخالص، وسلوك المكدس، وقليل من الرياضيات؛ دون استدعاء النواة، أو استخدام واجهات API في وضع المستخدم، أو إصدار تعليمات خروج شرطية أو غير شرطية من قِبل الـ hypervisors.
عند كشف انتهاك أمني، تُحطّم منظومة الحماية العملية الحالية عبر استدعاء INT 29h مع STATUS_SXS_EARLY_DEACTIVATION، متجاوزةً جميع معالجات الاستثناءات. في بعض الحالات، تُدرج أيضًا APC في النواة لإنهاء العملية الحالية.
توجد في نقطة الدخول الرئيسية (abdg.c) لهذه المكتبة، مشروحة بالترتيب.
شروحات موجزة؛ قد يُجري كشف معيّن فحوصات إضافية/فرعية أكثر مما هو مشروح هنا.
يمكنك العثور على مزيد من الشيفرة المصدرية لمفاهيم كشف أخرى في مجلد antidebug\archived.
1. يقرأ حقل BeingDebugged في PEB باستخدام دالة التصدير الخاصة بـ kernel32.2. يستدعي التصدير IsRemoteDebuggerPresent لمعرفة ما إذا كانت العملية الهدف تُنقَّح من خارج سياقها الخاص.3. ينفّذ المقاطعة البرمجية INT 2D، متحققًا مما إذا كان البايت التالي لهذه التعليمات يُتخطّى ولا يُوجَّه عبر معالج EXCEPTION_BREAKPOINT.4. ينفّذ مقاطعة نقطة التوقف INT 3D، ثم يراقب ما إذا كان الاستثناء يُعترَض أو يمرّ بشكل طبيعي.5. يستدعي ICE/0xF1، رافعًا EXCEPTION_SINGLE_STEP ومتحققًا مما إذا كان المنقّح سيعتبر هذا الاستثناء هو الاستثناء الطبيعي الناتج عن تنفيذ التعليمات مع ضبط بت الخطوة الواحدة في سجلات RFlags.مثال:
#include "adbg.h"
int main() {
StartDebugProtection();
return 0;
}
مثال:
#include "adbg.h"
int main() {
if (isProgramBeingDebugged()) {
printf("Debugger detected.\n");
}
else {
printf("No debugger was detected.\n");
}
return 0;
}
لبناء الملف التنفيذي لمشغّل الاختبار، فعّل -DBUILD_EXAMPLE=ON. هذا يجمّع example/main.c ويربطه بـ antidebug دون تلويث المكتبة الأساسية بنقطة دخول.
.sln المُولَّد).antidebug_runner كمشروع بدء التشغيل وانقر على Build (أو اضغط F5 للتشغيل).من جذر المشروع:
cmake -B build -S . -DBUILD_EXAMPLE=ON
cmake --build build --config Release
سيكون الملف التنفيذي في:
build/Release/antidebug_runner.exebuild/antidebug_runner.exeملاحظة حول إصدارات التنقيح (Debug Builds): التجميع في وضع التنقيح (
--config Debug) يمكّن سجلات التشخيص في وحدة التحكم/المنقّح عبرcore/debug.c. أما وضع الإصدار (Release) فيزيل التسجيل بالكامل.
بشكل افتراضي، يُنتج CMake مكتبة ثابتة (antidebug.lib أو libantidebug.a). لبناء مكتبة ربط ديناميكية (DLL)، مرّر -DBUILD_SHARED_LIBS=ON.
cl.exe)باستخدام مولّد Visual Studio:
# Static Library (.lib)
cmake -B build -S . -A x64 -DBUILD_SHARED_LIBS=OFF
cmake --build build --config Release
# Dynamic Library (.dll + import .lib)
cmake -B build -S . -A x64 -DBUILD_SHARED_LIBS=ON
cmake --build build --config Release
باستخدام Ninja مع clang-cl:
# Ensure clang-cl is in your PATH or run from the VS x64 Native Tools Command Prompt
cmake -B build -S . -G Ninja -DCMAKE_C_COMPILER=clang-cl -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
cmake --build build
شغّل صدفة LLVM-MinGW بمعمارية 64 بت (x86_64-w64-mingw32-clang في PATH الخاص بك) واستخدم Ninja:
# Static Library (.a)
cmake -B build -S . -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
cmake --build build
# Shared Library (.dll)
cmake -B build -S . -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON
cmake --build build
لتثبيت المكتبة المُجمَّعة والترويسات في بادئة محلية:
cmake --install build --prefix "C:/local/antidebug"
ينتج عن ذلك:
C:/local/antidebug/
├── bin/
│ └── antidebug.dll (if BUILD_SHARED_LIBS=ON)
├── lib/
│ └── antidebug.lib (or libantidebug.a)
└── include/
└── antidebug/
├── adbg.h
└── ...
لست مسؤولًا ولا أتحمل أي مسؤولية عن أي ضرر تسببه من خلال أي استخدام ضار لهذا المشروع.
تم إنشاء وثائق BUILD باستخدام الذكاء الاصطناعي، أبلغ عن أي مشكلات.
الترخيص: MIT
6. يستكشف سجلات مقطع المكدس عبر ضبط علم TF والتحقق مما إذا كان المنقّح يمسحه من RFLAGS، إذ تمسح المنقّحات عادةً علم الفخ (trap flag) بعد تسليم كل حدث من أحداث المنقّح.7. يستخدم حالة حدّية في تدفق التعليمات قائمة على البادئة ويتحقق مما إذا كانت 0xF3 0x64، التي تُفكّك كـ PREFIX REP، تفرض تخطّي 0xF1.8. يتحقق مما إذا كان بعد ضبط علم الفخ واستدعاء pushfd mov dword ptr [esp], 0x100 popfd nop، يتم الوصول إلى nop بدلًا من الدخول إلى معالج EXCEPTION_SINGLE_STEP.9. يرفع حدثي DBG_CONTROL_C وDBG_RIPEXCEPTION لمعرفة ما إذا كان الاستثناء يُعترَض ولا يُوجَّه عبر SEH.10. يتحقق من وجود مقبض كائن تنقيح مرتبط، مستعلمًا عن ProcessDebugObjectHandle عبر NtQueryInformationProcess.11. يستعلم عن وجود منقّح نواة باستخدام SystemKernelDebuggerInformation عبر NtQuerySystemInformation، ويقرأ مباشرةً صفحة الذاكرة KUSER_SHARED_DATA للحقل KdDebuggerEnabled. بالإضافة إلى ذلك، يتحقق مما إذا كانت مقاطعات مؤقت النواة (kernel timer ISRs) تدق بشكل غير متزامن.12. يقرأ علم NT العام لقناع من FLG_HEAP_ENABLE_TAIL_CHECK (0x10) وFLG_HEAP_ENABLE_FREE_CHECK (0x20) وFLG_HEAP_VALIDATE_PARAMETERS (0x40)13. يفحص ProcessDebugFlags، لاستنتاج ما إذا كان التنقيح ممكّنًا أو مكبوتًا.14. يضاعف مقابض العملية ويتحقق مما إذا كان المنقّح يلمس المقابض، أو يورّثها، أو يعيد فتحها / يضاعفها؛ هل يمكنني إنشاء مقبض مكرّر محمي، ثم مضاعفته مرة أخرى بشكل نظيف؟15. يفحص سلسلة العملية الأب لكشف مشغّلات المنقّح أو الأنساب المشبوهة مثل vsjitdebugger أو x64dbg أو ما شابه.16. يتحقق من حقول PEB الخاصة بالتنقيح دون استخدام التصديرات، بالقراءة المباشرة من القاعدة (__readgsqword(0x60)) عند الإزاحة *(BYTE*)((uintptr_t)peb + 2.17. يستعلم عن ProcessDebugPort للعملية الحالية.18. يتحقق من نقاط التوقف العتادية عبر فحص سجلات تنقيح الخيوط (Dr0–Dr7).19. يتحقق مما إذا كانت الذاكرة الافتراضية قد تأثرت بالمنقّحات عبر وضع مصائد ومراقبة التغييرات.20. يُجري اختباري إغلاق مقبض غير صالح باستخدام مقابض العملية والنافذة، ويراقب ما إذا كان ERROR_INVALID_WINDOW_HANDLE وEXCEPTION_INVALID_HANDLE لا يُعترَضان.21. يتحقق مما إذا كانت كائنات التنقيح تُعترَض من قِبل المنقّح وما إذا كان تجريد المقابض (handle stripping) يحدث.22. يحاول فتح عملية بطريقة تكشف ما إذا كان الوصول يُفلتَر أو يُعاد توجيهه من قِبل المنقّحات.23. يتحقق مما إذا كان يمكن إغلاق مقبض mutex مُعلَّم بـ HANDLE_FLAG_PROTECT_FROM_CLOSE مباشرةً.24. يستدعي NtSystemDebugControl مع SysDbgGetTriageDump ويتحقق مما إذا كان منقّح النواة يحجب الاستدعاء أو يزوّره لكنه لا يلمس مخزن الذاكرة الخاص بنا.25. يتحقق مما إذا كانت قراءات الذاكرة لمكدسنا الخاص تُستَخدم بأدوات أو تُعترَض.26. يتحقق مما إذا كانت العملية داخل كائن مهمة (job object) غير مُدرَج في القائمة البيضاء أنشأه منقّح.27. يستخدم اختبار وصول بأسلوب نقطة توقف الذاكرة، متوقعًا عادةً سلوك page-guard أو خطأً إذا كانت نقاط المراقبة (watchpoints) نشطة.28. يُطلق سيناريو نقطة توقف استثناء الصفحة ويفحص ما إذا كانت سلسلة الاستثناءات تسلّم STATUS_GUARD_PAGE_VIOLATION بشكل صحيح.29. يقيس توقيت التنفيذ لكشف الحمل الإضافي الذي تُدخله الخطوة الواحدة، أو نقاط التوقف، أو أدوات ثنائية ديناميكية/JIT recompilation.30. يبحث عن نوافذ المنقّح أو عناصر واجهته عبر تعداد النوافذ/الأصناف/العناوين المرتبطة بأدوات التنقيح.31. يتحقق مما إذا كان المنقّح يمسح بتات LBR/BTF المضبوطة سابقًا في DR7 لإجراء خطوته الواحدة الخاصة، مما يؤدي إلى مصفوفة ExceptionInformation فارغة، أو ما إذا كانت عناوين التفرّع في وضع النواة تُكتشَف إذا قرّر المنقّح إبقاء LBR ممكّنًا لكنه لا يزال يعترض EXCEPTION_SINGLE_STEP المستدعى من icebp.32. يمشي عبر الكومة (heap) مباشرةً ويتحقق من القيم السحرية 0xABABABAB و0xFEEEFEEE. فعليًا نفس الفحص 12 لكن باستخدام واجهات Heap APIs القابلة للخطف.33. يتحقق مما إذا حدث Copy-On-Write في الذاكرة الافتراضية عبر التحقق مما إذا كانت الصفحة المشتركة سابقًا قد لمسها منقّح.34. يرسل حدث وحدة تحكم (CTRL_C_EVENT) ويتحقق مما إذا كان منقّح يعترضه ويغيّر تسليمه إلى معالج التحكم الخاص بنا، أو يرفع DBG_CONTROL_C.35. يتحقق مما إذا كانت العملية معلّقة خارجيًا لمحاولات الحقن؛ يكشف أي استدعاء خارجي لـ NtResumeProcess يشير إلى عمليتنا.36. يستدعي NtSetDebugFilterState بمستويات صلاحيات SE_DEBUG_PRIVILEGE مختلفة ويتحقق مما إذا كان منقّح النواة يتعامل مع الوصول بشكل غير صحيح.37. يحلل كائنات الأجهزة، ويتحقق أيضًا مما إذا كان منقّح النواة يعترض قراءة الملف.38. يجعل الخيوط تتسابق ضد كلٍّ من منقّح النواة والنواة نفسها في قراءة بنية ContextFlags؛ يتحقق مما إذا كان DEBUG_REGISTERS مجرّدًا/إذا لم يُضبط Dr0.39. يجمّد بعض المنقّحات عبر إنشاء وتعيين عرض ضخم جدًا لقسم افتراضي؛ يكشف ما إذا كانت الاستدعاءات لـ NtMapViewOfSection قد عُبث بها.40. يتحقق من استدعاءات النظام غير المنفّذة (شائعة في المحاكيات).41. يضبط أربع نقاط توقف تنفيذ عتادية من DR0 إلى DR3 على أربع تعليمات NOP متتالية ويعدّ عمليات تسليم EXCEPTION_SINGLE_STEP الناتجة عبر VEH.42. يختبر تورّط المنقّح عبر الآثار الجانبية لـ OutputDebugString على Windows XP/2000 القديمة وعبر استثناءات DBG_PRINTEXCEPTION_{C,WIDE_C} التي يمكن للمنقّح اعتراضها.43. يتحقق مما إذا كان البايت الأول لصورتنا على القرص هو 0xCC ويستغل سلوك مقبض ملف محمّل Windows حيث يمكن لـ LoadLibrary أن يترك الملف قابلًا للوصول بشكل غير حصري تحت منقّح.