
محرك ربط (Hooking) بنقاط الإيقاف العتادية لنظام ويندوز، يستخدم سجلات التصحيح (Debug Registers) لاعتراض الدوال، وتجاوز ETW/AMSI، والتهرب من مراقبة EDR على مستوى المستخدم.
كان هذا المقال في الأصل منشورًا في VX-Underground Black Mass Halloween Edition 2022.
محركات الخطاف (Hooking Engines):
تقنية تهرب عامة في مساحة المستخدم (user-land) لمعمارية x64 تعتمد على سجلات التصحيح:
أمثلة على خطافات ETW/AMSI متاحة
مهمتنا هي ربط الدوال ببساطة وتحويل مسار تدفق الكود كما يلزم، وأخيرًا إزالة الخطاف بمجرد زوال الحاجة إليه.
لا يمكننا الاعتماد على خطافات IAT لأنها لا تُستدعى دائمًا، وبالتالي فهي غير موثوقة. يُعد الربط المضمّن (inline hooking) تقنية قوية؛ غير أنه يتطلب ترقيع الذاكرة التي يقع فيها الكود. إنها تقنية قوية، لكن أدوات مثل PE-Sieve وMoneta تستطيع تمييز الفرق بين النسخة المقيمة في الذاكرة والنسخة الموجودة على القرص من أي وحدة (module)، والإشارة إلى ذلك. وهذا يتركنا مع الأداة المثالية للمهمة: سجلات التصحيح (Debug Registers)، رغم أنها لا تحظى بالتقدير الكافي من مؤلفي البرمجيات الخبيثة!
في Windows، وبنظرة عامة عالية المستوى، تُعد العملية (process) في جوهرها تغليفًا للخيوط (threads)، ويحتفظ كل خيط من هذه الخيوط بسياق (context) يمثّل حالة الخيط: السجلات والمكدس (stack) وما إلى ذلك. تُعد سجلات التصحيح موردًا بصلاحيات مميزة (privileged)، وكذلك ضبطها؛ غير أن Windows يوفّر استدعاءات نظام (syscalls) متنوعة تسمح لنا بأن نطلب من النواة (kernel) القيام بإجراء بصلاحيات مميزة بالنيابة عنا؛ ويشمل ذلك ضبط سجلات التصحيح وهو أمر مثالي بالنسبة لنا. تُتيح كل من NtSetThreadContext وNtGetThreadContext إمكانية تعديل أي سياق خيط يمكننا فتح مقبض (handle) له بالصلاحية اللازمة. يمكننا أن نرى كيفية ضبط سجلات التصحيح باستخدام Win32 API.```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
هناك 8 سجلات تصحيح (Debug registers)، من Dr0 إلى Dr7. التي تهمنا هي فقط
Dr0-3 حيث نخزن العناوين التي نريد التوقف عندها، وDr6 هو مجرد حالة
التصحيح. الأهم هو Dr7، الذي يصف شروط نقاط التوقف التي سيرمي
المعالج استثناءً عندها. هناك قيود مختلفة عند استخدام سجلات التصحيح،
مثل عدد محدود (4) وعدم تطبيقها على جميع الخيوط/الخيوط المنشأة حديثًا.
سنسعى إلى معالجة بعض هذه القيود!
عندما يتم رمي الاستثناء، سيبحث عن معالج استثناء يمكننا تعريفه
وتسجيله في برنامجنا [1]. في معالج الاستثناء المعرّف لدينا، نريد أن يتم تشغيل الكود المرتبط
(مسارات كود مختلفة) عندما يتم تفعيل نقطة التوقف المقابلة.```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
يتم تحقيق ذلك عبر دالة مُنشِئة تحدد الربط بين دالة لامدا "callback" وعنوان. using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
يجب علينا التكرار عبر جميع مؤشرات ترابط العملية وتعيين التعديلات المقابلة
للسياق لكل منها. يمكن تحقيق ذلك باستخدام الدوال المساعدة ToolHelp32:
CreateToolhelp32Snapshot، وThread32Next. هذا ليس شيئًا معقدًا، لكنه يعالج أحد
قيودنا المتمثلة في عدم الارتباط بجميع مؤشرات الترابط.```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
وجود نقاط توقف الأجهزة (hardware breakpoints) مضبوطة قد يكون مريبًا إلى حد ما، فقد تشير إلى نشاط ضار (مع أنّه لا توجد أي EDRs، على حد علمي، تفحصها بنشاط). يمكن استخدامها ضدنا كمؤشر IoC محتمل، لذا يجب علينا إزالة آثارها بمجرد انتهائنا من استخدامها.
يمكننا تنفيذ ذلك في دالة deconstructor الخاصة بنا!! ستقوم بالتكرار عبر جميع الخيوط (threads) والتحقق مما إذا كان السجل (&context.Dr0)[pos] يشير إلى العنوان الذي قمنا بتعيين نقطة توقف الأجهزة عنده مبدئيًا (pos هو مجرد فهرس % 4 يمنحنا الوصول إلى context.Dr0-Dr3). يمكننا أيضًا إزالة الشروط المطلوبة في سجل Dr7. يجب علينا أيضًا تذكر إزالة إدخال التعيين الخاص بنا. وبالتالي، فإن نقطة توقف الأجهزة لدينا ستكون موجودة فقط للمدة المطلوبة!```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
مثال على نقطة توقف الأجهزة سيكون Sleep، حيث نستبدل مدة السكون
بـ 0.```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
نعلم أنه يتعين علينا تعيين RCX بسبب اصطلاح الاستدعاء السريع رباعي السجلات لويندوز x64[1]. الوسيطة الأولى للمُنشئ هي العنوان الذي سيتم التوقف عنده، والثانية هي أي سجل من Dr0- 3 سيتم التخزين فيه (لاحظ أنه يمكننا فقط أن يكون لدينا 4 عناوين للتوقف عندها في وقت واحد)، و الثالثة هي دالة لامدا ستلتقط بالمرجع PEXCEPTION_POINTERS التي هي المعلومات التي سيستلمها معالج الاستثناء. سيسمح لنا هذا في النهاية بالتحكم في تدفق البرنامج بشكل مختلف اعتمادًا على نقطة التوقف التي تم تشغيلها.
عند إنشاء مؤشر ترابط جديد، فإنه لا يرث مجموعة سجلات التصحيح المرتبطة به إلا إذا تمكنا بطريقة ما من اعتراض إنشاء مؤشر ترابط جديد! إحدى الحيل الأنيقة التي يمكننا استخدامها هي التقاط عنوان البداية الفعلي وتحويل مؤشر الترابط الجديد لإنشاء مؤشر ترابط خاص بنا. معظم مؤشرات الترابط الجديدة ينتهي بها الأمر باستدعاء NtCreateThreadEx.```c // Global Variable PVOID START_THREAD{ 0 };