
कोड निष्पादन/इंजेक्शन तकनीक DLL PEB मॉड्यूल संरचना हेरफेर का उपयोग करते हुए
लोड किए गए मॉड्यूल के EntryPoint को रनटाइम पर संशोधित करके गुप्त कोड निष्पादन।
Windows प्रक्रियाओं में रनटाइम पर विभिन्न मॉड्यूल लोड होते हैं। इनमें से प्रत्येक मॉड्यूल में एक DllMain() फ़ंक्शन परिभाषित होता है, जो प्रक्रिया या थ्रेड निर्माण/विनाश (चार संभावित परिदृश्य) पर आह्वान किया जाएगा।
प्रक्रिया के जीवनकाल के दौरान उन फ़ंक्शन को उचित रूप से कॉल करने के लिए, Windows लोडर फ़ंक्शन (ntdll!Ldrp*) प्रत्येक मॉड्यूल के लिए प्रमुख पैरामीटर ( EntryPoint फ़ील्ड सहित) वाली एंट्रीज़ की एक सूची का उल्लेख करेंगे।
एक DLL के लिए इस EntryPoint को अधिलेखित करके, हम सुनिश्चित करते हैं कि कोड निष्पादन हमारी पसंद के स्थान पर पुनर्निर्देशित हो जाएगा।
इसका उपयोग कोड निष्पादन प्रिमिटिव के रूप में और API प्रॉक्सीइंग के लिए किया जा सकता है, यानी कुछ API को गैर-संदिग्ध कॉलस्टैक के साथ चलाने के लिए क्योंकि उन्हें वैध Windows फ़ंक्शन द्वारा आह्वान किया जाएगा।
इसका उपयोग रिमोट प्रक्रिया में निष्पादन ट्रिगर करने के लिए भी किया जा सकता है, बशर्ते हमलावर के पास इस लक्ष्य प्रक्रिया पर मेमोरी पढ़ने और लिखने की क्षमता हो। Threadless Injection के समान, यह बिना निष्पादन से संबंधित क्लासिक API (CreateRemoteThread, QueueUserAPC) को आह्वान किए प्रक्रिया में कोड निष्पादित करने की क्षमता प्रदान करता है।
Windows प्रक्रिया के अंदर मॉड्यूल का लोड/अनलोड करना एक जटिल विषय है जो कई चुनौतियाँ, अस्थिरता, रेस कंडीशन और क्रैश की संभावना प्रस्तुत करता है। उदाहरण के लिए, DllMain() फ़ंक्शन के भाग के रूप में कोड चलाने से संबंधित एक प्रसिद्ध बाधा इस तथ्य में निहित है कि Loader Lock सक्रिय है और हम एक ऐसे थ्रेड में चल रहे हैं जो पूरी तरह से सेटअप नहीं हुआ है, या जो समाप्त होने की प्रक्रिया में है।
इसलिए, मैंने उचित रूप से दस्तावेज़ करने का प्रयास किया है कि क्या संभव है और क्या नहीं। उदाहरण के लिए, जबकि अधिकांश सामान्य API कॉल किए जा सकते हैं, एक पूर्ण बीकन चलाने के लिए अलग प्रक्रिया में होने की कुछ आवश्यकताएँ होती हैं, ताकि wininet.dll या winhttp.dll में उपयोग किए जाने वाले फ़ंक्शन के कारण होने वाले डेडलॉक से बचा जा सके।
प्रत्येक प्रक्रिया रनटाइम पर _LDR_DATA_TABLE_ENTRY संरचनाओं की एक सूची बनाए रखती है। ये संरचनाएँ DLL से संबंधित कई विवरण जैसे इसका EntryPoint (जिसे हम अधिलेखित करेंगे), इसका नाम, कुछ हैश, टाइमस्टैम्प, विभिन्न फ्लैग आदि रखती हैं। इनमें से कुछ संरचनाएँ दस्तावेज़ीकृत हैं और कुछ नहीं।
इन्हें इस 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 ''
संरचना का पता प्रक्रिया PEB में संदर्भित एक दोगुनी-लिंक संरचना (PEB_LDR_DATA संरचना में) को चलकर प्राप्त किया जा सकता है।
dt nt!_PEB_LDR_DATA 0xb4b4c3c3
DontCallForThreads फ्लैग पर ध्यान दें। जैसा कि नाम इंगित करता है, यदि वह फ्लैग सेट है, तो OS उस मॉड्यूल के DllMain() को थ्रेड इवेंट (जैसे DLL_THREAD_ATTACH या DLL_THREAD_DETACH) के लिए कॉल नहीं करेगा।
DLL बनाते समय, OS लोडर फ़ंक्शन के साथ हाथ से काम करने के लिए निम्नलिखित टेम्पलेट का पालन करना होगा:
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.
}
जैसा कि ऊपर बताया गया है, यह तकनीक निष्पादन को पुनर्निर्देशित करने के लिए अस्थायी रूप से एक DLL के EntryPoint को अधिलेखित करती है। चूंकि हमारे पास निष्पादन के पुनर्निर्देशन से अधिक किसी चीज़ पर नियंत्रण नहीं है, इसलिए हमें अपनी ओर से कुछ व्यवस्थाएँ करनी होंगी कि हम क्या चलाना चाहते हैं, किस तर्क के साथ, और रिटर्न वैल्यू कैसे वापस प्राप्त करें।
यह हीप पर एक DATA_T संरचना परिभाषित करके किया जाता है, इस तरह से कि यह विभिन्न चरणों के दौरान सुलभ रहे।
वह संरचना इस प्रकार परिभाषित की गई है:
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 फ़ील्ड को उन जटिल API फ़ंक्शन के लिए 1 पर सेट करने की आवश्यकता है जो DllMain() सेटअप में अच्छी तरह से नहीं चलेंगे (इसमें कई wininet और winhttp लाइब्रेरी शामिल हैं)।
यहाँ PoC में दिखाई देने वाले MessageBoxA() के लिए कॉल सेट करने का एक उदाहरण है:
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 में मॉड्यूल लोडिंग के आंतरिक भागों पर उनके द्वारा किए गए उत्कृष्ट कार्य से लाभ उठाया जा सके।
नोट: यह PoC एक बलि DLL (SACRIFICIAL_DLL_NAME) लोड करता है और इस DLL पर उन परिवर्तनों को करता है। हालाँकि, पहले से लोड किए गए DLL को संशोधित करना पूरी तरह से संभव है। वास्तव में, यह क्रॉस-प्रोसेस इंजेक्शन के लिए अपनाया गया दृष्टिकोण है। स्थिरता उद्देश्यों के लिए, मैं ntdll या kernel32 जैसे महत्वपूर्ण DLL को छूने से बचने की सलाह दूँगा, जो सुरक्षा समाधानों द्वारा अधिक जाँच-पड़ताल किए जाते हैं।
थ्रेड निर्माण या विनाश पर, निष्पादन Runner() को पुनर्निर्देशित किया जाता है, जो मॉड्यूल के लिए नकली DllMain() के रूप में कार्य करता है। यह फ़ंक्शन तब:
PDATA_T संरचना) स्थित हैRestoreLdr() के माध्यम से उसकी मूल स्थिति में पुनर्स्थापित करता हैDllMain() कॉल करता है (मूलतः सामान्य DLL कॉल को प्रॉक्सी करना)।इस बिंदु पर, "सामान्य" OS निष्पादन किया जा चुका है। फिर यह हमारे पेलोड के साथ जारी रहता है:
DATA_T संरचना पर संग्रहीत मानों और तर्कों के अनुसार हमारी दुर्भावनापूर्ण API कॉल निष्पादित करता है। यदि इस API को एक नए थ्रेड में चलाने के लिए चिह्नित किया गया है (createThread = 1), तो यह कॉल एक नए थ्रेड में किया जाएगा।pDataT->event) को संकेत देता है ताकि हमारा मुख्य कोड जान सके कि कॉल किया जा चुका है।जब Windows हमारे नकली EntryPoint (जो Runner() फ़ंक्शन पता है) को आह्वान करता है, तो कॉलस्टैक इस प्रकार दिखता है:

प्रदान किया गया PoC MessageBoxA() को आह्वान करने वाला एक उदाहरण शामिल करता है।
इसमें wininet का उपयोग करके HTTP डाउनलोड का एक प्रदर्शन भी शामिल है। उस कोड को सक्षम करने के लिए HTTP चर परिभाषित करें।

ऊपर उल्लिखित सिद्धांत प्रक्रिया मेमोरी स्पेस में पढ़ने और लिखने तक सीमित हो जाते हैं, ताकि भविष्य में किसी मनमाने समय पर कोड निष्पादन हो सके।
कुछ ट्वीक्स के साथ, इन रीड और राइट ऑपरेशंस को एक रिमोट प्रक्रिया पर लागू किया जा सकता है ताकि उसके एक DLL के EntryPoint को अधिलेखित किया जा सके।
एक पूर्व-आवश्यकता प्रक्रिया मेमोरी स्पेस में पढ़ने और लिखने की क्षमता है, अर्थात:
OpenProcess(PROCESS_VM_READ, FALSE, dwPid) and OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)
PoC में एक अतिरिक्त प्रोजेक्ट मौजूद है, जिसे LdrInject कहा जाता है, जो इन चरणों को करने का तरीका प्रदर्शित करता है। संक्षेप में, यह निम्नलिखित करता है:
ReadPEB() में, यह लक्ष्य प्रक्रिया में _LDR_DATA_TABLE_ENTRY सूची को चलता है ताकि अधिलेखित करने के लिए उपयुक्त DLL की पहचान की जा सके। ध्यान दें कि इस DLL का DontCallForThreads == 0 होना चाहिए क्योंकि हम चाहते हैं कि Windows थ्रेड निर्माण पर उस DLL के EntryPoint को आह्वान करे। हम सूची के पहले DLL भी नहीं चुन रहे हैं क्योंकि वे सुरक्षा उत्पादों द्वारा अधिक जाँच-पड़ताल किए जाते हैं (ntdll.dll, kernel32.dll...).
उस DLL के विवरण एक PEBINJ_DATA डेटा संरचना में संग्रहीत किए जाते हैं।
शेलकोड (इस मामले में एक बीकन) को InjectShellcodeToRemoteProcess() के साथ रिमोट प्रोसेसस्पेस में लिखा जाता है।
दो WriteProcessMemory() कॉल DLL के EntryPoint को अधिलेखित करते हैं और इसे OriginalBase में बैकअप करते हैं ताकि बाद में इसे पुनर्स्थापित किया जा सके।
उस बिंदु पर, अगला DLL_THREAD_ATTACH या DLL_THREAD_DETACH इवेंट शेलकोड के आह्वान का परिणाम देगा। बीकन चलाने के संदर्भ में यह कुछ सीमाओं और चेतावनियों के साथ आता है, जिनका विवरण अगले भाग में दिया गया है।
यह तकनीक एक बहुत ही विशिष्ट स्थिति में शेलकोड के निष्पादन का परिणाम देती है। Loader Lock सक्रिय है (क्योंकि OS मानता है कि वह DLL लोड/अनलोड करने की प्रक्रिया में है); एक थ्रेड या तो बनाया जा रहा है या नष्ट किया जा रहा है; और सामान्यतया, थ्रेड सिंक्रोनाइज़ेशन समस्याएँ, डेडलॉक आदि की संभावना है।
परीक्षण के दौरान, दो चुनौतियाँ देखी गई हैं:
एक सामान्य Cobalt Strike बीकन चलाने पर wininet.dll या winhttp.dll में एपीआई का उपयोग करते समय डेडलॉक होगा।
थ्रेड विनाश पर चलने से स्थिरता संबंधी समस्याएँ होती हैं क्योंकि हम एक ऐसे थ्रेड में चल रहे हैं जो नष्ट होने की प्रक्रिया में है।
स्थिरता बढ़ाने के लिए, हमें:
सुनिश्चित करें कि बीकन एक नए थ्रेड में चलेगा। इसलिए, UDRL सामान्य Cobalt Strike रिफ्लेक्टिव DLL एंट्री पॉइंट को आह्वान करने से पहले CreateThread करेगा।
केवल बनाए जा रहे थ्रेड में चलाएँ, न कि मर रहे थ्रेड में। ऐसा करने के लिए, हम यह सुनिश्चित करते हैं कि जब OS द्वारा EntryPoint कॉल किया जाता है, तो आह्वान कारण fdwReason == DLL_THREAD_ATTACH हो:
winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);
इसके बजाय सामान्य
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);
ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
// only run for a Thread creation event
if (fdwReason != DLL_THREAD_ATTACH) {
return TRUE;
}
...
}
ये दो अतिरिक्त कदम UDRL के डेमो में शामिल किए गए हैं।
createThread = 1 के साथ चलाने की आवश्यकता है)createThread = 1 के साथ चलाने की आवश्यकता है)