
लाइब्रेरी जो अप्रत्यक्ष syscalls के उपयोग को आसान बनाती है। PoC के रूप में काफी दिलचस्प AV/EDR बायपास।
यह प्रोजेक्ट NTAPI फ़ंक्शंस को इनडायरेक्ट सिस्कॉल (indirect syscalls) का उपयोग करके उपयोग करने का एक आरामदायक और आधुनिक तरीका लागू करता है, साथ ही डाइनामिक सिस्कॉल नंबर प्राप्त करने के लिए FreshyCalls विधि को थोड़े बदलाव के साथ जोड़ा गया है। यह एक ऐसी तकनीक का भी उपयोग करता है जिसका ज़िक्र मैंने विंडोज डिफेंडर की मेमोरी स्कैनिंग को बायपास करने के लिए कहीं नहीं देखा। प्रोजेक्ट लाइब्रेरी का उपयोग करके एक क्लासिक PoC प्रोसेस इंजेक्टर लागू करता है। एन्क्रिप्शन के लिए सुविधाजनक फ़ंक्शन भी उपलब्ध हैं।

सैंपल बनाने के लिए -DNULLGATE_BUILD_SAMPLE=ON का उपयोग करें।
यदि आपने nullgate को सीधे बिल्ड किया है तो यह <build_dir>/sample.exe पर उपलब्ध होगा, यदि आपने इसे डिपेंडेंसी के रूप में बिल्ड किया है तो <build_dir>/_deps/nullgate-build/sample.exe पर।
विंडोज़ पर, क्योंकि बिल्ड डेस्टिनेशन अजीब होते हैं, यह संभवतः सैंपल्स के उन्हीं आधार निर्देशिकाओं में होगा लेकिन शायद कई स्तर अंदर।
यह एक PID लेता है जिसमें आप शेलकोड इंजेक्ट करना चाहते हैं, एक आर्ग्युमेंट के रूप में।
[!WARNING] यदि आप लिनक्स का उपयोग कर रहे हैं तो आपके पास mingw क्रॉस-कंपाइलर इंस्टॉल होना आवश्यक है। उदाहरण के लिए आर्क पर आप
pacman -S mingw-w64-gccचला सकते हैं। फिर mingw को डिफ़ॉल्ट कंपाइलर के रूप में सेट करने के लिए-DNULLGATE_CROSSCOMPILE=ONविकल्प का उपयोग करें।
[!TIP] डिटेक्शन की संभावना कम करने के लिए परिणामी बाइनरी को स्ट्रिप करने की भी सिफारिश की जाती है
git clone https://github.com/0xsch1zo/NullGate
cd NullGate
cmake . -B build -DNULLGATE_BUILD_SAMPLE=ON
cmake --build build/
इसे -DNULLGATE_DEPRECATED_HASHER फ्लैग का उपयोग करके बिल्ड किया जा सकता है।
CMake FetchContent समर्थित है। यहाँ एक सरल CMakeLists.txt का उदाहरण है:
cmake_minimum_required(VERSION 3.25)
include(FetchContent)
FetchContent_Declare(nullgate
GIT_REPOSITORY https://github.com/0xsch1zo/NullGate
GIT_TAG 1.2.0
)
FetchContent_MakeAvailable(nullgate)
project(test)
add_executable(test
main.cpp
)
target_link_libraries(test
PRIVATE nullgate
)
लिंकिंग स्टैटिक रूप से की जाती है, इसलिए आपको प्रतीकों (symbols) के दिखाई देने की चिंता करने की आवश्यकता नहीं है।
[!NOTE] निम्नलिखित उदाहरण
namespace ng = nullgateका उपयोग करेंगे
उपयोग काफी सीधा है, यहाँ मुख्य कार्यक्षमता दर्शाने वाला एक स्निपेट है:
ng::syscalls syscalls;
typedef NTSTATUS NTAPI NtAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ _At_(*BaseAddress,
_Readable_bytes_(*RegionSize) _Writable_bytes_(*RegionSize)
_Post_readable_byte_size_(*RegionSize)) PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType, _In_ ULONG PageProtection);
NTSTATUS status = syscalls.SCall<NtAllocateVirtualMemory>(
ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"), processHandle,
&buf, 0, ®ionSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
इसमें बिल्ट-इन टाइप-सेफ्टी है। आपको बस उस nt फ़ंक्शन की परिभाषा प्रदान करनी है जिसे आप कॉल करना चाहते हैं! आप इसे आसानी से ntdoc से प्राप्त कर सकते हैं। लाइब्रेरी का उपयोग करने का यह अनुशंसित तरीका है।
उन लोगों के लिए जो c++ टेम्पलेटिंग ब्लैक मैजिक पसंद नहीं करते, पिछला इंटरफ़ेस अभी भी उपलब्ध है:
NTSTATUS status = syscalls.Call(ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"),
processHandle, (PVOID)&buf, (ULONG_PTR)0, ®ionSize,
(ULONG)(MEM_RESERVE | MEM_COMMIT), (ULONG)PAGE_EXECUTE_READWRITE);
इस इंटरफ़ेस का उपयोग करके आपको तर्कों को सही प्रकार में कास्ट करना आवश्यक है, ऐसा न करने पर समस्याएँ हो सकती हैं।
constexpr uint64_t hash = ng::obfuscation::fnv1Const("NtAllocateVirtualMemory");
पहले दिखाई गई fnv1Const विधि आधुनिक C++ की खुशियों को maldev की दुनिया में लाती है। यह एक consteval फ़ंक्शन है, इसलिए यह गारंटी है कि इसका मूल्यांकन कंपाइल समय पर होगा, जिससे पठनीय फ़ंक्शन नाम fnv1 हैश से बदल जाएगा।
इसका रनटाइम समकक्ष भी है जिसे fnv1Runtime कहा जाता है, लेकिन निश्चित रूप से यह हमारे फ़ंक्शन नामों को ऑब्स्क्यूरेट करने का लाभ नहीं देता है। इसका उपयोग कार्यान्वयन द्वारा यह जाँचने के लिए किया जाता है कि ntdll के अंदर किस फ़ंक्शन से सिस्कॉल नंबर प्राप्त करना है।
xor "एन्क्रिप्शन" के लिए तीन रूटीन हैं:
constexpr ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
std::cout << xored.string();
संभावित आउटपुट:
<Y:-E&X0
यह रूटीन fnv1Const फ़ंक्शन की तरह कंपाइलर समय पर मूल्यांकित होता है, इसलिए "some string" कभी भी बाइनरी में दिखाई नहीं देगा, क्योंकि स्ट्रिंग अपने एन्क्रिप्टेड रूप में संग्रहीत होगी।
लेकिन हमें वास्तव में उस डेटा का उपयोग करना होगा! ConstData कच्चे बाइट्स के चारों ओर एक पतला रैपर है जो स्थिर आकार का होता है।
इसके दो तरीके हैं raw() और string()।
raw() एक std::vector<unsigned char> देता है और string जैसा कि नाम से पता चलता है, एक std::string देता है।
[!NOTE] यदि आपको
ConstDataको सीधे स्ट्रिंग लिटरल के साथ बनाना है, तोstd::to_arrayका उपयोग करकेConstDataको दिया जाने वाला मध्यवर्ती ऐरे बनाएँ। कृपया ध्यान दें कि इस दृष्टिकोण के साथConstDataलिटरल के अतिरिक्त null कैरेक्टर को भी संग्रहीत करेगा।
बेशक हमें इसे डिक्रिप्ट करने और उपयोग करने का एक तरीका चाहिए, जो अगला फ़ंक्शन है जिसे हम कवर करेंगे।
xorRuntime दूसरा उपलब्ध रूटीन है।
जैसा कि नाम से पता चलता है, यह xorConst का रनटाइम समकक्ष है।
कृपया ध्यान दें कि इसका उपयोग केवल डिक्रिप्शन के लिए ही नहीं, बल्कि दोनों दिशाओं में किया जा सकता है, हालांकि एन्क्रिप्शन के लिए इसका उपयोग करने से स्ट्रिंग को कंपाइल समय पर ऑब्स्क्यूरेट करने का लाभ नहीं मिलता है।
ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
ng::obfuscation::ConstData data = ng::obfuscation::xorRuntime(xored);
assert(data.string() == "some string");
std::string dynamicString = "some data of dynamic size";
auto dynamicData = ng::obfuscation::DynamicData(dynamicString);
auto xoredDynamic = ng::obfuscation::xorRuntime(dynamicData);
auto unxoredDynamic = ng::obfuscation::xorRuntime(xoredDynamic);
assert(unxoredDynamic.string() == dynamicString);
DynamicData में ConstData के समान तरीके हैं, साथ ही इंटरऑपरेबिलिटी के लिए कुछ अतिरिक्त कंस्ट्रक्टर हैं, और जैसा कि नाम से पता चलता है, इसका आकार कंपाइल समय पर ज्ञात नहीं हो सकता है।
xorRuntime DynamicData और ConstData दोनों को स्वीकार करता है।
हालाँकि हम पहले xorConst को कॉल करके और परिणाम को xorRuntime में पास करके हमेशा रनटाइम पर डिक्रिप्ट कर सकते हैं, यह थोड़ा थकाऊ है।
इस समस्या का समाधान यहाँ है, सबसे ऊपर की चेरी है xorRuntimeDecrypted — यह स्ट्रिंग लिटरल के लिए एक सुविधाजनक फ़ंक्शन है जिन्हें एन्क्रिप्टेड अवस्था में संचालित करने की आवश्यकता नहीं होती है।
यह स्ट्रिंग लिटरल को कंपाइल समय पर एन्क्रिप्ट करता है और फिर एक ही कॉल में इसे रनटाइम पर डिक्रिप्ट करता है। क्या यह मज़ेदार नहीं है!
ng::obfuscation::ConstData text = ng::obfuscation::xorRuntimeDecrypted<"some string">();
assert(data.string() == "some string");
अब कोई कह सकता है कि सब कुछ बढ़िया है लेकिन कुंजी कहाँ है? nullgate 1.2 में, प्रत्येक नए बिल्ड (यानी cmake कमांड चलाने पर) के लिए कुंजी यादृच्छिक रूप से उत्पन्न होती है! इससे सिग्नेचर किए जाने की संभावना और भी कम हो जाती है।
समस्या का मूल यह है कि जब हम NtCreateRemoteThreadEx या NtCreateProcess कॉल करते हैं, तो एक मेमोरी स्कैन ट्रिगर होता है और हमारा सिग्नेचर-युक्त msfvenom पेलोड डिटेक्ट हो जाता है।
एक ज्ञात समाधान यह है कि पहले NtAllocateVirtualMemory कॉल करते समय पेज परमिशन को PAGE_NOACCESS पर सेट करें, फिर थ्रेड को सस्पेंडेड अवस्था में बनाएँ।
जब विंडोज डिफेंडर हमारे प्रोसेस की मेमोरी को स्कैन करेगा, तो वह ऐसा करने में विफल रहेगा।
फिर हम NtResumeThread के साथ अपने थ्रेड के निष्पादन को फिर से शुरू कर सकते हैं।
यह काम करता है, लेकिन क्या होगा यदि कोई अधिक सक्षम सुरक्षा समाधान उपयोग किया जा रहा है? वह क्या करेगा?
वह निश्चित रूप से हमारे पेज की अनुमतियों को बदलने के लिए VirtualProtect का उपयोग करेगा और msfvenom को डिटेक्ट करेगा।
इसे बायपास करने के लिए मैंने रणनीति को थोड़ा बदल दिया। पेज को PAGE_NOACCESS पर सेट करने के बजाय, प्रोसेस की मेमोरी में अपने पहले लेखन के दौरान हम प्रोसेस में कुछ जंक डेटा डाल सकते हैं (हाँ, यह आवश्यक है, या मैं बिना इसके इसे काम करवाने का तरीका खोजने के लिए बहुत मूर्ख हूँ)।
फिर हम सस्पेंडेड अवस्था में एक थ्रेड बनाते हैं।
उसके बाद हम प्रोसेस में अपना इच्छित शेलकोड लिखते हैं और अंत में NtResumeThread का उपयोग करके थ्रेड को फिर से शुरू करते हैं।
इस तकनीक के साथ हमें NtCreateThreadEx कॉल के बाद हमारी मेमोरी तक पहुँचे जाने की चिंता नहीं करनी पड़ती, क्योंकि वहाँ कुछ भी नहीं होता है।
केवल उसके बाद डिक्रिप्टेड शेलकोड लिखा जाता है और निष्पादन फिर से शुरू होता है।
यह लाइब्रेरी केवल शैक्षणिक उद्देश्यों के लिए बनाई गई थी। लेखक इस लाइब्रेरी को दी जाने वाली किसी भी चीज़ के लिए ज़िम्मेदार नहीं हैं, और इसलिए हम इसके दुरुपयोग से उत्पन्न किसी भी दायित्व से मुक्त हैं।