
Thread Stack Spoofing - PoC एक उन्नत In-Memory evasion तकनीक के लिए है जो स्कैनर्स और विश्लेषकों से इंजेक्ट किए गए शेलकोड के मेमोरी आवंटन को बेहतर ढंग से छिपाने की अनुमति देती है।
यह एक उन्नत इन-मेमोरी एवेज़न तकनीक का PoC कार्यान्वयन है जो थ्रेड कॉल स्टैक को स्पूफ करता है। यह तकनीक थ्रेड-आधारित मेमोरी जांच नियमों को बायपास करने और इन-प्रोसेस मेमोरी में शेलकोड को बेहतर ढंग से छिपाने की अनुमति देती है।
यह थ्रेड स्टैक स्पूफिंग तकनीक का एक उदाहरण कार्यान्वयन है जिसका उद्देश्य मैलवेयर विश्लेषकों, AV और EDR से बचना है जो जांचे गए थ्रेड के कॉल स्टैक में शेलकोड के फ्रेम के संदर्भों की तलाश करते हैं। विचार थ्रेड के कॉल स्टैक पर शेलकोड के संदर्भों को छिपाना है, इस प्रकार मैलवेयर कोड वाले आवंटन को छुपाना है।
मेरे ShellcodeFluctuation के साथ यह कार्यान्वयन Offensive Security समुदाय को वाणिज्यिक C2 उत्पादों द्वारा दी जाने वाली सुविधाओं के साथ तालमेल बिठाने के लिए नमूना कार्यान्वयन प्रदान करता है, ताकि हम अपने Red Team टूलिंग में कमतर न रहें। 💪
वर्तमान कार्यान्वयन मूल रूप से प्रकाशित से काफी भिन्न है। ऐसा इसलिए है क्योंकि मुझे एहसास हुआ कि थ्रेड के कॉल स्टैक प्रोसेसिंग को समाप्त करने और शेलकोड से संबंधित फ्रेम को छिपाने का एक सरल तरीका है: केवल उस पहले फ्रेम के रिटर्न एड्रेस पर 0 लिखना जिसे हम नियंत्रित करते हैं:
void WINAPI MySleep(DWORD _dwMilliseconds)
{
[...]
auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
const auto origReturnAddress = *overwrite;
*overwrite = 0;
[...]
*overwrite = origReturnAddress;
}
पिछला कार्यान्वयन, जो StackWalk64 का उपयोग करता है, इस कमिट c250724 में देखा जा सकता है।
यह कार्यान्वयन अधिक स्थिर है और दोनों आर्किटेक्चर - x64 और x86 पर Debug और Release दोनों में अच्छी तरह काम करता है।
यह इस प्रकार दिखता है जब कॉल स्टैक स्पूफ नहीं किया गया है:

इसके विपरीत, जब थ्रेड स्टैक स्पूफिंग सक्षम है:

ऊपर हम देख सकते हैं कि हमारे कॉल स्टैक पर अंतिम फ्रेम हमारा MySleep कॉलबैक है। कोई सोच सकता है कि क्या यह तुरंत नए IOC के अवसर लाता है? हंटिंग नियम उन थ्रेड्स की तलाश कर सकते हैं जिनके कॉल स्टैक सिस्टम लाइब्रेरीज में स्थित अपेक्षित थ्रेड एंट्री पॉइंट्स में नहीं खुलते:
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
हालांकि स्पूफ किए गए थ्रेड का कॉल स्टैक पहली नज़र में अजीब लग सकता है, मेरे सिस्टम की एक संक्षिप्त जांच से पता चला कि ऐसे अन्य थ्रेड भी हैं जो उपरोक्त एंट्री पॉइंट्स पर नहीं खुलते:

उपरोक्त स्क्रीनशॉट एक अपरिवर्तित Total Commander x64 के थ्रेड को दिखाता है। जैसा कि हम देख सकते हैं, इसका कॉल स्टैक प्रारंभिक कॉल स्टैक फ्रेम के मामले में हमारे जैसा ही दिखता है।
जब ऐसी प्रक्रियाएं हैं जो ऐसे लक्षण प्रदर्शित करती हैं जिन्हें हम आसानी से नकल कर सकते हैं, तो हमें अपने कॉल स्टैक को सावधानीपूर्वक नकली बनाने की चिंता क्यों करनी चाहिए?
मोटा एल्गोरिथ्म इस प्रकार है:
dbghelp.dll से सभी आवश्यक फ़ंक्शन पॉइंटर्स प्राप्त करें, SymInitialize को कॉल करेंkernel32!Sleep को हुक करें जो हमारे कॉलबैक की ओर इंगित करता है।VirtualAlloc + memcpy + CreateThread के माध्यम से शेलकोड इंजेक्ट और लॉन्च करें। थ्रेड को हमारे runShellcode फ़ंक्शन से शुरू होना चाहिए ताकि थ्रेड का StartAddress किसी अप्रत्याशित और असामान्य स्थान (जैसे ntdll!RtlUserThreadStart+0x21) पर इंगित न हो।MySleep कॉलबैक आमंत्रित होता है।0 से ओवरराइट करते हैं जो प्रभावी रूप से कॉल स्टैक को समाप्त कर देता है।::SleepEx पर कॉल किया जाता है।फ़ंक्शन रिटर्न एड्रेस थ्रेड के स्टैक मेमोरी क्षेत्र में बिखरे होते हैं, जो RBP/EBP रजिस्टर द्वारा इंगित होते हैं। उन्हें स्टैक पर खोजने के लिए, हमें पहले फ्रेम पॉइंटर्स एकत्र करने होंगे, फिर ओवरराइट करने के लिए उन्हें डीरेफरेंस करना होगा:

(उपरोक्त छवि Eli Bendersky की पोस्ट Stack frame layout on x86-64 से ली गई है)
*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
ThreadStackSpoofer के प्रारंभिक कार्यान्वयन ने walkCallStack और spoofCallStack फ़ंक्शंस में ऐसा किया, हालांकि वर्तमान कार्यान्वयन दिखाता है कि ये प्रयास स्टील्थी कॉल स्टैक बनाए रखने के लिए आवश्यक नहीं हैं।
उपयोग मामला:
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
जहां:
<shellcode> शेलकोड फ़ाइल का पथ है<spoof> जब 1 या true होगा तो थ्रेड स्टैक स्पूफिंग सक्षम होगी और बाकी सब इसे अक्षम करता है।उदाहरण रन जो बीकन के थ्रेड कॉल स्टैक को स्पूफ करता है:
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running.
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
कोड और उसके कार्यान्वयन को देखें, अवधारणा को समझें और अवधारणा को अपने स्वयं के शेलकोड लोडर में पुनः लागू करें जिनका उपयोग आप अपने Red Team अभियानों को वितरित करने के लिए करते हैं। यह उन्नत इन-मेमोरी एवेज़न के लिए एक और तकनीक है जो आपकी टीमों के एंटी-वायरस, EDR और आपके इम्प्लांट की जांच करने वाले मैलवेयर विश्लेषकों द्वारा पकड़े जाने की संभावनाओं को कम करती है।
अपने उन्नत शेलकोड लोडर को विकसित करते समय, आप निम्नलिखित को भी लागू करना चाह सकते हैं:
BeaconEye जैसे Beacon कॉन्फ़िगरेशन एक्सट्रैक्टर से बचने में मदद कर सकता हैRW में बदलें (RX/RWX से) और उनकी सामग्री को एन्क्रिप्ट करें - Shellcode Fluctuation तकनीक का उपयोग करके - स्लीप करने से ठीक पहले (जो Moneta या pe-sieve जैसे स्कैनर से बच सकता है)जैसा कि मुझे बताया गया है, यहां तकनीक अभी तक स्टैक स्पूफर होने के अपने नाम पर सही मायने में खरी नहीं उतरती है। चूंकि हम केवल थ्रेड के स्टैक पर रिटर्न एड्रेस को ओवरराइट कर रहे हैं, हम स्टैक के शेष क्षेत्रों को स्पूफ नहीं कर रहे हैं। इसके अलावा हम अपने कॉल स्टैक को अनविंडेबल छोड़ रहे हैं जो इसे असामान्य बनाता है क्योंकि सिस्टम पूरी कॉल स्टैक फ्रेम श्रृंखला को ठीक से नहीं चल सकेगा।
हालांकि मैं इन कमियों से अवगत हूं, फिलहाल मैंने इसे वैसे ही छोड़ दिया है क्योंकि मैं ज्यादातर स्वचालित स्कैनर से बचने के बारे में चिंतित था जो प्रक्रियाओं को पार कर सकते हैं, उनके थ्रेड्स की गणना कर सकते हैं, उन थ्रेड्स के स्टैक को चल सकते हैं और किसी भी रिटर्न एड्रेस को पकड़ सकते हैं जो एक गैर-इमेज मेमोरी (जैसे SEC_PRIVATE - जो VirtuaAlloc और इसके समान द्वारा गतिशील रूप से आवंटित किया गया है) की ओर इशारा करता है। एक केंद्रित मैलवेयर विश्लेषक तुरंत विषमता को पहचान लेगा और थ्रेड को असामान्य मानते हुए हमारे इम्प्लांट का शिकार करेगा। इसके बारे में मुझे पूरा यकीन है। फिर भी, मुझे नहीं लगता कि आजकल AV/EDR जैसे स्वचालित स्कैनर में इस तरह की ह्युरिस्टिक्स लागू की गई हैं जो वास्तव में प्रत्येक थ्रेड के स्टैक को चलेंगे यह सत्यापित करने के लिए कि क्या यह अनविंडेबल है ¯\_(ツ)_/¯।
निश्चित रूप से यह परियोजना (और C2 फ्रेमवर्क में पाया गया वाणिज्यिक कार्यान्वयन) AV और EDR विक्रेताओं को इस तरह की नवीन एवेज़न तकनीक को कवर करने वाली उपयुक्त ह्युरिस्टिक्स को लागू करने पर विचार करने के लिए तर्क देता है।
इस तकनीक को बेहतर बनाने के लिए, कोई व्यक्ति रिवर्स-अनविंडिंग प्रक्रिया में सावधानीपूर्वक तैयार किए गए नकली स्टैक फ्रेम डालकर एक सच्चे थ्रेड स्टैक स्पूफर का लक्ष्य रख सकता है। इस विचार के बारे में नीचे और पढ़ें।
namazso के साथ घंटों लंबी बातचीत ने मुझे सिखाया कि एक उचित थ्रेड स्टैक स्पूफर का लक्ष्य रखने के लिए हमें x64 कॉल स्टैक अनविंडिंग प्रक्रिया को उलटने की आवश्यकता होगी। सबसे पहले, किसी को नीचे लिंक (a) में समझाई गई स्टैक अनविंडिंग प्रक्रिया को ध्यान से स्वीकार करने की आवश्यकता है। जब सिस्टम x64 आर्किटेक्चर पर थ्रेड कॉल स्टैक को पार करता है, तो वह केवल थ्रेड के स्टैक पर बिखरे हुए रिटर्न एड्रेस पर निर्भर नहीं करेगा, बल्कि वह:
RUNTIME_FUNCTION, UNWIND_INFO और UNWIND_CODE संरचनाएं लौटाता है। ये संरचनाएं बताती हैं कि फ़ंक्शन का प्रारंभिक पता, समाप्ति पता कहां है, और वे सभी कोड अनुक्रम कहां हैं जो RBP या RSP को संशोधित करते हैं।UNWIND_CODE को संसाधित करता है ताकि उस फ्रेम के रिटर्न एड्रेस और स्टैक पॉइंटर मान के स्थान की सटीक गणना की जा सके।इस प्रक्रिया में हस्तक्षेप करने के लिए हमें RtlVirtualUnwind का अपना उल्टा रूप बनाकर इसे उलटना होगा। हमें एक मॉड्यूल (मान लें kernel32) में परिभाषित फ़ंक्शंस पर पुनरावृति करनी होगी, प्रत्येक फ़ंक्शन के UNWIND_CODE कोड को स्कैन करना होगा और इसे पीछे की ओर (जैसा कि RtlVirtualUnwind और विशेष रूप से RtlpUnwindPrologue की तुलना में) बारीकी से अनुकरण करना होगा ताकि स्टैक पर वे स्थान खोज सकें जहां हमें अपने नकली रिटर्न एड्रेस रखने हैं।
namazso कॉल स्टैक को अच्छी तरह से जोड़ने के लिए 3 नकली स्टैक फ्रेम पेश करने की आवश्यकता का उल्लेख करते हैं:
MySleep के कॉलर से अलग तरीके से अनविंड होता है (अलग UWOP - अनविंड ऑपरेशन कोड होता है)। हम एक मॉड्यूल के सभी फ़ंक्शंस को देखकर, उनके UWOP को देखकर, गणना करके करते हैं कि नकली फ्रेम कितना बड़ा होना चाहिए। इस फ्रेम में हमारे MySleep के कॉलर से अलग UWOP होना चाहिए।RBP में पॉप करके अनविंड होता है - मूल रूप से UWOP_PUSH_NONVOL कोड के माध्यम से।UWOP_SET_FPREG के माध्यम से RBP से RSP को पुनर्स्थापित करता हैपुनर्स्थापित RSP को उस RSP के साथ सेट किया जाना चाहिए जो नियंत्रण प्रवाह हमारे MySleep में प्रवेश करने पर लिया गया था, ताकि तीसरे गैजेट के वहां अनविंड होने के परिणामस्वरूप हमारे सभी फ्रेम छिप जाएं।
प्रक्रिया शुरू करने के लिए, कोई IMAGE_DIRECTORY_ENTRY_EXCEPTION डेटा डायरेक्ट्री एंट्री को डीरेफरेंस करके निष्पादन योग्य के .pdata पर पुनरावृति कर सकता है। नीचे दिए गए उदाहरण पर विचार करें:
ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);
auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
if (excdir.Size == 0 || excdir.VirtualAddress == 0)
return;
auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);
UNWIND_HISTORY_TABLE mshist = { 0 };
DWORD64 imageBase2 = 0;
PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
(DWORD64)caller,
&imageBase2,
&mshist
);
UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);
log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);
for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
{
UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);
if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
{
// Found candidate function for a desynch gadget frame
}
}
प्रक्रिया थोड़ी जटिल है, फिर भी यह ROP जैसे दृष्टिकोण में मनमाने स्टैक फ्रेम को सावधानीपूर्वक चयनित अन्य फ्रेम से प्रतिस्थापित करके थ्रेड के कॉल स्टैक अनविंडिंग प्रक्रिया को उलटने पर निर्भर करती है।
यह PoC इस एल्गोरिथ्म को दोहराता नहीं है, क्योंकि मेरी वर्तमान समझ मुझे EXE-आधारित स्टैक फ्रेम पर समाप्त होने वाले कॉल स्टैक को स्वीकार करने की अनुमति देती है और मैं न तो अपने शेलकोड लोडर और न ही इस PoC को अति जटिल बनाना चाहता हूं। इसे लागू करने और सार्वजनिक रूप से साझा करने का अभ्यास एक उत्सुक पाठक पर छोड़ता हूं। या हो सकता है कि मैं कुछ और खाली समय मिलने पर इसे स्वयं करने का प्रयास करूं :)
अधिक जानकारी:
RtlpUnwindPrologue and RtlVirtualUnwind.pdata sectionRtlpUnwindPrologueयदि आप इस कार्यक्षमता को अपने स्वयं के शेलकोड लोडर/टूलिंग में जोड़ने की योजना बनाते हैं तो kernel32.dll को अनहुक करने से बचना सुनिश्चित करें। kernel32 को अनहुक करने का प्रयास मूल Sleep कार्यक्षमता को पुनर्स्थापित करेगा और हमारे कॉलबैक को कॉल होने से रोकेगा। यदि हमारा कॉलबैक कॉल नहीं होता है, तो थ्रेड स्वयं अपने कॉल स्टैक को स्पूफ करने में असमर्थ होगा।
यदि आप यही चाहते हैं, तो आपको एक और वॉचडॉग थ्रेड चलाने की आवश्यकता हो सकती है, यह सुनिश्चित करते हुए कि जब भी Beacon स्लीप करे तो उसका थ्रेड स्पूफ हो जाए।
यदि आप Cobalt Strike और Raphael's Mudge द्वारा BOF unhook-bof का उपयोग कर रहे हैं, तो मेरे Pull Request को अवश्य देखें जो BOF में एक वैकल्पिक पैरामीटर जोड़ता है जो उन लाइब्रेरीज़ को निर्दिष्ट करता है जिन्हें अनहुक नहीं किया जाना चाहिए।
इस तरह आप kernel32 में अपने हुक बनाए रख सकते हैं:
beacon> unhook kernel32
[*] Running unhook.
Will skip these modules: wmp.dll, kernel32.dll
[+] host called home, sent: 9475 bytes
[+] received output:
ntdll.dll <.text>
Unhook is done.
निर्दिष्ट मॉड्यूल को अनदेखा करने के विकल्प के साथ संशोधित unhook-bof
यह PoC Cobalt Strike के Beacon शेलकोड के साथ काम करने के लिए डिज़ाइन किया गया था। Beacon अपने C2 से आगे के निर्देशों की प्रतीक्षा करने के लिए kernel32!Sleep को कॉल करने के लिए जाना जाता है। यह लोडर अपनी रखरखाव करने के लिए Sleep को हुक करके इस तथ्य का लाभ उठाता है। यह कार्यान्वयन बाजार में अन्य शेलकोड (जैसे Meterpreter) के साथ काम नहीं कर सकता है यदि वे कूल डाउन के लिए Sleep का उपयोग नहीं करते हैं। चूंकि यह केवल तकनीक दिखाने वाला एक प्रूफ ऑफ कॉन्सेप्ट है, मेरा किसी अन्य C2 फ्रेमवर्क के लिए समर्थन जोड़ने का इरादा नहीं है। जब आप अवधारणा को समझ लेंगे, तो निश्चित रूप से आप इसे अपने शेलकोड आवश्यकताओं में अनुवाद करने और अपने लाभ के लिए समाधान को अनुकूलित करने में सक्षम होंगे।
कृपया "यह कोड XYZ शेलकोड के साथ काम नहीं करता" से संबंधित GitHub मुद्दे न खोलें, उन्हें तुरंत बंद कर दिया जाएगा।
यह और अन्य परियोजनाएं नींद हराम रातों और बहुत सारी कड़ी मेहनत का परिणाम हैं। यदि आप मेरे काम को पसंद करते हैं और सराहना करते हैं कि मैं हमेशा समुदाय को वापस देता हूं, तो मुझे एक कॉफी खरीदने पर विचार करें (या बेहतर एक बियर) बस धन्यवाद कहने के लिए! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)