
फरवरी 2022 से एक नए रैंसमवेयर की सूचना मिली थी जो Windows 0-day भेद्यता का उपयोग करता प्रतीत होता है, Trend Micro द्वारा किए गए शोध के अनुसार।
इस रैंसमवेयर के बारे में अधिक जानकारी इस लिंक पर मिल सकती है।
Kaspersky के विश्लेषण के अनुसार, Nokoyawa रैंसमवेयर समूह ने जून 2022 से Common Log File System (CLFS) ड्राइवर को लक्षित करने वाले अन्य exploits का उपयोग किया है, जिनमें समान लेकिन भिन्न विशेषताएँ हैं, और ये सभी एक ही exploit डेवलपर से जुड़े हैं।
अप्रैल 2023 में जब Microsoft ने पैच जारी किया, तो CVE-2023-28252 को निर्दिष्ट किया गया।
इससे पहले, 2022 में उसी घटक में एक समान बग पर हमने शोध किया था, और इस ब्लॉगपोस्ट में प्रलेखित किया था।
विश्लेषण का सामना करने के लिए, .blf फ़ाइल प्रारूप को जानना आवश्यक है, जिसे CLFS.sys नामक भेद्य Common Log File System ड्राइवर द्वारा संभाला जाता है और जो system32 के भीतर ड्राइवर फ़ोल्डर में होता है।
इस फ़ाइल प्रकार के बारे में अधिक जानकारी नीचे दिए गए लिंक में मिल सकती है:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
यह विश्लेषण Windows 11 21H2, clfs.sys version 10.0.22000.1574 के लिए किया गया है, हालाँकि यह Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 और Windows server 2022 पर भी काम करता है।
पिछले Windows संस्करणों में, कुछ मानों को समायोजित करना आवश्यक है, अन्यथा हम BSOD उत्पन्न कर देंगे।
Microsoft Patch Tuesday अप्रैल 2023.
आप ड्राइवर संस्करण को दिखाए अनुसार जाँच सकते हैं
जब भेद्यता प्रकाशित हुई, अप्रैल 2023 में, मैंने Esteban Kazimirow के साथ CLFS.sys ड्राइवर का रिवर्सिंग करना शुरू किया, हालाँकि इस मामले में केवल पैच का विश्लेषण करना यह अनुमान लगाना बहुत कठिन था कि बग कहाँ था और उसे कैसे ट्रिगर किया जाए, क्योंकि शोषण बहुत जटिल है।
बाद में, एक ब्लॉगपोस्ट प्रकाशित हुआ जिसके लेखक ने एक मैलवेयर नमूने से, HexRays द्वारा डीकंपाइल किए गए कोड के कुछ हिस्से और कुछ जानकारी दिखाई, जिसने मार्गदर्शन किया कि शोषण को कहाँ सामना करना था।
स्पष्ट रूप से प्रदान की गई जानकारी पूरी नहीं थी, लेकिन इस सहायता के बिना PoC और बाद में एक कार्यात्मक exploit बनाना संभव नहीं होता।
समझने में आसानी के लिए, हम पहले समझाएंगे कि PoC कैसे बनाया जाए और फिर हम भेद्यता विश्लेषण करेंगे।
इस ब्लॉगपोस्ट में दो खंड हैं:
PoC बनाना:
1-शोषण के लिए आवश्यक कर्नेल पते प्राप्त करें
2-.blf फ़ाइलें बनाने के लिए पथ तैयार करना:
3-CreateLogFile() फ़ंक्शन का उपयोग करके "trigger blf" फ़ाइल बनाना
4-"trigger blf" फ़ाइल तैयार करना
5-trigger blf के BASE BLOCK का कर्नेल पता प्राप्त करना
6-trigger blf के हैंडल के साथ AddLogContainer को कॉल करना
7-spray blf फ़ाइलें तैयार करना
8-spray करने के लिए मेमोरी तैयार करना
9-बग को ट्रिगर करना
डीबगिंग:
1-मेमोरी स्प्रे की जाँच करना
2-trigger blf के RecordOffset[12] को देखना
3-spray blf फ़ाइल में iFlushBlock मान को देखना
4-यह BLOCK 0 CONTROL के बजाय BLOCK 1 SHADOW से क्यों पढ़ता है ?
5-blf spray फ़ाइलों में checksum शून्य के बराबर क्यों होता है ?
6-शोषण समाप्त करना।
7-वास्तविक पैच
मैं कुछ आवश्यक कर्नेल पते प्राप्त करने के लिए InitEnvironment नामक एक फ़ंक्शन बनाऊँगा।
अपनी प्रक्रिया का EPROCESS पता प्राप्त करें और उसे g_EProcessAddress वेरिएबल में संग्रहीत करें, फिर SYSTEM प्रक्रिया का EPROCESS पता प्राप्त करें और उसे system_EPROCESS में संग्रहीत करें, फिर अपनी प्रक्रिया के मुख्य थ्रेड का EHTREAD पता प्राप्त करें और उसे g_EThreadAddress में संग्रहीत करें और अंत में PREVIOUS MODE का पता, जो PoC के इस संस्करण में उपयोग नहीं किया जाएगा।

यह विधि सर्वविदित है, GetObjectKernelAddress फ़ंक्शन पहले तर्क SystemExtendedHandleInformation के साथ NtQuerySystemInformation को दो बार कॉल करता है, पहला कॉल गलत आकार के साथ किया जाता है और त्रुटि लौटाता है, लेकिन सही आकार भी लौटाता है जो दूसरे कॉल में उपयोग होता है और सभी हैंडल की जानकारी प्राप्त करता है, फिर लूप में प्रत्येक हैंडल की जानकारी से गुजरता है और सही handleinfo के Object फ़ील्ड में कर्नेल में खोजा गया पता प्राप्त करता है।

मुझे CLFS.sys द्वारा निर्यात किए गए निम्नलिखित फ़ंक्शनों के कर्नेल पते भी चाहिए:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
और NTOSKRNL.exe से निर्यात किए गए फ़ंक्शन
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
इन पतों को प्राप्त करने के लिए वही समान विधि उपयोग की जाती है जो दोनों मॉड्यूलों का कर्नेल आधार प्राप्त करने के लिए प्रयोग होती है, NtQuerySystemInformation को दो बार कॉल करके, लेकिन इस मामले में पहला तर्क SYSTEM_INFORMATION_CLASS होगा (PoC में हम इस उद्देश्य के लिए FindKernelModulesBase फ़ंक्शन का उपयोग करते हैं)।
फिर यह LoadLibrary को कॉल करके CLFS.sys और NTOSKRNL.exe को यूज़र मोड में सामान्य मॉड्यूल के रूप में लोड करता है, GetProcAddress के साथ यूज़र मोड में पते प्राप्त करता है और फिर प्रत्येक से imagebase घटाता है, जिससे फ़ंक्शन का ऑफ़सेट प्राप्त होता है और अंत में प्रत्येक ऑफ़सेट को संबंधित कर्नेल आधारों में जोड़ता है और इस प्रकार सभी आवश्यक फ़ंक्शनों के कर्नेल पते प्राप्त करता है।

मैं createInitialTriggerBlfFile नामक एक फ़ंक्शन बनाता हूँ जो एक .blf फ़ाइल उत्पन्न और लिखेगा।
CreateLogFile में तर्क के रूप में उपयोग किया जाने वाला पथ सामान्य पथ से भिन्न होता है, उदाहरण के लिए C:\Users\Public फ़ोल्डर में स्थित 1280.blf फ़ाइल को खोलने के लिए, हमें पथ LOG:C:\Users\Public\1280. सेट करना होगा। यह stored_name_CreateLog वेरिएबल में सहेजा जाएगा।
मैं यह wsprintfW() का उपयोग करके करता हूँ क्योंकि stored_env पहले पर्यावरण चरों से प्राप्त पथ C:\Users\Public संग्रहीत करता है। इस स्ट्रिंग में मैं LOG: स्ट्रिंग जोड़ूँगा और अंत में एक यादृच्छिक नाम जोड़ूँगा, बिना .blf एक्सटेंशन के।

यह मेरी प्रारंभिक फ़ाइल का पथ होगा जिसे मैं "trigger blf" कहूँगा। बेशक, मुझे उसी फ़ाइल का सामान्य पथ भी सहेजना होगा, बिना LOG: के सामने और BLF एक्सटेंशन के साथ, ताकि इसे CreateFile(), WriteFIle() के साथ किसी भी अन्य फ़ाइल की तरह खोलकर संशोधित किया जा सके, यह पथ उदाहरण के लिए होगा: C:\Users\Public\1280.blf, और इसे stored_name_fopen वेरिएबल में संग्रहीत किया जाएगा।

बेशक, दोनों पथ एक ही फ़ाइल के अनुरूप हैं, और मुझे आवश्यकतानुसार एक या दूसरे का उपयोग करना होगा।
CreateLogFile फ़ंक्शन CreateFile() के समान कार्य पूरा करता है (नई फ़ाइलें बनाता है या मौजूदा फ़ाइलें खोलता है और उनका हैंडल प्राप्त करता है), यहाँ तक कि कुछ तर्क समान हैं, लेकिन CreateLogFile() केवल blf फ़ाइलों के साथ काम करता है।
इसके अलावा, जब यह किसी मौजूदा फ़ाइल को खोलता है, तो यह सत्यापित करता है कि प्रारूप ठीक है, यहाँ तक कि प्रत्येक ब्लॉक में checksum होता है और यदि यह सही नहीं है तो यह एक त्रुटि लौटाएगा।
मैं 2 प्रकार की BLF फ़ाइलें बनाऊँगा:
ट्रिगर blf
स्प्रे blf
दोनों blf फ़ाइलें हैं लेकिन अलग तरीके से संशोधित की जाती हैं।
इस प्रकार PoC पहले "trigger blf" फ़ाइल बनाता है, CreateLogFile का उपयोग करके, उदाहरण के लिए पथ: LOG:C:\Users\Public\1280 जो मैंने पहले सेट किया था, और stored_name_CreateLog वेरिएबल में संग्रहीत किया गया था।
पाँचवाँ तर्क fCreateDisposition, CreateFileA() की तरह, निम्नलिखित मान ले सकता है:

इस मामले में मैं OPEN_ALWAYS तर्क का उपयोग करूँगा, इसलिए फ़ाइल बनाई जाएगी यदि यह मौजूद नहीं है और यदि यह मौजूद है तो इसे खोला जाएगा। चूँकि फ़ाइल अभी तक मौजूद नहीं है, इसे एक यादृच्छिक नाम के साथ बनाया जाएगा।
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() हमारी "trigger blf" फ़ाइल को उसके 6 ब्लॉकों और उनके संबंधित checksums के साथ बनाएगा और वह हैंडल लौटाएगा जो logFile वेरिएबल में संग्रहीत किया जाएगा।

प्रत्येक ब्लॉक में बाएँ कॉलम में दिखाए गए ऑफ़सेट से एक हेडर होगा, जिसका आकार 0x70 बाइट्स है।
इसलिए, उदाहरण के लिए, CONTROL BLOCK का हेडर ऑफ़सेट 0x0 से 0x70 तक जाता है।

सभी ब्लॉकों के सभी हेडरों की संरचना समान होती है जिसे _CLFS_LOG_BLOCK_HEADER कहा जाता है।
यह हेडर संरचना है:

हेडर के ऑफ़सेट 0xC पर मुझे checksum मिलता है, इसलिए जैसे CONTROL BLOCK ऑफ़सेट 0 से शुरू होता है, checksum फ़ाइल के ऑफ़सेट 0xC पर होगा और इस प्रकार प्रत्येक ब्लॉक के अपने ब्लॉक की शुरुआत से 0xC पर checksum होगा।

trigger blf फ़ाइल को संशोधित करने के लिए, मुझे इसे एक सामान्य फ़ाइल के रूप में CreateFileA या fopen के साथ खोलना होगा और फिर क्रमशः WriteFile या fwrite के साथ संशोधित करना होगा, मैं इसे PoC के fun_prepare फ़ंक्शन की शुरुआत में करता हूँ।
याद रखें कि सामान्य पथ stored_name_fopen वेरिएबल में संग्रहीत है, इसलिए मैं इसे wfopen_s के साथ फ़ाइल खोलने के लिए उपयोग करता हूँ (जो fopen का एक प्रकार है जो यूनिकोड स्ट्रिंग्स का समर्थन करता है)।
फ़ाइल को craftTriggerBlfFile फ़ंक्शन में संशोधित किया जाता है जो fun_prepare से कॉल किया जाता है।

फिर मैं बदले जाने वाले ऑफ़सेट को इंगित करने के लिए fseek कॉल करता हूँ और फिर fwrite के साथ फ़ाइल संशोधित की जाती है।
"trigger blf" फ़ाइल में किए जाने वाले परिवर्तन इस प्रकार हैं:
ये परिवर्तन करने के बाद, FixCRCFile को नया checksum गणना करने और पहले 4 ब्लॉकों के checksums को ठीक करने के लिए कॉल किया जाता है। अगले दो ब्लॉकों में कोई परिवर्तन नहीं होता है, इसलिए उनके checksums की पुनर्गणना करना आवश्यक नहीं है।

CLFS.sys ड्राइवर फ़ाइल के छह ब्लॉकों को पढ़ता है, और उनकी सामग्री को संग्रहीत करने के लिए कर्नेल पूल में एक आवंटन करता है।

आकार 0x90 की एक बहुत महत्वपूर्ण संरचना है, जिसे पिछले CVE-2022-37969 के ब्लॉगपोस्ट में, रिवर्सिंग के माध्यम से मैंने कुछ फ़ील्ड पाए और इसे pool_0x90 कहा। बहुत अधिक रिवर्सिंग के बाद, अब मुझे पता है कि इसका वास्तविक नाम m_rgBlocks है और जैसे ही कंट्रोलर फ़ाइल से प्रत्येक ब्लॉक की सामग्री की प्रतिलिपि बनाने के लिए मेमोरी आवंटित करता है, वहाँ यह प्रत्येक ब्लॉक का आकार, प्रारंभ ऑफ़सेट और कर्नेल पता सहेजता है जहाँ इसे संग्रहीत किया गया था।

इसमें छह CLFS_METADATA_BLOCK हैं जो प्रत्येक ब्लॉक के अनुरूप उसकी संख्या से मेल खाते हैं।
प्रत्येक CLFS_METADATA_BLOCK संरचना 0x18 बाइट्स लंबी है। (0x18*6=0x90)
ऑफ़सेट 0 में एक union है, लेकिन कम से कम इस exploit में केवल pbImage फ़ील्ड का उपयोग किया जाता है, इसलिए इसे सरल बनाने पर यह होगा:
उस संरचना का आवंटन CLFS.sys ड्राइवर के दो अलग-अलग स्थानों से किया जा सकता है, नई फ़ाइल के निर्माण के अनुसार या यदि कोई मौजूदा फ़ाइल खोली जाती है। नई फ़ाइल बनाने के मामले में, ड्राइवर CClfsBaseFilePersisted::CreateImage+28A से 0x90 बाइट्स आवंटित करता है, जबकि मौजूदा फ़ाइल के मामले में यह CClfsBaseFilePersisted: ReadImage+6E से आवंटित करता है।
उसके बाद, मैं ब्लॉक 2 का प्रारंभ पता प्राप्त करूँगा जो trigger blf फ़ाइल से मेल खाता है, जिसे BASE BLOCK कहा जाता है, जो ऑफ़सेट 0x800 से शुरू होता है और इसकी लंबाई 0x7a00 है।

नीचे fun_prepare फ़ंक्शन के अंदर यह पता कर्नेल में कोड के इस टुकड़े का उपयोग करके पाया जाएगा।

सबसे पहले, getBigPoolInfo फ़ंक्शन पूल में उन सभी आवंटनों को ढूँढता है जिनमें "Clfs" टैग और आकार 0x7a00 है, फिर उन्हें एक सरणी में संग्रहीत करता है।
उसके बाद यह पहले से संशोधित trigger blf फ़ाइल को CreateLogFile का उपयोग करके OPEN_EXISTING तर्क के साथ फिर से खोलता है, इसलिए यह एक मौजूदा फ़ाइल खोलता है, इससे उसके BASE BLOCK का आवंटन होगा।
जब getBigPoolInfo फिर से कॉल किया जाता है, तो आकार 0x7a00 का एक नया "Clfs" पूल होगा, और इसका पता NtQuerySystemInformation को दो बार कॉल करके प्राप्त किया जाता है।
trigger blf फ़ाइल के BASE BLOCK का पता CLFS_kernelAddrArray वेरिएबल में संग्रहीत किया जाता है।

ध्यान दें कि यदि संशोधित trigger blf फ़ाइल में सही checksum नहीं है, तो CreateLogFile() फ़ंक्शन विफल हो जाएगा।
fun_prepare फ़ंक्शन का अंतिम भाग, trigger blf फ़ाइल के हैंडल का उपयोग करके AddLogContainer api को कॉल करता है।

PoC के अंतिम फ़ंक्शन में, जिसे to_trigger कहा जाता है, दूसरे प्रकार की blf फ़ाइल बनाई जाएगी,
मैं इसे spray blf नाम दूँगा।
इस प्रकार की फ़ाइल का उपयोग मेमोरी स्थान भरने (स्प्रे) के लिए किया जाएगा, इस प्रकार की 10 समान फ़ाइलों की आवश्यकता होती है, लेकिन प्रारंभ में केवल एक बनाई जाती है। 
इन फ़ाइलों के यादृच्छिक नामों को संग्रहीत करने के लिए तीन सरणियाँ बनाई जाएंगी:
stored_log_arrays: दस नए यादृच्छिक नामों के .blf फ़ाइलों को संग्रहीत करता है जिनका उपयोग CreateLogFile के साथ किया जाएगा।
stored_container_arrays: दस नई कंटेनर फ़ाइलें बनाने के लिए यादृच्छिक नाम संग्रहीत करता है।
stored_fopen_arrays: पहली सरणी (stored_log_arrays वेरिएबल) के लॉग फ़ाइलों के नाम संग्रहीत करता है, लेकिन उनके सामान्य पथ के साथ (बिना "LOG:" स्ट्रिंग के) और .blf एक्सटेंशन के साथ।

प्रत्येक पुनरावृत्ति में blf फ़ाइल को CopyFileW का उपयोग करके कॉपी किया जाता है, सरणियों में संग्रहीत नाम निर्दिष्ट किए जाते हैं।
fun_trigger फ़ंक्शन craftSprayBlfFile को कॉल करता है जहाँ प्रत्येक फ़ाइल में संशोधन किए जाते हैं और FixCRCFile CRCs को ठीक करेगा।

संक्षेप में, मैंने निम्नलिखित संशोधनों के साथ यादृच्छिक नामों वाली 10 समान फ़ाइलें (spray blf) बनाई हैं:

अंतिम परिवर्तन संपूर्ण ब्लॉक 0 (CONTROL BLOCK) को ब्लॉक 1 (CONTROL BLOCK SHADOW) में कॉपी करना है।

इन परिवर्तनों का प्रभाव, साथ ही trigger blf फ़ाइल में किए गए परिवर्तनों का, बाद में डीबगिंग अध्याय में समझाया जाएगा।
इनमें से कुछ परिवर्तन वे हैं जो भेद्यता उत्पन्न करते हैं, जबकि अन्य केवल ड्राइवर की जाँचों को दरकिनार करने के लिए आवश्यक हैं।
इस बिंदु पर फ़ाइलें पहले ही बनाई और संशोधित की जा चुकी हैं, spray करने के लिए तैयार हैं, फिर जब वे CreateLogFile के साथ खोली जाती हैं, तो वे उस मेमोरी क्षेत्र में स्थित होंगी जिसे हम चाहते हैं, जैसा कि बाद में दिखाया जाएगा।

to_trigger फ़ंक्शन में, 12 तत्वों की एक सरणी बनाई जाती है, जिसमें trigger blf फ़ाइल के BASE BLOCK का पता और 0x30 जोड़ा गया होता है।
फिर, fun_pipeSpray फ़ंक्शन में, मेमोरी को पाइप्स के स्प्रे से भर दिया जाता है, अंदर एक लूप होता है जो CreatePipe को कॉल करता है और पाइप्स की वह संख्या बनाता है जो पहले तर्क के रूप में पारित की जाती है, दूसरा तर्क एक सरणी है जो बनाए गए सभी पाइप्स के हैंडल संग्रहीत करेगी।

एक लूप के भीतर, यह रीड-राइट पाइप्स बनाने के लिए CreatePipe को कॉल करता है।
इस प्रकार पहले 0x5000 पाइप्स बनाए जाएंगे और फिर अन्य 0x4000 पाइप्स बनाने के लिए फिर से कॉल किया जाएगा।
फिर हाल ही में बनाई गई सरणी को trigger blf फ़ाइल के BASE BLOCK + 0x30 के पतों के साथ पहले 5000 पाइप्स में लिखने के लिए WriteFile का उपयोग करता है।

अब मेमोरी में पहले से ही एक कॉम्पैक्ट ब्लॉक बन चुका है, यह संख्या 0x2000 से लेकर 0x2667 तक 0x667 पाइप्स को मुक्त करेगा, चूँकि मेमोरी में पाइप्स उसी क्रम में नहीं होते जिस क्रम में वे बनाए गए थे, जो होगा वह यह कि इस मेमोरी ब्लॉक में खाली स्थान होंगे।
ध्यान दें कि पाइप्स के आवंटन का उपयोगकर्ता आकार 0x90 बाइट्स है, इसलिए जब मुक्त किया जाएगा तो हमारे पास होगा
यह पाइप्स से भरी मेमोरी के बीच 0x90 आकार के मेमोरी स्थानों को मुक्त करता है।
फिर यह 10 spray blf फ़ाइलों के साथ CreateLogFile को कॉल करने के लिए लूप करता है।
जब CreateLogFile को मौजूदा फ़ाइलों को खोलने के लिए कॉल किया जाता है, तो प्रत्येक spray blf फ़ाइल के लिए m_rgBlocks हेतु 0x90 बाइट्स का आवंटन किया जाता है, इसलिए ये आवंटन उन खाली स्थानों को भर देंगे जो पाइप्स को मुक्त करते समय छोड़े गए थे क्योंकि वे समान आकार के हैं।

फिर अंतिम 0x4000 पाइप्स में वह सरणी लिखने की प्रक्रिया दोहराएँ जिसमें trigger blf के BASE BLOCK +0x30 का पता होता है।
ये सभी हेरफेर एक नियंत्रित मेमोरी स्थान बनाते हैं, मैं आपको दिखाऊँगा कि यह डीबग करते समय कैसा होता है, लेकिन विचार यह है कि प्रत्येक spray blf फ़ाइल का m_rgBlocks उन 0x90 बाइट्स के खाली स्थानों को भरता है जो मुक्त किए गए थे।
फिर पहले से अंतिम भाग में, बग को while( 1 ) के भीतर spray blf फ़ाइलों में AddLogContainer के कॉल का उपयोग करके ट्रिगर किया जाता है।
इस while के भीतर बग ट्रिगर होता है:

यह while तब बाहर निकलेगा जब उसे System टोकन मिल जाएगा, NtFsControlFile फ़ंक्शन का उपयोग करते हुए, जो पाइप की विशेषताओं को पढ़ता है।

फिर CreateLogFile का उपयोग करके, हमारी प्रक्रिया के टोकन को हाल ही में मिले System टोकन से अधिलेखित कर देता है और इस प्रकार हम विशेषाधिकार उन्नयन प्राप्त करते हैं।

फिर कुछ मानों को पुनर्स्थापित करें, पाइपों और blf फ़ाइलों के हैंडल बंद करें, और यह सत्यापित करने के लिए Notepad को System के रूप में चलाएँ कि हमने सही ढंग से उन्नयन किया है।

PUBLIC फ़ोल्डर पर बनाई गई blf फ़ाइलों पर ध्यान दें। याद रखें कि यदि आप दोबारा प्रयास करना चाहते हैं, तो आपको पहले बनाई गई फ़ाइलों को हटाना होगा। कुछ लॉक हो जाएँगी और हटाई नहीं जा सकेंगी, लेकिन PoC फिर भी काम करेगा।

शोषण करने के लिए trigger blf और spray blf फ़ाइलों में परिवर्तनों के प्रभाव से शुरुआत करने से पहले, मुझे यह सत्यापित करना होगा कि spray blf फ़ाइलों के m_rgBlocks, पाइप स्प्रे करने और बाद में एक निश्चित संख्या में पाइपों को रिलीज़ करने के बाद, मेमोरी वितरण में बने होल्स में स्थित हैं।
जब यह प्रक्रिया समाप्त होती है, तो m_rgBlocks के 0x90 बाइट्स के नीचे एक पाइप स्थित होना चाहिए, इसलिए जब m_rgBlocks का उपयोग किया जाता है, तो एक OUT OF BOUNDS स्थिति उत्पन्न होगी और यह नीचे स्थित उस पाइप से पढ़ेगा।
PoC के पास ब्रेकपॉइंट लगाने के लिए एक आदर्श बिंदु है:

इस बिंदु पर, spray blf फ़ाइलों को खोलना पूरा हो चुका है और AddLogContainer फ़ंक्शन को अभी भी कॉल नहीं किया गया है।
यूज़र मोड में डिबग करने के लिए, मैं x64dbg का उपयोग करूँगा और कर्नेल मोड के लिए, Windbg प्लगइन के साथ IDA का उपयोग करूँगा।

इस बिंदु पर मेमोरी पहले से तैयार होनी चाहिए, और मैं वितरण देख सकता हूँ।
ब्रेकपॉइंट लगाने के लिए एक दिलचस्प बिंदु खोजने हेतु मैं IDA को रोकूँगा।
मैं CClfsBaseFilePersisted::AddContainer पर ब्रेकपॉइंट सेट करूँगा, जिसे AddLogContainer से कॉल किया जाता है और शुरुआत में, RCX रजिस्टर CClfsBaseFilePersisted संरचना की ओर इंगित करता है और ऑफसेट 0x30 पर m_rgBlocks का एक पॉइंटर होता है।

जब ब्रेकपॉइंट पहुँच जाता है, तो मैं कॉल स्टैक पर जाँच करता हूँ कि AddLogContainer मेरे PoC से कॉल हो रहा है।

RCX रजिस्टर इंगित करता है:

पहला फ़ील्ड vtable (CLFS! CClfsBaseFilePersisted::'vftable') का पॉइंटर है और ऑफसेट 0x30 पर m_rgBlocks का पॉइंटर है।

ब्लॉक 0, 1, 4 और 5 ने अभी pbImage को सहेजा नहीं है, जबकि ब्लॉक 2 (BASE BLOCK) और 3 (SHADOW BLOCK) ने सहेजा है।
m_rgBlocks तालिका में प्रत्येक ब्लॉक का अपना cbOffset होता है जो फ़ाइल में ब्लॉक की शुरुआत का ऑफसेट है, cbImage ब्लॉक का आकार है, और eBlockType ब्लॉक का प्रकार है।
यदि स्प्रे सही है, तो m_rgBlocks के नीचे एक पाइप होना चाहिए और उसके भीतर trigger blf के BASE BLOCK + 0x30 के पॉइंटर होने चाहिए।

windbg पर "!pool" कमांड मेमोरी वितरण दिखाता है:

प्रत्येक m_rgBlocks में एक "Clfs” टैग होता है और इसका आकार 0xa0 है क्योंकि यह 0x90 यूज़र साइज़ plus 0x10 हेडर है और नीचे "NpFr" टैग वाला एक पाइप है जिसका आकार समान 0x90 यूज़र साइज़ + 0x10 हेडर है।
चूँकि वितरण कोई exact विज्ञान नहीं है, कुछ "Clfs” लगातार रखे गए, जो अवांछनीय है, लेकिन जिस पर मैं काम कर रहा हूँ, वह सही ढंग से रखा गया है और उसके बाद एक पाइप है।
जिन पहले परिवर्तनों का प्रभाव पड़ता है, उनमें से एक trigger blf फ़ाइल में ऑफसेट 0x858 पर किया गया परिवर्तन है, जहाँ मान 0x369 संग्रहीत होता है।

BASE BLOCK फ़ाइल में ऑफसेट 0x800 से शुरू होता है।

_CLFS_LOG_BLOCK_HEADER के अंदर ऑफसेट 0x800+0x58 पर (BASE BLOCK हेडर की शुरुआत से 0x58)।

ऑफसेट 0x28 पर सरणी RecordOffsets (DWORD) शुरू होती है।
0x30 बाइट्स आगे बढ़ने पर, ऑफसेट 0x58 (शुरुआत से 0x828+0x30=0x858) पर, RecordOffsets का फ़ील्ड 12 होता है।

मैं नीचे दिखाए गए अनुसार CreateLogFile के लिए PoC चलाता हूँ:


CreateLogFile में प्रवेश करने से पहले मैं एक ऐसी जगह ब्रेकपॉइंट लगाने जा रहा हूँ जहाँ 0x369 मान का अभी उपयोग नहीं हुआ है।
ऐसे मामले में जब CreateLogFile किसी मौजूदा फ़ाइल को खोलता है, तो m_rgBlocks संरचना यहाँ आवंटित होती है:
CClfsBaseFilePersisted::ReadImage+6E
इसलिए, मैं IDA पर ब्रेकपॉइंट यहीं सेट करूँगा:

जब ब्रेकपॉइंट ट्रिगर होता है**:**

m_rgBlocks में अभी भी कुछ कचरा है क्योंकि यह अभी भी अप्रारंभीकृत है, लेकिन जैसे ही ब्लॉक 2 का pbImage आवंटित होता है, पता शुरुआत से ऑफसेट 0x30 में सहेजा जाएगा, क्योंकि प्रत्येक CLFS_METADATA_BLOCK के अंदर पहला फ़ील्ड pbImage होता है।


अब मैं राइट पर एक हार्डवेयर ब्रेकपॉइंट सेट करता हूँ: ba w1 ffffd003'7f5bea30
शून्य से प्रारंभ करने के बाद, यह pbImage सहेजने पर रुकता है।

विश्लेषण कहता है कि यह block0 के अनुरूप है, क्योंकि यह स्थिरांक r14*8 पर विचार नहीं करता जो बाद में 0x30 है, परिणामस्वरूप यह वास्तव में block 2 का pbImage लिख रहा है।

ध्यान दें कि CClfsBaseFilePersisted::ReadMetadataBlock का उपयोग किसी भी ब्लॉक को आवंटित करने के लिए किया जाता है, तर्क के रूप में पारित आकार का उपयोग करके।

अब बेस ब्लॉक से 0x58 पर एक read/write ब्रेकपॉइंट सेट करें, यह देखने के लिए कि यह 0x369 मान का उपयोग कब करता है।
ba r1 FFFF978A'16ECF000+0x58

जब ब्रेकपॉइंट हिट होता है, तो यह RecordOffset[12] पर स्थित 0x369 मान को पढ़ता है, इसे r14 पर एक अजीब पॉइंटर में जोड़ता है और RAX+r14 की सामग्री को बढ़ाता है।
कोड में कुछ पंक्तियाँ ऊपर, ESI में मान 0x13 होता है और 0x18 से गुणा करता है, जो m_rgBlocks में प्रत्येक ब्लॉक का आकार है।
WINDBG>? 0x18*0x13
Evaluate expression: 456 = 00000000'000001c8
यदि मैं r8= 0x1c8 का मान, जो 0x90 से अधिक है, m_rgBlocks के प्रारंभिक पते में जोड़ता हूँ, तो यह OUT OF BOUNDS पढ़ेगा।


m_rgBlocks के नीचे, BASE BLOCK + 0x30 के पॉइंटर वाला पाइप है, यह उस पॉइंटर को पढ़ता है जिसे जानबूझकर पाइप के अंदर रखा गया था।
कोड में वर्तमान स्थिति मुख्य मॉड्यूल के while(1) कथन से कॉल की गई थी।
spray blf फ़ाइल के अंदर मैंने जानबूझकर मान 0x13 को ऑफसेट 0x48a (iFlushBlock) पर रखा है।

spray blf फ़ाइल के ऑफसेट 0x8a पर, BLOCK 0 का iFlushBlock स्थित है, जिसका मान 4 है, जबकि ऑफसेट 0x48a BLOCK 1 के iFlushBlock का है, और इसका मान 0x13 है।

अब मुझे यह पता लगाना है कि यह BLOCK 0 से iFlushBlock = 4 के बजाय BLOCK 1 से iFlushBlock = 0x13 क्यों पढ़ता है।
अगर मैं पीछे देखूँ कि 0x13 कहाँ से आया, तो मैं कॉल स्टैक पर देखता हूँ कि WriteMetadataBlock को CClfsBaseFilePersisted::ExtendMetadataBlock+416 से कॉल किया गया है, वहाँ दूसरा iFlushBlock तर्क EDX=0x13 है, जो r9w से आता है।


कुछ पंक्तियाँ पहले, BLOCK 0 का पता प्राप्त करने के लिए CClfsBaseFile::GetControlRecord को कॉल किया गया था, शायद समस्या यहीं है, इसलिए मैं रीबूट करूँगा और उस पर ब्रेकपॉइंट लगाऊँगा।
GetControlRecord, CClfsBaseFile::AcquireMetadataBlock को कॉल करता है, जिसे m_rgBlocks तालिका को block 0 के पते से भरना चाहिए, जब मैं इस फ़ंक्शन को step over करता हूँ तो यह block 1 का पता प्राप्त करता है, इसलिए समस्या CClfsBaseFile::AcquireMetadataBlock के अंदर होती है।
प्राप्त पते में 0x8A जोड़कर, मैं पुष्टि कर सकता हूँ कि BLOCK 1 से संबंधित 0x13 मान मौजूद है।

मैं रीबूट करूँगा और वहाँ ब्रेकपॉइंट सेट करूँगा:

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

AcquireMetadataBlock को दिया गया दूसरा तर्क zero है, यह block 0 से मेल खाता है, यह फ़ाइल से कॉपी करके अपना पता m_rgBlocks में संग्रहीत करेगा
.
_CLFS_METADATA_BLOCK_TYPE ब्लॉक टाइप enumeration में, उनके नाम मेरे द्वारा उपयोग किए गए नामों से भिन्न हैं, लेकिन वे समान 6 ब्लॉक हैं।

यह जाँचने के बाद कि ब्लॉक प्रकार अधिकतम m_cBlocks=6 से कम है, यह उसी ब्लॉक को दो बार पढ़ने से बचने के लिए एक reference मान सहेजता है।

ReadMetadataBlock कॉल किया जाता है, block 0 के बजाय block 1 पढ़ने की समस्या इसी फ़ंक्शन के अंदर होगी।

यदि सब कुछ ठीक है, तो यह आकार के रूप में cbImage का उपयोग करके आवंटित करता है और पते को m_rgBlocks में फ़ील्ड block 0-> pbImage में संग्रहीत करता है।

!pool कमांड आवंटित tag और size प्रदर्शित करता है।

तो, मेरे पास पहले से ही block 0 का pbImage पता m_rgBlocks में संग्रहीत है, इसलिए मुझे यह देखना होगा कि यह block 0 के बाइट्स के बजाय block 1 के बाइट्स वहाँ क्यों कॉपी करता है।
मैं CClfsContainer::ReadSector की कॉल तक पहुँचता हूँ, जहाँ बाइट्स लिखने के लिए pbImage वाले वेरिएबल का पॉइंटर पारित किया जाता है।

ReadSector को step over करते समय pbimage सामग्री में किए गए परिवर्तनों पर ध्यान दें।

pbImage में 0x8a जोड़कर मुझे मान 4 मिलता है जो सही मान है, 0x13 के बजाय, इसलिए समस्या बाद में होनी चाहिए।
ClfsDecodeBlock को कॉल करने के बाद यह त्रुटि 0x0C01A000A लौटाता है।
CClfsBaseFilePersisted::ReadMetadataBlock+153 calls to ClfsDecodeBlock
इस त्रुटि के बाद, यह प्रकार में 1 जोड़ता है और block 1 को पढ़ने के लिए CClfsBaseFilePersisted::ReadMetadataBlock को फिर से कॉल करता है, लेकिन प्रकार 1 के साथ।

CClfsBaseFilePersisted::ReadMetadataBlock में यह block 1 के लिए m_rgBlocks में एक नया pbImage आवंटित और संग्रहीत करता है।

Blocks 0 और 1 के पते अलग-अलग हैं, अब यदि मैं block 1 के पते में 0x8a जोड़ता हूँ तो इसका मान 0x13 है।
शायद चूँकि block 0 ने त्रुटि लौटाई, यह block 1 का उपयोग करता है और इसे Control Block के रूप में GetControlRecord को लौटा देता है।
जैसा पहले दिखाया गया है, जब यह 4 के बजाय 0x13 मान का उपयोग करता है, तो यह m_rgBlocks की सीमा से बाहर चला जाता है और मेरे द्वारा नियंत्रित पाइप स्प्रे मानों को पढ़ता है।
फिर यह block 0 से pbImage को मुक्त करता है और block 1 से block 0 में पॉइंटर कॉपी करता है।

उस मान को खोजना आवश्यक होगा जो ClfsDecodeBlock के अंदर त्रुटि 0x0C01A000A उत्पन्न करता है।
ClfsDecodeBlock के अंदर पहले ब्लॉक का checksum शून्य है, यही त्रुटि 0xC01A000A है।

AddLogContainer को कॉल करने से पहले, किसी भी spray blf फ़ाइल को हेक्साडेसिमल एडिटर से खोलने पर, checksum को zero में बदल दिया गया था।

इसे पहले ही बदल दिया जाना चाहिए था जब इसे CreateLogFile के साथ खोला गया था।

किसी कारण से spray blf फ़ाइलें, CreateLogFile से बाहर निकलने के बाद, block 0 के checksum के 0 के बराबर होने के साथ समाप्त होती हैं और एक valid handle लौटाती हैं, आइए देखें कि ऐसा क्यों होता है।
मैं कुछ spray blf फ़ाइल खोलने से पहले CreateLogFile पर रुकता हूँ।

ध्यान दें कि CreateLogFile को कॉल करने से पहले, spray files में block 0 में सही checksum होता है और फ़ंक्शन पूरा होने के बाद, checksum मान शून्य में बदल जाता है।

इसलिए, मैं अंदर देखने के लिए CClfsBaseFile::GetControlRecord पर ब्रेकपॉइंट सेट करता हूँ।
CClfsContainer::ReadSector को पार करने के बाद checksum शून्य नहीं है।
CRC32 की गणना करने से पहले, यह CRC की गणना करने के लिए मेमोरी में checksum फ़ील्ड को शून्य कर देता है, और परिणाम सही होता है।


फिर यह eExtendState =2 के मान की जाँच करता है और WriteMetadataBlock पर जाता है।

यहाँ मेमोरी में checksum अभी भी शून्य है, मुझे बस यह देखना है कि यह मान फ़ाइल में कब लिखा जाता है।

यह कुछ मानों की जाँच करता है जो blf spray फ़ाइल में तैयार किए गए हैं ताकि CClfsBaseFilePersisted::ExtendMetadataBlock तक पहुँचा जा सके।

अभी तक न पढ़े गए ब्लॉकों को पढ़ने के लिए एक लूप के बाद, block 0 checksum = 0 के साथ जारी रहता है।

WriteMetadataBlock पर पहुँचना।

चूँकि मैं block 0 को 1 से बदलने से पहले चला रहा हूँ, blf spray फ़ाइल का iFlushBlock मान अभी भी 4 है जो सही मान है।

अब यह block 4 के साथ काम कर रहा है, और यह block 4 को फ़ाइल में लिखेगा, अभी समस्या यहाँ नहीं है।
फिर यह CClfsBaseFilePersisted::FlushControlRecord पर आता है

इसके अंदर यह WriteMetadataBlock तक पहुँचता है, लेकिन argument 0 के साथ, block 0 को फ़ाइल में लिखने के लिए।

फिर ClfsEncodeBlock त्रुटि 0xC01A000A लौटाता है, हालाँकि यह ठीक नीचे CClfsContainer::WriteSector में खराब block 0 के साथ फ़ाइल लिख देगा।

वेरिएबल var_54 0xC01A000A त्रुटि मान संग्रहीत करता है और फ़ंक्शन से बाहर निकलने से पहले जाँचा जाएगा।

लेकिन CClfsContainer::WriteSector को कॉल करने के बाद, जो कोई त्रुटि नहीं लौटाता, var_54 की सामग्री शून्य से अधिलेखित हो जाती है।
इसलिए, फ़ंक्शन बिना किसी त्रुटि के शून्य लौटाता है और यह काम करता रहता है क्योंकि CreateLogFile त्रुटि मान के बजाय एक handle लौटाएगा।

iFlushBlock में मान 0x13 इसे out of bounds जाने का कारण बनता है और यह उस पॉइंटर को पढ़ेगा जो पाइपों में है और जो trigger blf के Base Block +30 की ओर इंगित करता है।
फिर यह उस पॉइंटर में 0x28 जोड़ता है, ( trigger blf के आधार ब्लॉक की शुरुआत से
0x58) जिसका मान 0x369 है।


INC निर्देश मान 0x14 को 1 से बढ़ाएगा और 4 बार दोहराएगा, इसलिए 0x14 बढ़कर 0x18 हो जाता है।
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
उसके बाद, CreateLogFile कॉल किया जाता है, और 0x1858 मान पढ़ता है।

GetSymbol जाँच करता है कि क्या फेक ब्लॉक
पहले trigger blf में बनाया गया, जो ऑफसेट 0x1858 द्वारा इंगित किया गया है,
सही मान रखता है।

अगर पॉइंटर को कई बार नहीं बढ़ाया गया होता, तो इसका मूल मान 0x1458 होता और यह सही ब्लॉक की ओर इंगित करता।
GetSymbol से बाहर निकलने के बाद, यह उस फेक ब्लॉक का उपयोग यहाँ करेगा।

फिर यह फेक ब्लॉक के ऑफसेट 0x18 का मान पढ़ेगा
जहाँ मैंने 0x05000000 रखा है, और वहाँ जो सामग्री है
उस पर कूद जाएगा।
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

यह 0x05000000 और उसके 0x05001000 की सामग्री पढ़ता है, और वहाँ ClfsEarlierLsn है।

यह फ़ंक्शन RDX में मान 0xFFFFFFFF लौटाने के लिए उपयोग किया जाता है, हालाँकि इस पहली बार उस मान का उपयोग नहीं किया जाता है।
दूसरा कॉल यहाँ होता है, यह PoFxProcessorNotification को कॉल करता है जो 0x501000 +8 पर था

WINDBG>dps 00000000'05001000
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

इस फ़ंक्शन में RCX = 0x05000000 , यह जाँचता है कि 0x40 बाइट्स बाद गैर-शून्य होना चाहिए
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
कूदने का पता 0x68 बाद होगा।
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
और तर्क 0x48 बाइट्स बाद होगा।
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
ClfsMgmtDeregisterManagedClient, यह एक सुविधाजनक फ़ंक्शन है क्योंकि मैं तर्क को नियंत्रित कर सकता हूँ और मेरे पास मेरे द्वारा नियंत्रित फ़ंक्शनों के लिए दो कूद भी हैं।

पहला कॉल फिर से ClfsEarlierLsn के लिए है, जो RDX=0xFFFFFFFF में लौटा था।


यह RDX=0xFFFFFFFF की सामग्री से लिखने के लिए स्रोत लेगा।
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
पते 0xFFFFFFFF पर मैंने system_EPROCESS & 0xfffffffffffff000 संग्रहीत किया था।

गंतव्य 0x5000400 +0x48 पर स्थित पॉइंटर है
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

कर्नेल में PipeAttribute पॉइंटर, जो एक बफर की ओर इंगित करता है जो “A” से भरा है, SYSTEM EPROCESS पॉइंटर के उच्च भाग के साथ अधिलेखित कर दिया जाएगा।
यह पॉइंटर तब बनाया गया था जब मैंने पहले _NtFsControlFile को “A” से भरे बफर के साथ कॉल किया था।

उस विशेषता की सामग्री को NtFsControlFile का उपयोग करके पढ़ा जा सकता है।

अब पाइप विशेषता अब “A” वाले बफर की ओर नहीं, बल्कि system_EPROCESS & 0xffffffffffffff000 की ओर इंगित करती है।

यह कोड तब तक दोहराया जाएगा जब तक सिस्टम टोकन प्राप्त नहीं हो जाता।


विंडोज 11 पर सिस्टम टोकन हाल ही में पढ़ी गई EPROCESS संरचना के ऑफसेट 0x4b8 पर होता है।

मुझे केवल CreateLogFile को कॉल करके उस सिस्टम टोकन को अपनी प्रक्रिया में लिखने की आवश्यकता है।

यह कार्य करने के लिए, सिस्टम टोकन पढ़ने के लिए उपयोग किए गए चरण को दोहराएं।

डबल कॉल में, यह पहले ClfsEarlierLsn को कॉल करता है ताकि RDX में 0xFFFFFFFF लौटे और फिर nt_SeSetAccessStateGenericMapping को कॉल करता है।

मैं जाँचता हूँ कि RDX द्वारा इंगित मान सिस्टम टोकन है।

मेरी प्रक्रिया का टोकन है:

यह वहाँ लिखने जा रहा है।
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
अब मेरी प्रक्रिया System है, मैं सत्यापित करने के लिए Notepad चला सकता हूँ।



BINDIFF बहुत सारे बदले हुए फ़ंक्शन दिखाता है

कमजोर फ़ंक्शन यहाँ है:


प्राथमिक पैच किया गया संस्करण है, द्वितीयक कमजोर संस्करण है।
पैच CflsEncodeBlock के रिटर्न मान का परीक्षण करता है, जो 0xC01A000A है, इसे var_54 चर में संग्रहीत करता है, और चूंकि यह नकारात्मक है, यह जाँचता है और WriteSector से बचता है।
पैच, फ़ाइल को न लिखने के अलावा, फ़ंक्शन सही ढंग से 0xc01a000a लौटाता है, जिसके साथ CreateLogFile कोई हैंडल नहीं लौटाता और शोषण जारी नहीं रह सकता।


केवल अगर ClfsDecodeBlock नकारात्मक नहीं है, तो यह WriteSector पर जाता है लेकिन नकारात्मक मान 0xC01A000A लौटाता हुआ छोड़ता है।
यह वास्तविक पैच है जो
मेरे द्वारा अभी संलग्न किए गए PoC का उपयोग करके शोषण को वास्तव में रोकता है।
इस बिंदु पर हमने समझाया है कि बग का शोषण कैसे किया गया, यह उन फ़ंक्शनों को नियंत्रित करने की ओर ले जाता है जो हमें SYSTEM टोकन पढ़ने और स्थानीय विशेषाधिकार वृद्धि प्राप्त करने के लिए इसे अपनी प्रक्रिया में लिखने की अनुमति देते हैं। आप कार्यात्मक PoC यहाँ पा सकते हैं: Fortra का GitHub।
हमें उम्मीद है कि आपको यह उपयोगी लगेगा, यदि आपको कोई संदेह हो तो हमसे संपर्क कर सकते हैं: