
Beacon द्वारा उत्पन्न फ़ाइलों को डिस्क के बजाय मेमोरी में लिखने के लिए एक CobaltStrike टूलकिट
MemFiles CobaltStrike के लिए एक टूलकिट है जो Operators को Beacon प्रक्रिया द्वारा निर्मित फ़ाइलों को लक्षित सिस्टम पर डिस्क पर लिखने के बजाय मेमोरी में लिखने में सक्षम बनाता है। इसका सफलतापूर्वक Windows 7, 10, और 11 पर परीक्षण किया गया है; संबंधित सर्वर संस्करण बिना किसी समस्या के काम करने चाहिए। MemFiles केवल x64 Beacons तक सीमित है।
यह NTDLL.dll के भीतर कई अलग-अलग NtAPI's को hook करके और उन API के कॉल्स को उन फ़ंक्शनों पर पुनर्निर्देशित करके ऐसा करता है जिन्हें Beacon प्रक्रिया मेमोरी स्पेस में इंजेक्ट किया गया है।
MemFiles मानता है कि Beacon प्रक्रिया में NTDLL की एक साफ़/बिना hook वाली प्रति मौजूद है। ऐसे Beacon प्रक्रिया में MemFiles की व्यवहार्यता के बारे में कोई गारंटी नहीं दी जाती है जहाँ EDR hooks अभी भी सक्रिय हैं। MemFiles का उपयोग करने से पहले NTDLL को repair/refresh करें!
MemFiles टूलकिट में एक "विशेष", गैर-मौजूद निर्देशिका परिभाषित की गई है; इस विशेष निर्देशिका में लिखी गई कोई भी फ़ाइल MemFiles द्वारा कैप्चर की जाएगी और मेमोरी में लिखी जाएगी, जहाँ से उन्हें बाद में Teamserver पर डाउनलोड किया जा सकता है।
MemFiles उन अधिकांश (सभी नहीं) टूल्स के साथ संगत है जो Beacon प्रक्रिया के भीतर चलते हैं और जिन्हें अपना आउटपुट किसी विशिष्ट निर्देशिका में लिखने का निर्देश दिया जा सकता है। इसे काम करने के लिए उन्नत विशेषाधिकारों की आवश्यकता नहीं होती है।
इसमें शामिल हैं:
-BOF's
-.NET assemblies इनलाइन चलाई गईं, जैसे inline-executeAssembly का उपयोग करके
-PE's इनलाइन चलाए गए, जैसे Inline-Execute-PE का उपयोग करके
ये सभी संगत हैं क्योंकि ये Beacon प्रक्रिया के अंदर चलते हैं, जहाँ प्रासंगिक NtAPI's hook किए गए हैं।
MemFiles निम्नलिखित जैसी चीज़ों के साथ काम नहीं करता है:
-execute-assembly
-shell
-run
इनमें से कोई भी संगत नहीं है क्योंकि ये सभी अन्य प्रक्रियाओं को स्पॉन करते हैं जिनके NtAPI's hook नहीं किए गए हैं।
MemFiles का परीक्षण Rubeus, SharpHound, Procdump, और Powershell जैसे टूल्स के साथ सफलतापूर्वक किया गया है जब उन्हें Beacon प्रक्रिया के भीतर चलाया जाता है।

रिपॉजिटरी को clone करें और वैकल्पिक रूप से hookdir वेरिएबल को बदलें, जो /PIC/Source/NtCreateFile.c और /PIC/Source/NtOpenFile.c दोनों में लाइन 56 पर परिभाषित है। यह वेरिएबल वह "विशेष" निर्देशिका है जो MemFiles को संकेत देती है कि उसे बनाई जा रही फ़ाइल को इंटरसेप्ट करना चाहिए। hookdir वेरिएबल डिफ़ॉल्ट रूप से "redteam" पर सेट है। सुनिश्चित करें कि यह वेरिएबल लक्षित सिस्टम पर कोई वास्तविक निर्देशिका नहीं है, और यह दोनों फ़ाइलों में समान है!

आवश्यक BOF's और PIC फ़ंक्शन दोनों को compile करने के लिए 'make all' चलाएँ।
MemFiles.cna को CobaltStrike Client में load करें। सुनिश्चित करें कि जिस निर्देशिका से CobaltStrike चल रहा है वह आपके उपयोगकर्ता द्वारा writable है; MemFiles वहाँ एक text फ़ाइल (memfiles.txt) बनाता है ताकि MemFiles के कार्य करने के लिए आवश्यक डेटा की उपलब्धता सुनिश्चित हो सके।
MemFiles को प्रत्येक नए Beacon में इंस्टॉल करने के लिए कॉन्फ़िगर किया जा सकता है जो Teamserver में कॉल करता है; यह MemFiles->Config मेनू आइटम का उपयोग करके किया जाता है। डिफ़ॉल्ट रूप से, MemFiles नए Beacons में ऑटो-इंस्टॉल नहीं होता है। ध्यान दें कि यह एक वैश्विक सेटिंग है; यदि दो Clients Teamserver से जुड़े हैं और दोनों में MemFiles.cna लोड है, यदि Client A "Install on beacon initial" सेटिंग को टॉगल करता है, तो यह परिवर्तन Client B पर भी प्रभावी होगा!

MemFiles में 4 लक्ष्य-उन्मुख कमांड्स हैं जो BOF's चलाते हैं और 1 आंतरिक कमांड है जो प्रोजेक्ट डेटा संरचना को संचालित करता है।
लक्ष्य-उन्मुख:
आंतरिक डेटा संरचना:
meminit Beacon प्रक्रिया में MemFiles इंस्टॉल करने के लिए ज़िम्मेदार है।
MemFiles द्वारा hook किए गए NtAPI's की सूची निम्नलिखित है:
meminit निम्नलिखित प्रमुख क्रियाएँ करता है:
memlist का उपयोग किसी दिए गए Beacon के लिए MemFiles द्वारा वर्तमान में मेमोरी में संग्रहीत सभी फ़ाइलों को प्रदर्शित करने के लिए किया जाता है।

कई फ़ील्ड प्रदर्शित की जाती हैं, जिनमें से उपयोगकर्ता के लिए सबसे प्रासंगिक और रुचिकर फ़ील्ड फ़ाइल का नाम और संग्रहीत डेटा की लंबाई है।
memfetch का उपयोग किसी दिए गए Beacon के लिए MemFiles द्वारा मेमोरी में संग्रहीत फ़ाइलों को वास्तव में प्राप्त करने के लिए किया जाता है।
डिफ़ॉल्ट रूप से, memfetch उन सभी फ़ाइलों को प्राप्त करेगा जो MemFiles द्वारा संग्रहीत हैं और जिनका "handle" बंद कर दिया गया है। यह डिज़ाइन विकल्प उस फ़ाइल को डाउनलोड करने के प्रयास से संबंधित किसी भी समस्या से बचने के लिए बनाया गया था जिसे कोई प्रोग्राम/एप्लिकेशन अभी तक लिखना समाप्त नहीं किया है।
इसका मतलब है कि यदि कोई प्रोग्राम/एप्लिकेशन फ़ाइल के लिए खोले गए handle को बंद करने में विफल रहता है, तो फ़ाइल memfetch द्वारा डाउनलोड नहीं की जाएगी।
इसे memfetch के साथ "force" तर्क का उपयोग करके कम किया जा सकता है, अर्थात 'memfetch force' का उपयोग करके handle की स्थिति की परवाह किए बिना सभी फ़ाइलों को मेमोरी से प्राप्त किया जा सकता है।
memfetch द्वारा मेमोरी से प्राप्त की गई फ़ाइलें डाउनलोड के रूप में Teamserver को वापस भेजी जाती हैं और CobaltStrike में Downloads टैब के माध्यम से Teamserver से Client पर synced की जा सकती हैं।
एक बार जब कोई फ़ाइल Teamserver द्वारा डाउनलोड हो जाती है, तो उसे Beacon प्रक्रिया की मेमोरी से मिटा दिया जाता है और उसकी प्रविष्टि, जैसा कि memlist के माध्यम से दिखाई जाती है, हटा दी जाती है।
memclean Beacon प्रक्रिया से MemFiles को साफ़ करने और हटाने के लिए ज़िम्मेदार है।
MemFiles के लिए मानक उपयोग केस में इसे इंस्टॉल करना और Beacon के जीवनकाल के दौरान इसे इंस्टॉल छोड़ देना शामिल है; हालाँकि यदि कोई MemFiles का उपयोग किसी टूल के साथ मिलकर फ़ाइल आउटपुट को कैप्चर और प्राप्त करने के लिए करना चाहता है, और फिर MemFiles को अनइंस्टॉल करना चाहता है ताकि उसके आर्टिफैक्ट मेमोरी में न रहें, तो memclean का उपयोग Beacon प्रक्रिया को meminit चलाने से पहले की मूल स्थिति में वापस लाने के लिए किया जा सकता है।
इसमें शामिल हैं:
ध्यान दें कि इन क्रियाओं को करने से पहले, memclean MemFiles द्वारा मेमोरी में संग्रहीत किसी भी फ़ाइल को forcefully डाउनलोड करेगा। यदि कोई MemFiles का उपयोग किसी एकल टूल के साथ करना चाहता है और फिर उसे हटाना चाहता है, तो वह memfetch का उपयोग छोड़ सकता है और केवल memclean का उपयोग करके एक ही बार में फ़ाइलों को प्राप्त कर सकता है और Beacon प्रक्रिया से MemFiles को हटा सकता है।
memtable का उपयोग उन Beacons के बारे में जानकारी प्रदर्शित करने और ट्रैक करने के लिए किया जाता है जिनमें MemFiles वर्तमान में इंस्टॉल है। यह वैश्विक कॉन्फ़िगरेशन जानकारी भी प्रदर्शित करता है।
प्रत्येक CobaltStrike Client का अपना memtable होता है; MemFiles यह सुनिश्चित करने के लिए बहुत प्रयास करता है कि उसके डेटा की सिंक्रोनिसिटी सभी जुड़े CobaltStrike Clients के बीच बनी रहे, ताकि MemFiles का उपयोग सभी Operators द्वारा सभी Beacons में किया जा सके। इसके बारे में अधिक जानकारी के लिए, "डिज़ाइन संबंधी विचार और टिप्पणी" देखें।

meminit कमांड का उपयोग करके किसी Beacon में MemFiles को initialize करें। इसे MemFiles->Config मेनू में विकल्प को टॉगल करके स्वचालित रूप से होने के लिए कॉन्फ़िगर किया जा सकता है।

MemFiles initialized होने के बाद, अब आप अपने पसंदीदा टूल्स का उपयोग करके फ़ाइलों को मेमोरी में लिख सकते हैं! यह कैसे करें यह विशिष्ट टूल पर निर्भर करेगा; कुछ आपको एक निर्देशिका निर्दिष्ट करने की अनुमति देते हैं जिसमें कई फ़ाइलें आउटपुट की जा सकती हैं, जबकि अन्य टूल द्वारा बनाई गई एकल फ़ाइल के लिए एक absolute path निर्दिष्ट करने की अनुमति देते हैं। कुछ उदाहरण नीचे देखे जा सकते हैं:
यहाँ हम निर्दिष्ट करते हैं कि SharpHound को सभी निर्मित फ़ाइलों को c:\redteam\ निर्देशिका (हमारी विशेष MemFiles निर्देशिका) में आउटपुट करना चाहिए और उसे फ़ाइलों को zip नहीं करना चाहिए; MemFiles प्रोग्रामों को मेमोरी से फ़ाइलें पढ़ने का समर्थन नहीं करता है, केवल उन्हें लिखने का, इसलिए SharpHound में zip कार्यक्षमता काम नहीं करती है।

Rubeus के साथ "dump" कमांड का उपयोग किया जाता है और हम इसे सभी कंसोल आउटपुट को एक फ़ाइल (हमारी विशेष निर्देशिका में स्थित) में भेजने का निर्देश देते हैं।

इस उदाहरण में, Inline-Execute-PE का उपयोग powershell.exe को Beacon प्रक्रिया में लोड करने और डोमेन उपयोगकर्ताओं की सूची प्राप्त करने के लिए 'Get-ADUser' चलाने के लिए किया जाता है। एक pipe और 'out-file' का उपयोग करके, डेटा को मेमोरी में लिखा जा सकता है और फिर प्राप्त किया जा सकता है।

जब आप अपनी फ़ाइलें प्राप्त करना चाहें, तो memfetch चलाएँ:

जब आप MemFiles का उपयोग समाप्त कर लें और/या इसे Beacon प्रक्रिया में इंस्टॉल छोड़ना न चाहें, तो memclean चलाएँ:

ध्यान दें कि उपरोक्त उदाहरण में एक फ़ाइल थी जो अभी तक डाउनलोड नहीं हुई थी; memclean इस फ़ाइल को डाउनलोड करता है और MemFiles को अनइंस्टॉल करने से पहले इसे मेमोरी से मिटा देता है।
memtable का उपयोग करके MemFiles की स्थिति और कॉन्फ़िगरेशन की जाँच करें। लंबे ऑपरेशनों के दौरान, अव्यवस्था से बचने के लिए मृत/पुराने beacons की प्रविष्टियों को memtable से साफ़ करें।

जैसा कि परिचय में ज़ोर देकर कहा गया है, MemFiles को कार्य करने के लिए Beacon प्रक्रिया में NTDLL की एक साफ़ प्रति की आवश्यकता होती है। यह आवश्यक है क्योंकि यह NtFunction में मूल बाइट्स को पढ़ता है और कुछ को trampoline पर कॉपी करता है, जिसका उपयोग बाद में NtFunction के सामान्य कॉल्स को पूरा करने के लिए किया जाता है जिनमें MemFiles को हस्तक्षेप नहीं करना चाहिए। इस विषय पर "तकनीकी विवरण, डिज़ाइन संबंधी विचार और टिप्पणी" अनुभाग में अधिक विस्तार से चर्चा की गई है।
MemFiles प्रत्येक फ़ाइल के लिए 1048576 बाइट्स का प्रारंभिक आवंटन करता है; जैसे-जैसे डेटा मेमोरी में लिखा जाता है, यह बड़ी फ़ाइलों को रखने के लिए आवश्यकतानुसार इस आवंटन का विस्तार कर सकता है और करेगा भी।
MemFiles struct में संग्रहीत फ़ाइल नाम को replacement NtCreateFile फ़ंक्शन में पारित तर्क में से parse किया जाता है। MemFiles इसे काफी सरल तरीके से करता है, फ़ाइल पथ तर्क में "विशेष" निर्देशिका का पता लगाकर, उसके अंत तक जाकर, और फिर "विशेष" निर्देशिका और फ़ाइल नाम को अलग करने वाले '\' वर्ण के लिए pointer को 1 से बढ़ाकर। उदाहरण के लिए, पथ 'C:\users\tom\redteam\myfile.txt' में, MemFiles 'redteam' का पता लगाता है, backslash वर्ण को ध्यान में रखता है, और फ़ाइल नाम के रूप में 'myfile.txt' का चयन करता है।
MemFiles को फ़ाइल पथ में किसी भी पूर्ववर्ती निर्देशिका की परवाह नहीं है; 'C:\redteam\myfile.txt' और 'c:\users\tom\appdata\local\redteam\myfile.txt' समान रूप से मान्य पथ हैं।
यह देखते हुए कि MemFiles फ़ाइल पथों से फ़ाइल नाम कैसे parse करता है, यह ध्यान दिया जाना चाहिए कि MemFiles उपनिर्देशिकाओं के निर्माण का समर्थन नहीं करता है; इसका मतलब है कि MemFiles उन टूल्स के साथ ठीक से काम नहीं करेगा जो, उदाहरण के लिए, c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt, आदि बनाने का प्रयास करते हैं।
MemFiles द्वारा hook किए गए NtAPI's विभिन्न प्रोग्रामों द्वारा I/O ऑपरेशनों के लिए उपयोग किए जाने वाले API के रूप में पहचाने गए हैं। जैसा कि पहले उल्लेख किया गया है, MemFiles/hooked NtAPI's के इस सेट के साथ सफलतापूर्वक परीक्षण किए गए टूल्स/क्षमताएँ हैं: SharpHound, Rubeus, Powershell, Procdump, BOF's, फ़ाइल लेखन ऑपरेशन करने वाले सामान्य C प्रोग्राम, और CobaltStrike की bupload_raw कमांड (जो Operator को दूरस्थ फ़ाइल स्थान निर्दिष्ट करने की अनुमति देती है)। निश्चित रूप से अन्य टूल भी हैं जो MemFiles के साथ सीधे काम करेंगे; अन्य असंगत होंगे।
पहली नज़र में, Windows फ़ाइल निर्माण प्रक्रिया सीधी लगती है: NtCreateFile->NtWriteFile->NtClose। इस प्रोजेक्ट में गहराई से जाने के तुरंत बाद मैंने पाया कि इसमें कई अन्य API शामिल हैं, और अतिरिक्त मज़े के लिए, शामिल अन्य API प्रोग्रामों के बीच भिन्न होते हैं। कुछ प्रोग्राम अपनी I/O प्रक्रिया के भाग के रूप में Win32 API SetFilePointerEx को कॉल करते हैं, जो बदले में NtAPI NtSetInformationFile को कॉल करता है। अन्य, जैसे SharpHound सहित .NET प्रोग्राम, NtFlushBuffersFile को कॉल करते हैं।
फ़ाइलों को बनाने और लिखने के लिए API कॉल्स की एक सामान्य श्रृंखला की कमी, MemFiles के साथ उपयोग किए जाने वाले टूल के आधार पर असंगतता की गुंजाइश पैदा करती है। कोई प्रोग्राम किसी अन्य NtAPI को कॉल कर सकता है जिसे MemFiles ने hook नहीं किया है, replacement NtCreateFile फ़ंक्शन में MemFiles द्वारा बनाए गए fake handle को पारित करते हुए, जिसके परिणामस्वरूप "Invalid Handle" त्रुटि होती है जो निष्पादन को रोक देती है। अन्य प्रोग्राम अधिक जटिल क्रियाएँ करते हैं जिन्हें MemFiles पर्याप्त रूप से spoof/replace नहीं कर पाता है। ऐसी एक पहचानी गई असंगति ADExplorer.exe है।
ADExplorer.exe एक signed Microsoft बाइनरी है जिसका उपयोग active directory enumeration के लिए किया जाता है। रनटाइम के दौरान, ADExplorer निर्दिष्ट आउटपुट फ़ाइल में डेटा लिखता है और फिर बाद में वापस जाकर उसे संदर्भित करता है, अंततः निष्पादन के अंत में अंतिम आउटपुट को फ़ाइल में लिखता है। चूँकि यह आउटपुट फ़ाइल को एक प्रकार के कैश के रूप में उपयोग करता है, इसलिए उसे फ़ाइल में पहले से लिखे गए डेटा को पढ़ने में सक्षम होना चाहिए, और इस बात की बहुत कम संभावना है कि वह इसे किसी सरल, पूर्वानुमानित तरीके से करे।
MemFiles वर्तमान में प्रोग्रामों या एप्लिकेशनों द्वारा इन-मेमोरी फ़ाइलों को पढ़े जाने का समर्थन नहीं करता है, लेकिन यह कस्टम NtReadFile फ़ंक्शन को और अधिक विस्तारित करके और MemFiles struct में कुछ अतिरिक्त वेरिएबल/डेटा ट्रैकिंग जोड़कर संभव होना चाहिए।
ADExplorer अपने द्वारा निर्मित फ़ाइलों के आकार में एक और चुनौती प्रस्तुत करता है। बड़े enterprise वातावरण में आउटपुट फ़ाइल 1GB से अधिक हो सकती है; हालाँकि MemFiles प्रोग्रामेटिक रूप से इसे संभालने में सक्षम होना चाहिए, यह निश्चित रूप से ऐसे उपयोग के मामलों के लिए अभिप्रेत नहीं है।
निस्संदेह समुदाय ऐसे टूल्स खोजेगा जिनके साथ MemFiles ठीक से काम नहीं करता है; मैं आपको प्रोत्साहित करता हूँ कि आप एक issue खोलें जिसमें असंगत प्रोग्राम/टूल और वे परिस्थितियाँ विस्तृत हों जिनमें आपने उसे चलाया, अर्थात यह BOF है, inline-executeAssembly के माध्यम से, Inline-Execute-PE, आदि, ताकि मैं देख सकूँ कि क्या मैं MemFiles का विस्तार करके उसे काम करवा सकता हूँ।
MemFiles से जुड़े IOC's में निम्नलिखित शामिल हैं, लेकिन इन्हीं तक सीमित नहीं हैं:
VirtualAlloc का उपयोग करके मेमोरी आवंटित करना
WriteProcessMemory का उपयोग करके डेटा लिखना
आवंटित मेमोरी पर मेमोरी सुरक्षा को RW और RX के बीच बदलना
NTDLL.dll के भीतर मेमोरी को overwrite करना
MemFiles को किसी उचित EDR के विरुद्ध विकसित या परीक्षण नहीं किया गया था; Microsoft Defender ही उपलब्ध था। फिर भी, मैं यह कहने का साहस करूँगा कि जो भी टूल/प्रोग्राम Beacon फ़ाइल बनाने के लिए चला रहा है, उस पर MemFiles द्वारा उस फ़ाइल को कैप्चर या मेमोरी में संग्रहीत करने की तुलना में अलर्ट होने की अधिक संभावना है। NTDLL में मेमोरी को overwrite करना/NtAPI's को hook करना मुझे ऐसी चीज़ लगती है जिस पर कुछ उत्पाद आपत्ति जता सकते हैं, लेकिन इसकी पुष्टि करने के लिए मेरे पास कोई सबूत नहीं है। उन hook किए गए NtFunctions के कॉल्स के लिए जो उन फ़ाइलों से संबंधित नहीं हैं जिन्हें MemFiles द्वारा कैप्चर किया जाता है/किया जाना चाहिए, syscall अभी भी NTDLL.dll एड्रेस स्पेस के भीतर से जारी किया जाता है, क्योंकि सुरक्षा उत्पाद इस क्षेत्र के बाहर से किए गए syscalls का पता लगाते हैं और उन पर अलर्ट करते हैं।
यह ध्यान दिया जाना चाहिए कि MemFiles द्वारा मेमोरी में संग्रहीत फ़ाइलें encoded या encrypted नहीं होती हैं; यह सुविधा तब जोड़ी जा सकती है जब कोई वास्तविक उपयोग मामला/उदाहरण पहचाना जाए जहाँ AV/EDR मेमोरी में निर्मित फ़ाइल पर अलर्ट कर रहा हो।
मुझे सबसे पहले इन-मेमोरी फ़ाइल सिस्टम की अवधारणा से कई महीने पहले KFiveFour के Tradecraftcon में एक कॉन्फ़्रेंस टॉक में परिचित कराया गया था, जहाँ एक वक्ता (@DexterGerig) ने एक POC प्रदर्शित किया था जिसने Client-server मॉडल का उपयोग करके एक इन-मेमोरी फ़ाइल सिस्टम बनाया था। ऐसे प्रोजेक्ट की मेरी कल्पित कार्यक्षमता का आधा हिस्सा मेरे पिछले प्रमुख रिलीज़, Inline-Execute-PE द्वारा कवर किया गया था। दूसरा आधा हिस्सा, टूलिंग द्वारा निर्मित फ़ाइलों को कैप्चर करने और उन्हें डिस्क के बजाय मेमोरी में संग्रहीत करने में सक्षम होने का विचार, उस प्रोजेक्ट द्वारा पूरा नहीं किया गया था और स्पष्ट कारणों से अत्यधिक वांछनीय क्षमता बना रहा।
MemFiles मेरे लिए एक अविश्वसनीय रूप से चुनौतीपूर्ण कार्य था, क्योंकि इस प्रोजेक्ट से पहले मैंने debugger में बहुत कम समय बिताया था, assembly की कोई समझ नहीं थी, और API hooking को नहीं समझता था। इस प्रोजेक्ट के दौरान मुझे कई 10-20 घंटे लंबी बाधाओं का सामना करना पड़ा, जिन्हें मैं दृढ़ता के साथ, शुक्र है, पार करने में सक्षम रहा। हालाँकि यह शायद सबसे कुशल तरीका नहीं था, मुझे debuggers के साथ काफी परिचितता मिली और इस बात की अधिक समझ मिली कि assembly, registers, stack, और calling conventions के मामले में कंप्यूटर अंदर से कैसे काम करते हैं।
निम्नलिखित कुछ अधिक महत्वपूर्ण तकनीकी विवरणों और डिज़ाइन संबंधी विचारों पर एक तकनीकी गहन चर्चा है, जो MemFiles में शामिल किए गए हैं।
Windows पर फ़ाइल निर्माण NtCreateFile से शुरू होता है, जिसे वांछित फ़ाइल का पथ दिया जाता है और बदले में Windows उस स्थान पर एक फ़ाइल बनाता है और उसका एक handle प्रदान करता है। लौटाया गया handle फ़ाइल से जुड़े सभी बाद के कॉल्स में उपयोग किया जाता है, उदाहरण के लिए NtWriteFile और NtClose में।
इन सभी API के कॉल्स को उन कॉल्स में अलग करने के बारे में सोचते हुए जिन्हें हम इंटरसेप्ट और छेड़छाड़ करना चाहते हैं और उन्हें जिन्हें हम अछूता छोड़ना चाहते हैं, मैंने NtCreateFile कॉल में एक कीवर्ड खोजने पर ध्यान केंद्रित किया। यह NtCreateFile कॉल में फ़ाइल पथ के भाग के रूप में एक अद्वितीय, गैर-मौजूद निर्देशिका निर्दिष्ट करके पूरा किया गया था। जब हमारा hook निष्पादन को replacement NtCreateFile फ़ंक्शन पर पुनर्निर्देशित करता है, तो NtCreateFile को तर्क के रूप में पारित फ़ाइल पथ की जाँच उस अद्वितीय "कीवर्ड" की उपस्थिति के लिए की जाती है; यदि वह उसे ढूंढ लेता है, तो MemFiles जान जाता है कि यह NtCreateFile कॉल उस फ़ाइल से संबंधित है जिसे डिस्क के बजाय मेमोरी में रखा जाना चाहिए। जब ऐसा होता है, MemFiles कई वेरिएबल्स को initialize करता है और फ़ाइल के उपयोग के लिए प्रारंभिक 1MB मेमोरी आवंटित करता है, लेकिन सबसे महत्वपूर्ण बात यह है कि यह MemFiles struct में निर्दिष्ट फ़ाइल नाम के साथ एक fake handle जोड़ता है, और इस fake handle को caller को लौटा देता है।
MemFiles द्वारा hook किए गए अन्य सभी NtAPI's के लिए, संबंधित replacement NtFunction तर्क के रूप में पारित handle को देखते हैं और जाँचते हैं कि क्या वह MemFiles struct में मौजूद है; यदि handle वास्तव में MemFiles struct में मौजूद है (MemFiles द्वारा उत्पादित fake handles इतने fake होते हैं कि उनका वास्तविक handle के साथ कभी टकराव नहीं होना चाहिए), तो MemFiles इस कॉल को इन-मेमोरी फ़ाइल से संबंधित बताते हुए पहचानता है और तदनुसार कार्य करता है।### हुकिंग सिद्धांत और प्रतिस्थापन फ़ंक्शन
डिस्क के लिए नियत फ़ाइलों को मेमोरी में लिखने के लिए, MemFiles को उन API कॉल्स को इंटरसेप्ट करने की आवश्यकता होती है जो प्रोग्राम किसी फ़ाइल को बनाने का प्रयास करते समय करते हैं। API हुकिंग काफी लंबे समय से मौजूद है, और कई EDR उत्पादों द्वारा उनकी कार्यक्षमता के मुख्य भाग के रूप में सक्रिय रूप से उपयोग की जाती है; कुछ API कॉल्स को EDR एड्रेस स्पेस की ओर पुनर्निर्देशित किया जाता है, जहाँ API कॉल और उसमें पास किए गए वेरिएबल्स का विश्लेषण किया जाता है। यदि EDR यह निर्धारित करता है कि कॉल दुर्भावनापूर्ण है, उदाहरण के लिए किसी हमले के टूल या किल चेन का हिस्सा है, तो यह कॉल को पूरा होने से रोक देगा और अलर्ट जारी करेगा। यदि EDR निर्णय लेता है कि कॉल हानिरहित है, तो यह निष्पादन को उस स्थान पर वापस पैच कर देगा जहाँ से इसे पुनर्निर्देशित किया गया था और API कॉल को मूल रूप से इच्छित अनुसार पूरा करने की अनुमति देगा। एक सरल उपमा यह कहकर दी जा सकती है कि आप एक मित्र को पत्र डाक से भेजते हैं, लेकिन आपके मित्र तक पहुँचने से पहले कोई तीसरा पक्ष उसे खोलकर पढ़ता है, और फिर तय करता है कि क्या पत्र में कुछ अवैध है; ऐसी स्थिति में आपके मित्र को पत्र कभी नहीं मिलता और पुलिस को सूचित किया जाता है।
MemFiles उसी सिद्धांत का पालन करता है, बिना अलर्ट (या सैद्धांतिक पुलिस संलिप्तता) के। API हुकिंग आमतौर पर यूज़रलैंड में सबसे निचले संभव स्तर पर लागू की जाती है; NTDLL.dll के भीतर के NtFunctions। आइए किसी भी हुकिंग से पहले NtCreateFile पर एक नज़र डालें:
All NtFunctions समान होते हैं, सिवाय syscall नंबर के, जो इस उदाहरण में 55 है। syscall नंबर अलग-अलग NtFunctions में बदलता है, और यह भी ध्यान दिया जाना चाहिए कि यह नंबर Windows संस्करणों के बीच बदल सकता है; इस OS (Windows 11) पर NtCreateFile का syscall नंबर 55 है, हालाँकि Windows 10 पर यह भिन्न हो सकता है (और Windows 7 पर निश्चित रूप से भिन्न है)।
TEST और JNE निर्देशों पर ध्यान देना उचित है। ये यह निर्धारित करने के लिए मौजूद हैं कि NtFunction को सामान्य syscall निर्देश का उपयोग करना चाहिए या लीगेसी INT 2E निर्देश का। मैं klezvirus की पोस्ट SysWhispers is dead, long live SysWhispers! से उद्धृत करूँगा:
अब दिलचस्प हिस्सा यह है कि फ़ंक्शन जाँचता है कि क्या SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) 1 पर सेट है। SharedUserData एक प्रतीक है जो कर्नेल मोड संरचना KUSER_SHARED_DATA को संदर्भित करता है।
KUSER_SHARED_DATA संरचना एक निश्चित (या पूर्व-परिभाषित) मेमोरी स्पेस को परिभाषित करती है जिसका उपयोग यूज़र-मोड सॉफ़्टवेयर के साथ जानकारी साझा करने के लिए किया जाता है। यह निश्चित रूप से कुछ वैश्विक सिस्टम जानकारी को यूज़र-लैंड कोड द्वारा उपभोग के लिए तैयार रखने के लिए किया गया था, बिना हर बार यूज़र और कर्नेल-मोड निष्पादन के बीच स्विच करने के ओवरहेड के।
इंडेक्स 0x308 पर मान syscall निर्देश को दर्शाता है, जो 1511 से सभी Windows संस्करणों में समर्थित है। जैसा कि आप कल्पना कर सकते हैं, 1511 से पहले के सभी Windows संस्करणों में, syscall निष्पादित करने का मानक तरीका इंटरप्ट int 2Eh को कॉल करना था।
...
यदि आप सोच रहे हैं कि यह int 2Eh अभी भी क्यों मौजूद है, भले ही Windows अब संस्करण 1511 से काफी ऊपर है, तो इसका कारण यह है कि यह निर्देश अभी भी उपयोग होता है। वास्तव में, जब HVCI (Hypervisor-protected Code Integrity) सक्षम होता है, तो SharedUserData[0x308] को 0 पर सेट किया जाता है, और syscall निर्देश के बजाय int 2Eh का उपयोग किया जाता है। यह अधिकतर प्रदर्शन कारणों से किया जाता है, इस आधार पर कि Ring3 से Ring0 स्विच को एक या दूसरे निर्देश का उपयोग करके कैसे संचालित किया जाता है।
मैंने इस विषय पर आगे स्पष्टीकरण के लिए ट्विटर पर पूछा, जिस पर @yarden_shafir ने निम्नलिखित कहा:

संक्षेप में कहें तो, NtFunction का हर निर्देश किसी न किसी बिंदु पर आवश्यक हो सकता है (शायद अंत में पाए जाने वाले मल्टी-बाइट NOP के अपवाद के साथ, जो अप्राप्य प्रतीत होता है), और यदि हम NtFunction में निर्देशों को अधिलेखित करते हैं, तो हमें यह सुनिश्चित करने की आवश्यकता है कि हम उन्हें सहेजें और अंतिम syscall (या मामले के अनुसार INT 2E) करने से पहले किसी बिंदु पर उन्हें निष्पादित करें।
यह ध्यान देने योग्य है कि syscall करने से पहले, syscall नंबर को RAX में ले जाया जाता है (स्क्रीनशॉट में EAX के रूप में दिखाया गया है)। क्योंकि हम इससे पहले RAX को स्टैक पर पुश होते नहीं देखते हैं, मैंने (शायद भोलेपन से) मान लिया था कि syscall नंबर वहाँ ले जाने से पहले RAX में मौजूद मान syscall जारी होने के बाद महत्वपूर्ण या आवश्यक नहीं है। यह अच्छी खबर है, क्योंकि इसका मतलब है कि हम RAX रजिस्टर का स्वतंत्र रूप से उपयोग कर सकते हैं, जब तक कि हम यह सुनिश्चित करते हैं कि syscall जारी करने से पहले उसमें syscall नंबर मौजूद हो।
निष्पादन को हमारे कस्टम कोड/प्रतिस्थापन NtFunction की ओर पुनर्निर्देशित करने के लिए, हम मूल NtAPI के कुछ हिस्से को अधिलेखित करेंगे, अपने प्रतिस्थापन NtFunction का पता RAX में ले जाएँगे और फिर उस कोड पर जाने के लिए JMP निर्देश का उपयोग करेंगे:
MOV और JMP निर्देशों के लिए 12 बाइट्स की आवश्यकता होती है; क्योंकि हम NtAPI के पहले 12 बाइट्स को अधिलेखित करके अन्य निर्देशों को विकृत कर रहे हैं, उन निर्देशों को NtAPI के उचित स्पेसिंग और संरेखण को बनाए रखने के लिए NOP's से बदल दिया गया है।
जब अब प्रोग्राम NtCreateFile को कॉल करता है, तो यह निष्पादन को हमारे प्रतिस्थापन NtFunction पर कूदा देगा।
MemFiles इस मामले में EDRs के हुकिंग करने के तरीके से भिन्न है कि हुक किए गए API जिन प्रतिस्थापन फ़ंक्शनों की ओर पुनर्निर्देशित होते हैं, उन्हें कहाँ रहना चाहिए। कई EDR अपने स्वयं के DLL को प्रोसेस में लोड करते हैं। हुक किए गए API इस लोड किए गए DLL के एड्रेस स्पेस की ओर पुनर्निर्देशित होते हैं जहाँ विश्लेषण हो सकता है। चूँकि इस परियोजना का पूरा उद्देश्य चीज़ों को डिस्क पर गिराने से बचना था, डिस्क पर DLL रखना और हमारी Beacon प्रोसेस को उसे लोड करना ताकि हमारे प्रतिस्थापन NtFunctions तक पहुँच मिल सके, एक खराब रास्ता लगा। एक कई साल पुराना POC उपलब्ध है जो DLL को मेमोरी से लोड करने की अनुमति देता है, जो हमारी आवश्यकताओं के लिए एक व्यवहार्य रणनीति है, लेकिन यह परियोजना अनुरक्षित नहीं है और इसमें कई समस्याएँ प्रतीत होती हैं। इसके अलावा यह 1200 लाइनों का कोड है और इसे BOF प्रारूप में बदलना एक दुष्कर कार्य होगा।
एक संक्षिप्त पार्श्व टिप्पणी के रूप में, हमारे प्रतिस्थापन NtFunctions किसी BOF में नहीं रह सकते; CobaltStrike BOFs को प्रोसेस मेमोरी में लोड करता है, चलाता है, और फिर उन्हें मिटा देता है। चूँकि हमें मेमोरी में एक स्थायी फ़ंक्शन (या फ़ंक्शनों) की आवश्यकता है जिसे जब भी प्रोसेस हुक किए गए NtAPI में से किसी एक को कॉल करे, बुलाया जा सके, BOFs काम नहीं करेंगे।
मेरे पास जो उत्तर आया वह था पोज़िशन इंडिपेंडेंट कोड (PIC)। जैसा कि नाम से पता चलता है, सामान्य एक्ज़ीक्यूटेबल्स के विपरीत जिन्हें एक निश्चित स्थान पर/उसके हिस्सों को एक-दूसरे के सापेक्ष एक निश्चित संबंध में लोड किए जाने की आवश्यकता होती है, PIC को मेमोरी में कहीं भी रखा और चलाया जा सकता है। यह हमारे प्रतिस्थापन NtFunctions को PIC एक्ज़ीक्यूटेबल्स के रूप में लिखने, उन्हें Beacon प्रोसेस में इंजेक्ट करने, और हमारे हुक्स को निष्पादन को उनकी ओर पुनर्निर्देशित करने का द्वार खोलता है जब हमारे हुक किए गए NTAPIs के लिए कॉल की जाती हैं।
इन PIC NtFunctions के लिए टेम्पलेट Cracked5pider के ShellcodeTemplate प्रोजेक्ट से आता है।
आधार परियोजना से एक उल्लेखनीय विचलन यह है कि आधार परियोजना एक पूर्ण PIC exe बनाने के इर्द-गिर्द डिज़ाइन की गई है; अर्थात् इसमें ASM शामिल है जो स्टैक पॉइंटर को सहेजता है, स्टैक पर स्थान बनाता है, exe के भीतर निहित निर्दिष्ट प्रतिस्थापन NtFunction को कॉल करता है, और फिर उस फ़ंक्शन के लौटने पर स्टैक पॉइंटर को बहाल करता है। PIC exe द्वारा की गई call निर्देश एक समस्या प्रस्तुत करती है, क्योंकि ऐसा करने से call किए जाने वाले स्थान का रिटर्न पता (PIC exe ASM में) स्टैक पर पुश हो जाता है, जिससे बाद में मिलने वाला ret निर्देश निष्पादन को वापस PIC exe में लौटा देगा, न कि मूल NtAPI के कॉलर के पास।
इसे कम करने के लिए, ShellcodeTemplate प्रोजेक्ट में ASM फ़ाइल को संपादित करके स्टैक सेट करने, फ़ंक्शन को कॉल करने और फ़ंक्शन के निष्पादन समाप्त होने के बाद स्टैक पॉइंटर को बहाल करने से संबंधित ASM को हटा दिया गया। परिणाम यह है कि NtAPI में लगाया गया हुक अब निष्पादन को सीधे प्रतिस्थापन NtFunction में कूदा देता है, जिसमें स्टैक और रजिस्टर उसी तरह सेट होते हैं जैसे वे तब थे जब मूल NtAPI को प्रोग्राम द्वारा कॉल किया गया था (RAX के अपवाद के साथ, जो हमारे JMP के लिए उपयोग होता है)।
Original ShellcodeTemplate ASM:

MemFiles ASM:

प्रत्येक हुक किए गए NtAPI का अपना PIC NtFunction होता है जिसमें आवश्यक तर्क होता है या तो:
A. MemFiles-विशिष्ट क्रियाएँ करना, जैसे नकली हैंडल बनाना, मेमोरी में डेटा लिखना, MemFiles struct के वेरिएबल्स को बदलना आदि
या
B. निष्पादन को एक trampoline की ओर निर्देशित करना जो API कॉल को पटरी पर वापस लाएगा और इसे वापस NTDLL में पैच करेगा जहाँ syscall किया जा सके
कुछ प्रतिस्थापन NtFunctions दूसरों की तुलना में अधिक जटिल हैं; जब हुक किए गए NtAPI की कॉल उनमें से एक होती है जो MemFiles से संबंधित होती है, तो कुछ, जैसे NtCreateFile और NtQueryVolumeInformationFile, MSDN प्रलेखन, परीक्षण के परिणामों और कुछ अनुमान/सामान्य ज्ञान के अनुसार NtAPI में तर्क के रूप में पास किए गए वेरिएबल्स को संशोधित करते हैं। अन्य, जैसे NtClose और NtReadFile, मूल कॉलर को केवल STATUS_SUCCESS लौटाते हैं ताकि अपरिहार्य "Invalid Handle" त्रुटि से बचा जा सके, जो अन्यथा MemFiles द्वारा बनाए गए नकली हैंडल को पास करने से उत्पन्न होती।
जब हुक किए गए NtAPI की कॉल MemFiles से संबंधित नहीं होती है, तो हमें चीज़ों को पटरी पर वापस लाने के लिए निष्पादन को एक trampoline की ओर निर्देशित करने की आवश्यकता होती है:

ट्रैम्पोलिन उन सभी निर्देशों को निष्पादित करने के लिए ज़िम्मेदार है जो मूल NtAPI में उस API के हुक किए जाने के कारण निष्पादित नहीं हुए थे; इसमें वे निर्देश शामिल हैं जो मूल हुक द्वारा आंशिक रूप से या पूरी तरह से अधिलेखित किए गए थे। API हुकिंग जल्दी ही ऐसी समस्याएँ पैदा कर सकती है जहाँ हमारे पास उन सभी निर्देशों को करने के लिए पर्याप्त स्थान नहीं होता जिन्हें हमें करने की आवश्यकता होती है। ट्रैम्पोलिन इस समस्या को भी कम करने में मदद कर सकते हैं, क्योंकि हम मूल NtAPI में वापस कूदने से पहले अपने रजिस्टरों और/या स्टैक को सेट करने के लिए किसी भी संख्या में क्रियाएँ कर सकते हैं। NtCreateFile ट्रैम्पोलिन नीचे देखा जा सकता है:

सबसे स्पष्ट रूप से, हमारे प्रारंभिक हुक द्वारा अधिलेखित किए गए तीन निर्देशों को ट्रैम्पोलिन के पहले तीन निर्देशों के रूप में देखा जा सकता है:
MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1
जैसा कि पहले उल्लेख किया गया है, इस पूरे हेरफेर में बड़ी आवश्यकता यह है कि syscall जारी करने से पहले syscall नंबर (उपरोक्त उदाहरण में 55) RAX(EAX) में मौजूद हो। हालाँकि हमारे सामने एक समस्या है कि हमें अभी भी निष्पादन को वापस मूल NtAPI की ओर निर्देशित करने के लिए JMP निर्देश का उपयोग करने की आवश्यकता है। जबकि कोई अन्य रजिस्टर हो सकता है जो महत्वपूर्ण जानकारी नहीं रखता और ऐसा करने के लिए उपयोग किया जा सकता है, मुझे हमारे द्वारा हुक किए जा रहे NtAPIs की संख्या को देखते हुए कोई लगातार सुरक्षित विकल्प नहीं मिला, जिनमें से प्रत्येक रजिस्टरों का अलग-अलग उपयोग कर सकता है। सुरक्षित विकल्प RAX का उपयोग जारी रखना है; हम ऐसा RAX में संग्रहीत syscall नंबर को स्टैक पर पुश करके कर सकते हैं। फिर हम मूल NtAPI में वापस कूदने के लिए इच्छित पते को RAX में ले जा सकते हैं और NTDLL में वापस पहुँचने के लिए JMP निर्देश का उपयोग कर सकते हैं:

NtAPI में स्थापित हुक में एक POP RAX निर्देश शामिल है, और यह वही स्थान है जहाँ हम अपने ट्रैम्पोलिन का उपयोग करके कूदते हैं। इस निर्देश को निष्पादित करने से syscall नंबर स्टैक के शीर्ष से RAX में बहाल हो जाता है और हमें syscall जारी करने के लिए तैयार करता है। ध्यान दें कि मूल, बिना हुक वाले NtAPI का JNE निर्देश अभी भी यहाँ है; संगत TEST निर्देश, जो ZF फ्लैग सेट करता है और यह निर्धारित करता है कि JNE लिया जाता है या नहीं (जो हमें syscall के ऊपर से INT 2E तक कूदा देगा), ट्रैम्पोलिन में निष्पादित किया गया था। चीज़ों को इस तरह व्यवस्थित करने से हम NtAPI को सफलतापूर्वक हुक कर सकते हैं और निष्पादन को अपने प्रतिस्थापन PIC NtFunction की ओर पुनर्निर्देशित कर सकते हैं, साथ ही यह सुनिश्चित कर सकते हैं कि हम अपने हुकिंग के परिणामस्वरूप कुछ भी नहीं छोड़ रहे हैं या कोई कार्यक्षमता नहीं खो रहे हैं।
अब तक जो कवर किया गया है उसे संक्षेप में समझने के लिए, जब MemFiles आरंभीकृत होता है, तो Beacon प्रोसेस मेमोरी में एक struct बनाया जाता है जिसमें Memfiles की कार्यक्षमता के लिए महत्वपूर्ण जानकारी होती है। इस struct की जानकारी MemFiles के जीवनचक्र में लगातार संदर्भित होती है, जिसमें प्रत्येक प्रतिस्थापन PIC NtFunction के साथ-साथ उन BOFs द्वारा भी शामिल है जिनका उपयोग MemFiles द्वारा मेमोरी में संग्रहीत फ़ाइलों को क्वेरी करने और प्राप्त करने के लिए किया जाता है। इस उद्देश्य के लिए, जब struct बनाया जाता है, तो struct जिस मेमोरी पते पर रहता है, उसे Teamserver को वापस भेज दिया जाता है:

इस पते को memtable में संग्रहीत करने के बाद, बाद के MemFiles कमांड (memlist, memfetch, memclean) इस पते को BOF को एक तर्क के रूप में भेजते हैं ताकि struct का पता लगाया जा सके और संदर्भित किया जा सके। लेकिन PIC NtFunctions struct का पता कैसे लगाते हैं?
सबसे स्पष्ट समस्या यह है कि PIC NtFunctions को MemFiles struct के मेमोरी पते की आवश्यकता होती है, लेकिन struct बनने के समय तक वे पहले ही संकलित हो चुके होते हैं। MemFiles के एक प्रारंभिक कार्यान्वयन ने MemFiles के आरंभीकरण को दो अलग-अलग BOFs में विभाजित करके इस समस्या का समाधान किया। पहले ने struct बनाया और पता Teamserver को वापस भेजा, जो कुछ Aggressor स्क्रिप्ट जादू के माध्यम से पते को पार्स करेगा, उसे प्रत्येक NtFunction के स्रोत कोड फ़ाइल में सम्मिलित करेगा, और फिर उन्हें अंतिम PIC NtFunction में पुनः संकलित करेगा। दूसरा BOF तैयार PIC NtFunctions को प्रेषित करेगा और NtAPIs की वास्तविक इंजेक्टिंग और हुकिंग करेगा।
बदसूरत होने और अतिरिक्त समय लेने के अलावा, ऐसी स्थिति में वास्तविक समस्याएँ उत्पन्न हो सकती हैं जहाँ कई beacons एक ही समय में MemFiles को आरंभ करने का प्रयास कर रहे हों। यदि Beacon 2 अपने MemFiles struct पते के साथ वापस कॉल करे जबकि Beacon 1 NtFunction स्रोत कोड फ़ाइलों को पैच और पुनः संकलित करने की प्रक्रिया में हो, तो चीज़ें गड़बड़ हो सकती हैं।
इस समस्या का सुरुचिपूर्ण समाधान PIC NtFunction पर बाइनरी पैच करना है, जिसमें MemFiles struct पता संकलित NtFunction में पैच किया जाता है और रनटाइम के दौरान सुलभ होता है। इसे सुविधाजनक बनाने के लिए, प्रत्येक NtFunction में एक प्लेसहोल्डर स्ट्रिंग लिखी जाती है:

इस वेरिएबल को xxd जैसे टूल का उपयोग करके संकलित कोड में देखा जा सकता है:

जब meminit कमांड चलाया जाता है, तो प्रत्येक PIC NtFunction को InstallHooks BOF के साथ Beacon को भेजा जाता है। यह BOF MemFiles struct बनाने के लिए ज़िम्मेदार है; ऐसा करने के बाद, यह प्रत्येक PIC NtFunction पर patchAddr फ़ंक्शन को कॉल करता है। patchAddr A के स्ट्रिंग का पता लगाने और उसे struct पते के स्ट्रिंग प्रतिनिधित्व के साथ बदलने के लिए ज़िम्मेदार है। वास्तव में, पहले के स्क्रीनशॉट से मेमोरी पते का उपयोग करते हुए, pFileInfoStr वेरिएबल अब ऐसा दिखता है:
char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );
इस स्ट्रिंग प्रतिनिधित्व को फिर वास्तविक हेक्स मान में बदला जा सकता है, जो हमारा मेमोरी पता है। BOFs इसे _strtoi64 API का उपयोग करके काफी सरलता से पूरा करते हैं:

बेशक PIC NtFunctions के लिए चीज़ें इतनी आसान नहीं हो सकती थीं।
असेंबली का यह संक्षिप्त अंश एक PIC NtFunction के अंत को दर्शाता है। JMP RAX निर्देश पर ध्यान दें, जो PIC NtFunction द्वारा trampoline को कॉल करना है (अर्थात यह एक कॉल है जिसमें MemFiles ने स्पूफ़/हस्तक्षेप नहीं किया):

अज्ञात कारणों से, जब मैंने _strtoi64 (या उसके संबंधित API जैसे stroull या atoll) का उपयोग करने का प्रयास किया, तो यह JMP RAX निर्देश CALL RAX निर्देश बन गया। मुझे यकीन है कि इसके लिए एक वैध कारण है जिसमें "कंप्यूटर कैसे काम करते हैं" का कोई गहरा स्तर शामिल है, लेकिन यह प्रतीत होने वाला महत्वहीन परिवर्तन काफी कुछ तोड़ देता है। MemFiles struct पते के स्ट्रिंग प्रतिनिधित्व को वास्तविक हेक्स मान में बदलने का तरीका खोजने में 15+ घंटे बिताने के बाद, ताकि मैं उसके भीतर संग्रहीत मानों का उपयोग कर सकूँ, मुझे यह StackOverflow पोस्ट मिली, जिसमें एक टिप्पणीकार ने माइक्रोकंट्रोलर के लिए स्ट्रिंग को uint32 में बदलने के लिए डिज़ाइन किया गया एक कस्टम रूटीन प्रदान किया; शुक्र है कि यह बिना संशोधन के uint64 के लिए भी काम कर गया, और सबसे महत्वपूर्ण बात यह है कि इसने PIC NtFunction में बाद में JMP RAX निर्देश को सुरक्षित रखा, बिना उसे CALL में बदले। अंतिम अंश:

सरलता के लिए इसका पहले उल्लेख नहीं किया गया था, लेकिन NtAPIs का प्रारूप वर्षों में बदल गया है। पुराने संस्करण, जैसे कि Windows 7 द्वारा उपयोग किए जाने वाले, अपने आधुनिक समकक्षों की तुलना में बहुत छोटे होते हैं, 32 बाइट्स के बजाय केवल 16 बाइट्स लंबे होते हैं:

इसके लिए MemFiles द्वारा NtAPIs को हुक करने के तरीके के साथ-साथ trampoline बनाने के तरीके में भी बदलाव की आवश्यकता होती है। MemFiles को उन NtAPIs के संस्करण की पहचान करने के लिए जिनसे वह निपट रहा है, InstallHooks BOF पहले संबंधित NtAPI का पता हल करता है और फिर उस स्थान से 32 बाइट्स पढ़ता है। अलग प्रारूप के परिणामस्वरूप syscall निर्देश NtAPI संस्करणों के बीच अलग-अलग स्थानों पर स्थित होता है; एक निश्चित बाइट ऑफसेट पर syscall की उपस्थिति या अनुपस्थिति की जाँच करके, MemFiles यह निर्धारित करने में सक्षम होता है कि वह आधुनिक या लीगेसी NtAPI कार्यान्वयन से निपट रहा है और तदनुसार कार्य करता है:

हुकिंग के बाद, लीगेसी NtCreateFile API इस तरह दिखता है:

और रजिस्टरों को सेट करने के बाद NtCreateFile में वापस कूदने के लिए उपयोग किया जाने वाला trampoline:

कुल मिलाकर तकनीक बहुत हद तक वही है, लेकिन चीज़ें काफी तंग हैं और अधिक गुंजाइश नहीं है। यह ध्यान देने योग्य है कि सब कुछ ठीक से फिट और कार्यशील बनाने के लिए, syscall निर्देश को NtCreateFile के भीतर स्थानांतरित करना पड़ा; यह अभी भी NTDLL के भीतर NtCreateFile API मेमोरी स्पेस में रहता है, लेकिन यह API के 9वें और 10वें बाइट से स्थानांतरित होकर API के 14वें और 15वें बाइट पर आ गया है, सब कुछ सही ढंग से ठूँसने के लिए मल्टी-बाइट NOP को बलिदान कर दिया गया है।
फ़ाइल I/O संचालन से संबंधित कुछ API सहज थे और इसलिए उन्हें पहचानना और हुक करना आसान था; अन्य कहीं अधिक दुर्लभ थे, जिनके लिए WinDbg और x64dbg में घंटों बिताकर असेंबली से गुजरते हुए यह पहचानने की कोशिश करनी पड़ी कि कौन से API कॉल किए जा रहे थे। मुझे विश्वास करना होगा कि कार्य को पूरा करने का एक अधिक कुशल तरीका था, लेकिन मैं चलते-चलते सीख रहा था।एक त्वरित अतिरिक्त जानकारी के रूप में, मैं उस प्रक्रिया का विवरण देना चाहूँगा, जो पीछे मुड़कर देखने पर बहुत त्वरित होनी चाहिए थी, जिससे उस NtAPI की पहचान हुई जो SharpHound को 20 घंटों तक काम करने से रोक रही थी। .NET प्रोग्राम होने के नाते, SharpHound ने एक बहुत ही बदसूरत कॉल स्टैक निकाला, जो पूरी तरह से इस तथ्य से संबंधित था कि उसने निर्धारित किया था कि "हैंडल अमान्य है"। जबकि .NET एक प्रोग्रामर-अनुकूल भाषा है, अधिकांश (सभी?) कार्यक्षमता का अनुवाद होता है और अंततः Win32 API (और परिणामस्वरूप NtAPI) से होकर गुजरती है, जहाँ हम इसे किसी और चीज़ की तरह देख और हुक कर सकते हैं।

“Invalid Handle” त्रुटि एक स्पष्ट संकेत थी कि SharpHound द्वारा उपयोग किया जाने वाला कोई NtAPI था जिसे मैं हुक नहीं कर रहा था (मेरे मौजूदा PIC NtFunctions में से किसी के ठीक से काम न करने के विपरीत), इसलिए मैं यह पता लगाने के लिए निकला कि वह क्या था। इस बिंदु तक मैंने अन्य टूल्स के साथ जो तकनीक इस्तेमाल की थी, वह यह थी कि पहले NtCreateFile पर ब्रेकपॉइंट सेट करें और उस कॉल का पता लगाएं जो मेरी "विशेष" निर्देशिका से संबंधित थी। वहाँ से मैं प्रोग्राम में चरण-दर-चरण आगे बढ़ता था (आमतौर पर कई बार क्योंकि मैं भटक जाता था), और देखता था कि प्रोग्राम ने आगे किन फ़ंक्शनों को कॉल किया। इस पद्धति का अनुसरण करते हुए ही मुझे पता चला कि NtQueryVolumeInformationFile, NtQueryInformationFile, और NtSetInformationFile को कॉल किया गया था और उन्हें हुक करने की आवश्यकता थी।
SharpHound कुछ अतिरिक्त चुनौतियाँ पेश करता है, क्योंकि यह अपने कई कार्यों को अतुल्यकालिक रूप से करता है। यह उन रैखिक चरणों का पता लगाना और अधिक कठिन बना देता है जो एक एकल फ़ाइल API कॉलों के माध्यम से करती है, क्योंकि कई फ़ाइलें एक साथ इस प्रक्रिया से गुजर रही होती हैं। इसके अलावा, NtCreateFile कॉल के बाद प्रोग्राम में कदम दर कदम चलते हुए मैंने पाया कि जिस थ्रेड में NtCreateFile कॉल किया गया था, वह अंततः समाप्त हो जाता है; अंतिम NtWriteFile कॉल्स (और जो भी अन्य अज्ञात API कॉल्स, जो इस खोज के विषय हैं) एक अलग थ्रेड में होते हैं, जो खोज प्रक्रिया को और अधिक भ्रमित करता है।
चूंकि मैंने .NET के साथ केवल संक्षिप्त रूप से काम किया था, इसलिए मैं त्रुटियों द्वारा उत्पन्न कॉल स्टैक्स को पार्स करने में अच्छी तरह पारंगत नहीं था, विशेष रूप से उनमें जो async के उपयोग से दोगुने बदसूरत बन जाते हैं। इस 20 घंटे की समस्या के दौरान, अपनी पिछली रणनीति से प्रगति न होने पर, मैं बार-बार उसी ओर लौटता रहा और धीरे-धीरे लेकिन निश्चित रूप से उसका अधिक अर्थ समझने लगा। शीर्ष से तीसरे "भाग" के आसपास, जैसा कि "--- End of stack trace..." पंक्तियों द्वारा अलग किया गया है, एक पंक्ति दिखाई देती है जिसमें लिखा है "at Sharphound.Writers.JsonDataWriter..."। इसने मुझे वास्तविक SharpHound कोड में शुरू करने के लिए एक सापेक्ष स्थान दिया, जो ओपन सोर्स है और Github पर उपलब्ध है। जैसा कि नाम से पता चलता है, SharpHound फ़ंक्शन JSON आउटपुट को फ़ाइल में लिखने से संबंधित था; मुझे पहले से ही पता था कि मेरा डेटा सफलतापूर्वक फ़ाइल में नहीं लिखा जा रहा था, इसलिए यह कोई नई बात नहीं थी। कॉल स्टैक को एक स्तर ऊपर ट्रेस करने पर, अगली प्रासंगिक पंक्ति थी "at System.IO.Streamwriter."। System.IO उपसर्ग ने मुझे बताया कि यह एक .NET अंतर्निहित था, न कि कोई SharpHound-विशिष्ट फ़ंक्शन। कॉल स्टैक के बिल्कुल ऊपरी भाग को देखते हुए, जो पंक्ति मेरी नज़र में आई वह थी "at System.IO.FileStream.FlushOSBuffer()"। मैंने FlushOSBuffer को गूगल करने और देखने का निर्णय लिया कि मुझे क्या मिल सकता है।
ऐसा करने पर मैं Microsoft के .NET प्रलेखन पर पहुँचा filestream.cs। वहाँ मुझे FlushOSBuffer की परिभाषा मिली:

ऐसा प्रतीत होता है कि यह एक Win32 API, FlushFileBuffers को कॉल करता है। Win32Native.FlushFileBuffers की परिभाषा Win32Native प्रलेखन को देखकर पाई जा सकती है:

जिसने भी .NET में P/Invoke के साथ काम किया है, वह इस प्रारूप को पहचान लेगा। मेरे पास अब एक Win32 API था, जिसके बारे में मुझे पता था कि System.IO.Filestream.FlushOSBuffer(), मेरा समस्या वाला .NET फ़ंक्शन, उसे कॉल करता है। KERNEL32!FlushFileBuffers पर ब्रेकपॉइंट सेट करना और SharpHound चलाना इसकी पुष्टि करता है, और कदम दर कदम चलने पर मैंने शीघ्र ही देखा कि आंतरिक रूप से FlushFileBuffers, NtFlushBuffersFile को कॉल करता है। इस API को हुक करने से SharpHound की समस्याएँ दूर हो गईं और यह सफलतापूर्वक चलने में सक्षम हो गया, अपनी आउटपुट फ़ाइलों को मेमोरी में लिखता हुआ।
इस परियोजना का एक महत्वपूर्ण हिस्सा फ़ाइलों के मेमोरी में आने के बाद उन्हें वास्तव में CobaltStrike Teamserver पर डाउनलोड करने की क्षमता है। स्वाभाविक रूप से, सामान्य CobaltStrike डाउनलोड कमांड ऐसे फ़ाइलपथ के साथ काम नहीं करेगा जो वास्तव में मौजूद नहीं है। अब जो मैं जानता हूँ, उसके अनुसार, एक समाधान संभवतः प्रतिस्थापन PIC NtReadFile कोड को विस्तारित करने में निहित है, ताकि इन-मेमोरी फ़ाइलों को केवल लिखे जाने के बजाय पढ़े जाने में सक्षम बनाया जा सके। पहले से वह ज्ञान न होने के कारण, फ़ाइलों को वास्तव में प्राप्त करने में सक्षम होना एक प्रमुख बाधा थी।
संयोगवश मुझे EspressoCake द्वारा लिखित एक BOF मिला, जिसमें एक फ़ंक्शन था जो तुरंत मेरी नज़र में आया:

कोड को देखने पर ऐसा प्रतीत होता है कि यह एक अप्रलेखित Beacon CALLBACK विकल्प का उपयोग करता है:

यह फ़ंक्शन एक BOF को क्लाइंट से हटकर लक्ष्य से Teamserver पर फ़ाइल के डाउनलोड को आरंभ करने में सक्षम बनाता है। यह क्षमता (जिसके बारे में मुझे बाद में पता चला कि यह कई अन्य लोगों के संयुक्त प्रयास था, जिनमें @Cr0Eax, @EthicalChaos, और @anthemtotheego शामिल हैं) ने पहले से मौजूद प्रमुख बाधा को दूर कर दिया, क्योंकि अब मेरे पास इन-मेमोरी फ़ाइल के लिए लक्ष्य प्रणाली से फ़ाइल स्थानांतरण आरंभ करने का एक तरीका था। इस कोड स्निपेट के लिए सभी शामिल लोगों को बहुत-बहुत धन्यवाद, जो मुझे भविष्य में भी उपयोगी लगेगा।
इस परियोजना का एक चुनौतीपूर्ण हिस्सा Team Server से जुड़े सभी CobaltStrike Clients के लिए MemFiles कार्यक्षमता की उपलब्धता सुनिश्चित करना था। MemFiles का डेटा MemFiles.cna द्वारा निर्मित संरचनाओं में संग्रहीत होता है, जिन्हें टूल का उपयोग करने के इच्छुक प्रत्येक Client में लोड किया जाना चाहिए; परिणामस्वरूप, ये डेटा संरचनाएँ प्रत्येक Client के भीतर रहती हैं, Team Server पर नहीं। यदि यह डेटा किसी एक केंद्रीय स्थान (TS) में रहता, तो इसे प्रत्येक Client से प्राप्त करना सरल होता और यह पूरा मामला कोई समस्या नहीं होता; यदि CobaltStrike टीम औपचारिक रूप से MemFiles जैसी क्षमता को CobaltStrike में एकीकृत करती, तो मुझे पूरा विश्वास है कि वे इसी दिशा में जाते। लेकिन चूँकि यह एक सामुदायिक ऐड-ऑन है, हम जो हमारे पास है उसी से काम चलाते हैं।
जब यह सुनिश्चित करने की बात आती है कि प्रत्येक CobaltStrike Client के पास Beacons और कॉन्फ़िगरेशन के भीतर MemFiles स्थिति के संबंध में नवीनतम, सटीक डेटा है, तो हमें कुछ अलग-अलग परिदृश्यों के बारे में चिंता करनी होती है:
नए Clients जो TS से जुड़ते हैं और वर्तमान memtable की आवश्यकता होती है
ऐसे उदाहरण जहाँ केवल एक ही Client TS से जुड़ा होता है और CobaltStrike को पुनः आरंभ करता है (इस प्रकार Client की मेमोरी में संग्रहीत memtable खो जाती है)
Client A द्वारा MemFiles डेटा में परिवर्तन करना जिसे Client B तक संचारित किया जाना चाहिए
इन परिदृश्यों को संबोधित करने के लिए एक बहु-आयामी दृष्टिकोण अपनाया गया। जहाँ केवल एक ही CobaltStrike Client TS से जुड़ा है (और इस प्रकार केवल वही इकाई है जिसके पास memtable डेटा है), उस स्थिति को संभालने के लिए, हर बार जब Client memtable में परिवर्तन करता है (meminit, memclean), वह अपने memtable की सामग्री को CobaltStrike निर्देशिका में स्थित एक स्थानीय टेक्स्ट फ़ाइल में भी लिखता है। यदि Client बाहर निकलता/पुनः आरंभ होता है, या जब MemFiles.cna पुनः लोड होता है, तो वह पहले स्थानीय memtable.txt फ़ाइल से पढ़ने का प्रयास करेगा ताकि अपनी इन-मेमोरी memtable को आबाद कर सके।
जब एक TS से कई Clients जुड़े होते हैं और एक नया Client जुड़ता है (जैसा कि Event Log में दिखता है), प्रत्येक Client TS से जुड़े सभी उपयोगकर्ताओं की सूची प्राप्त करता है और उसे वर्णानुक्रम में क्रमबद्ध करता है। उस सूची में पहला Client "Broadcast" Client के रूप में चुना जाता है, और 5 सेकंड प्रतीक्षा करने के बाद (नए Client को आरंभ करने और अपनी स्थानीय memtable.txt पढ़ने की अनुमति देने के लिए) अपनी memtable की प्रत्येक प्रविष्टि के लिए Event Log में संदेश (Actions) भेजेगा। सभी Clients (Broadcasting वाले को छोड़कर) इन संदेशों को पढ़ेंगे और प्रसारण जानकारी के साथ अपनी memtable को अद्यतन करेंगे; इसमें मौजूदा प्रविष्टियों को अद्यतन करना और साथ ही उन अतिरिक्त प्रविष्टियों को जोड़ना शामिल है जो उनकी संबंधित memtable में नहीं हैं।
MemFiles से जुड़े सामान्य संचालन भी Event Log में संदेश भेजने पर निर्भर करते हैं। जब Client A, meminit चलाता है, तो सभी प्रासंगिक memtable जानकारी वाला एक संदेश प्रसारित होता है; सभी Clients "on Event_Action" हुक का उपयोग करके इन प्रसारित Event Log संदेशों को पार्स करके अपनी संबंधित memtable को अद्यतन करते हैं। जब meminit अपना BOF निष्पादित करना समाप्त करता है, तो MemFiles डेटा में भी परिवर्तन किए जाते हैं; ये परिवर्तन Beacon द्वारा वापस संचारित किए जाते हैं (उदाहरण के लिए, meminit चलाने के बाद, Beacon pMemAddrs संरचना के मेमोरी स्थान के साथ वापस कॉल करता है) और इस प्रकार सभी जुड़े Clients को दिखाई देते हैं, जो "on Beacon_Output" हुक का उपयोग करके अपनी संबंधित memtable को अद्यतन करते हैं।
ये अलग-अलग प्रयास मिलकर MemFiles को कई Clients के बीच महत्वपूर्ण डेटा को कुशलतापूर्वक और विश्वसनीय रूप से सिंक्रनाइज़ करने में सक्षम बनाते हैं।
यह परियोजना निम्नलिखित व्यक्तियों और परियोजनाओं के योगदान के बिना संभव नहीं होती: