
تقنية تنفيذ/حقن الأكواد باستخدام التلاعب ببنية وحدة DLL في PEB
تنفيذ كود خفي عبر تعديل EntryPoint للوحدات المحمّلة وقت التشغيل.
تمتلك عمليات Windows وحدات (modules) مختلفة محمّلة وقت التشغيل. لكل وحدة من هذه الوحدات دالة DllMain() معرفة، ويتم استدعاؤها عند إنشاء/إنهاء عملية أو مؤشر ترابط (أربعة سيناريوهات محتملة).
من أجل استدعاء هذه الدوال بشكل صحيح طوال عمر العملية، تشير دوال محمّل Windows (ntdll!Ldrp*) إلى قائمة من الإدخالات التي تحتوي على معاملات رئيسية (بما في ذلك حقل EntryPoint) لكل وحدة.
من خلال الكتابة فوق EntryPoint لوحدة DLL، نضمن إعادة توجيه تنفيذ الكود إلى مكان نختاره.
يمكن استخدام هذا كأداة بدائية لتنفيذ الكود (code execution primitive)، وللوساطة في استدعاءات API (API proxying)، أي لتشغيل بعض واجهات برمجة التطبيقات مع مكدس استدعاءات (callstack) غير مريب، حيث سيتم استدعاؤها بواسطة دوال Windows شرعية.
يمكن أيضًا استخدامه لبدء التنفيذ في عملية بعيدة، بشرط أن تكون لدى المهاجم القدرة على قراءة وكتابة الذاكرة في العملية الهدف. وبالمثل إلى Threadless Injection، يوفر هذا القدرة على تنفيذ كود في عملية دون استدعاء واجهات API التقليدية المرتبطة بالتنفيذ (CreateRemoteThread, QueueUserAPC).
إن تحميل/إلغاء تحميل الوحدات داخل عملية Windows موضوع معقد يطرح الكثير من التحديات، واحتمال عدم الاستقرار، وسباقات التوقيت (race conditions)، والتعطل. من العقبات المعروفة المتعلقة بتشغيل كود كجزء من دالة DllMain() مثلاً، حقيقة وجود قفل المحمّل (Loader Lock) وأننا نعمل في مؤشر ترابط لم يتم إعداده بالكامل، أو أنه في طور الإنهاء.
لذلك، حاولت توثيق ما هو ممكن وما هو غير ممكن بشكل صحيح. على سبيل المثال، في حين يمكن تنفيذ معظم استدعاءات API المعتادة، فإن تشغيل beacon كامل يتطلب أن يكون في عملية منفصلة، لتجنب حالات الجمود (deadlocks) الناجمة عن الدوال المستخدمة في wininet.dll أو winhttp.dll.
تحتفظ كل عملية بقائمة من بنى _LDR_DATA_TABLE_ENTRY وقت التشغيل. تحتوي هذه البنى على تفاصيل كثيرة تتعلق بوحدة DLL، مثل EntryPoint (الذي سنقوم بالكتابة فوقه)، واسمها، وبعض التجزئات (hashes)، والطوابع الزمنية، وعدة أعلام (flags)، وما إلى ذلك. بعض هذه البنى موثّق وبعضها غير موثّق.
يمكن تصور ذلك عبر أمر WinDbg التالي:
dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
+0x030 DllBase : 0x00007ffe`e87e0000 Void
+0x038 EntryPoint : 0x00007ffe`e8838d00 Void
+0x040 SizeOfImage : 0x2fe000
+0x048 FullDllName : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
+0x058 BaseDllName : _UNICODE_STRING "KERNELBASE.dll"
+0x068 FlagGroup : [4] "???"
+0x068 Flags : 0x8a2cc
+0x068 PackagedBinary : 0y0
+0x068 MarkedForRemoval : 0y0
+0x068 ImageDll : 0y1
(...)
+0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x0f8 OriginalBase : 0x00007ffe`e87e0000
+0x100 LoadTime : _LARGE_INTEGER 0x01db5f86`2fa735fc
+0x108 BaseNameHashValue : 0x235bec4
+0x10c LoadReason : 0 ( LoadReasonStaticDependency )
+0x110 ImplicitPathOptions : 0x4000
+0x114 ReferenceCount : 1
+0x118 DependentLoadFlags : 0x800
+0x11c SigningLevel : 0 ''
يمكن الحصول على عنوان البنية من خلال اجتياز بنية مرتبطة بشكل مضاعف (doubly-linked) مرجعية في PEB الخاص بالعملية داخل بنية PEB_LDR_DATA.
dt nt!_PEB_LDR_DATA 0xb4b4c3c3
لاحظ العلامة DontCallForThreads. كما يشير الاسم، إذا تم تعيين هذه العلامة، لن يقوم نظام التشغيل باستدعاء DllMain() لهذه الوحدة لأحداث المؤشرات (أي DLL_THREAD_ATTACH أو DLL_THREAD_DETACH).
عند إنشاء وحدة DLL، يجب اتباع القالب التالي للعمل جنبًا إلى جنب مع دوال محمّل نظام التشغيل:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break; // do not do cleanup if process termination scenario
}
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
كما هو موضح أعلاه، تقوم التقنية مؤقتًا بالكتابة فوق EntryPoint لوحدة DLL لإعادة توجيه التنفيذ. وبما أنه ليس لدينا أي تحكم يتجاوز إعادة توجيه التنفيذ، يجب اتخاذ بعض الترتيبات على الجانب الآخر للتعامل مع ما نريد تشغيله، وبأي وسائط، وكيفية استعادة القيمة المرتجعة.
يتم ذلك عبر تعريف بنية DATA_T في الكومة (heap)، بحيث تظل قابلة للوصول خلال الخطوات المختلفة.
تُعرَّف هذه البنية كما يلي:
typedef struct _DATA_T {
// LDR structures manipulation
ULONG_PTR runner; // malicious entry point to execute
ULONG_PTR bakOriginalBase; // backup of overwritten OriginalBase
ULONG_PTR bakEntryPoint; // backup of overwritten EntryPoint
HANDLE event; // event signalling that the Runner has executed
// function call
ULONG_PTR ret; // return value
DWORD createThread; // run this API call in a new thread (required for wininet/winhttp)
ULONG_PTR function; // Windows API to call
DWORD dwArgs; // number of args
ULONG_PTR args[MAX_ARGS]; // array of args
} DATA_T, * PDATA_T;
لإعداد تنفيذ API، يجب تجهيز هذه الحقول. قيمة ret هي التي ستجمع القيمة المرتجعة بعد التنفيذ. أما event فيُستخدم للمزامنة، للإشارة إلى اكتمال التنفيذ. جميع الحقول الأخرى هي مدخلات تحدد واجهة API المراد استدعاؤها (function)، وبأي وسائط (dwArgs و args[])، وعنوان دالة Runner() التي يُعاد توجيه التنفيذ إليها، ونسخ احتياطية لإدخالات DLL الأصلية التي تمت الكتابة فوقها (bakOriginalBase و bakEntryPoint).
يجب تعيين حقل createThread إلى 1 لوظائف API المعقدة التي لن تعمل بشكل جيد في إعداد DllMain() (وهذا يشمل العديد من مكتبات wininet و winhttp).
إليك مثال لإعداد استدعاء للدالة MessageBoxA() كما يظهر في الإثبات المفاهيمي (PoC):
pDataT->dwArgs = 4;
pDataT->runner = (ULONG_PTR)Runner;
pDataT->function = (ULONG_PTR)MessageBoxA;
pDataT->args[0] = (ULONG_PTR)0;
pDataT->args[1] = (ULONG_PTR)"Hello";
pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");
تتولى دالة UpdateLdr() مسؤولية إجراء التعديل الصحيح في _LDR_DATA_TABLE_ENTRY للوحدة الهدف.
ستقوم دالة RestoreLdr() باستعادة تلك التغييرات في مرحلة لاحقة (يتم استدعاؤها بواسطة Runner()).
تحدد هذه الدوال موقع PEB وتجتاز بنى الوحدات لتحديد وحدة DLL الصحيحة وحقولها. في ملفات الرؤوس، أعيد استخدام تعريفات استخدمها Batsec في DarkLoadLibrary، وأشجع القراء على الاطلاع على هذا المشروع ومقال MDSec المرتبط للاستفادة من العمل الرائع الذي قام به حول دواخل تحميل الوحدات في Windows.
ملاحظة: يقوم هذا الإثبات المفاهيمي بتحميل وحدة DLL تضحية (SACRIFICIAL_DLL_NAME) وينفذ تلك التغييرات على هذه الوحدة. ومع ذلك، من الممكن تمامًا تعديل وحدة DLL محمّلة بالفعل. في الواقع، هذا هو الأسلوب المتبع للحقن عبر العمليات. لأغراض الاستقرار، أوصي بتجنب التعامل مع وحدات DLL المهمة مثل ntdll أو kernel32، والتي تميل أيضًا إلى أن تكون أكثر تدقيقًا من قبل حلول الأمان.