
No execution के दौरान पेज प्रोटेक्शन बदलावों को लागू करते हुए, वर्तमान थ्रेड को समाप्त करने और निष्पादन फिर से शुरू करने से पहले इसे पुनर्स्थापित करने की एक एवेज़न तकनीक के लिए PoC कार्यान्वयन।
██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗
██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗
██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝
██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝
██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝
वर्तमान थ्रेड को समाप्त करने और निष्पादन फिर से शुरू करने से पहले इसे पुनर्स्थापित करने के लिए एक चोरी तकनीक का PoC कार्यान्वयन, जबकि नो एक्ज़ीक्यूशन के दौरान पेज सुरक्षा परिवर्तन लागू किया जाता है।

स्लीप और अस्पष्टीकरण विधियाँ maldev समुदाय में प्रसिद्ध हैं, विभिन्न कार्यान्वयनों के साथ, उनका उद्देश्य सोते समय मेमोरी स्कैनर्स से छिपना है, आमतौर पर पेज सुरक्षाएँ बदलना और शेलकोड को एन्क्रिप्ट करने जैसी शानदार सुविधाएँ जोड़ना, लेकिन हमारे शेलकोड को छिपाने का एक और महत्वपूर्ण बिंदु है, और वह है वर्तमान निष्पादन थ्रेड को छिपाना। स्टैक को स्पूफ करना अच्छा है, लेकिन इसके बारे में थोड़ा सोचने के बाद मैंने सोचा कि स्टैक को स्पूफ करने की कोई आवश्यकता नहीं है... अगर कोई स्टैक ही नहीं है :)
इस तकनीक की उपयोगिता का आकलन पाठक पर छोड़ दिया गया है, लेकिन किसी भी मामले में, मुझे लगता है कि यह कुछ विषयों की समीक्षा करने और उन लोगों के लिए कुछ maldev सीखने का एक अच्छा तरीका है जो, मेरी तरह, इस दुनिया में शुरुआत कर रहे हैं।
यहाँ दिखाया गया मुख्य कार्यान्वयन वह सब कुछ रखता है जिसे हमें स्टैक से बाहर निकालने की आवश्यकता है डेटा अनुभाग में, वैश्विक चर के रूप में, लेकिन सब कुछ हीप में ले जाने वाला एक कार्यान्वयन जल्द ही प्रकाशित किया जाएगा। इसका उद्देश्य कुछ प्रमुख संशोधन दिखाना है जिन्हें इस कोड को pic और इंजेक्टेबल बनाने के लिए करने की आवश्यकता है।
यह रिपॉजिटरी GitHub और GitLab के बीच मिरर की गई है।
यहाँ बताई गई सभी बातें विभिन्न विषयों की मेरी समझ से आती हैं, या तो पढ़ने से या विकास के दौरान अनुभव से। मैं जानता हूँ कि मैं कोई विशेषज्ञ नहीं हूँ और सबसे आखिरी चीज जो मैं करना चाहता हूँ वह है गलत सूचना फैलाना, इसलिए यदि आपको लगता है कि कुछ सही नहीं है, तो मुझे खुशी होगी अगर आप मुझे इसके बारे में बताएँ, आप मुझसे ट्विटर पर संपर्क कर सकते हैं, या इस रेपो में इश्यू खोल सकते हैं। आपकी समझ के लिए बहुत-बहुत धन्यवाद। :)
इस तकनीक का मुख्य उद्देश्य स्पष्ट है, वर्तमान थ्रेड को समाप्त करना और निष्पादन फिर से शुरू करने से पहले इसे पुनर्स्थापित करना, लेकिन इसका वास्तव में क्या मतलब है, और यह कौन सी नई बाधाएँ लाता है?
निष्पादन को पुनर्स्थापित करने में सक्षम होने के लिए, हमें थ्रेड को समाप्त करने से पहले दो चीजों को सहेजना होगा, पहला, CPU स्थिति, और दूसरा स्टैक, और नया थ्रेड लॉन्च होने के बाद उन्हें प्रभावी ढंग से फिर से सेट करना होगा।
मैंने इस तकनीक में दिखाई देने वाली नई बाधाओं के बारे में बात की, और दो बड़ी हैं: पहला, हमें उस क्षण से जब थ्रेड समाप्त होता है जब तक स्टैक पुनर्स्थापित नहीं हो जाता, स्टैक के बाहर किसी भी चीज़ को संग्रहीत करने की आवश्यकता है, और जैसा कि आप देखेंगे, यह कुछ नई चुनौतियाँ पैदा करता है।
दूसरा, हमें हमेशा अपनी प्रक्रिया में कम से कम एक और थ्रेड चलाने की आवश्यकता होती है, क्योंकि हम अपने थ्रेड को समाप्त कर रहे हैं, यदि कोई अन्य थ्रेड नहीं है तो प्रक्रिया समाप्त हो जाएगी। मुझे नहीं लगता कि यह एक बड़ी समस्या है, क्योंकि अधिकांश एजेंट अन्य प्रक्रियाओं में इंजेक्ट किए जाते हैं, हम मान सकते हैं कि यह प्रक्रिया कम से कम एक थ्रेड चलाती रहेगी।
हम इस POC में 4 मुख्य कार्य देख सकते हैं:
जब हम स्टैक को सहेजने वाले होते हैं, तो एक प्रश्न उठता है: स्टैक का कितना भाग सहेजने की आवश्यकता है?
आइए पहले समीक्षा करें कि DeathSleep फंक्शन को कॉल करने के बाद स्टैक में क्या है (यह वह फंक्शन है जो संदर्भ, स्टैक को सहेजता है और अस्पष्टीकरण और पुनर्स्थापना के लिए सब कुछ तैयार करता है)

जैसा कि हम देख सकते हैं, प्रत्येक फंक्शन के तीन भाग होते हैं:
स्टैक का न्यूनतम भाग जिसे हमें निश्चित रूप से सहेजने की आवश्यकता है, वह हमारे मुख्य प्रोग्राम के अंदर सब कुछ है, अर्थात इसका शैडो स्पेस, इसका रिटर्न एड्रेस और DeathSleep फंक्शन तक सब कुछ। इससे पहले कुछ भी वास्तव में आवश्यक नहीं है (एंट्री फंक्शन द्वारा उपयोग किए गए स्टैक को सहेजने के अपने फायदे हैं, लेकिन हम बाद में इस पर चर्चा करेंगे), क्योंकि यह हमारे नए थ्रेड को लॉन्च करने के लिए विंडोज रूटीन द्वारा उपयोग किया जाने वाला स्टैक है। इसके अलावा, मैंने DeathSleep फंक्शन के शैडो स्पेस को भी संग्रहीत करने का निर्णय लिया (वास्तव में आवश्यक नहीं है, लेकिन यह जागने के समय Rsp की गणना को आसान बनाता है)।
तो अंत में, हम यह सहेज रहे हैं:

एक मानक संकलन पर प्रत्येक फंक्शन 3 भागों से बना होना चाहिए, प्रस्तावना, फंक्शन कोड, और उपसंहार।
Rsp (स्टैक पॉइंटर) को केवल फंक्शन प्रस्तावना और उपसंहार पर संशोधित किया जाना चाहिए। प्रस्तावना स्टैक पॉइंटर को बढ़ाती है (याद रखें स्टैक बढ़ाने का मतलब पतों को घटाना है, क्योंकि वे विपरीत दिशाओं में जाते हैं), रजिस्टरों को सहेजने, अपने सभी स्थानीय चरों को रखने और फिर शैडो स्पेस को रखने के लिए, और उपसंहार बिल्कुल विपरीत करता है।
इसका मतलब है कि फंक्शन कोड के अंदर स्टैक पॉइंटर को हमेशा शैडो स्पेस के अंत (उपरोक्त चित्र में बैंगनी) की ओर इंगित करना चाहिए, और फंक्शन के स्टैक आकार और शैडो स्पेस का योग इस गणना से पाया जा सकता है कि प्रस्तावना स्टैक पॉइंटर को कितना बढ़ाती है। यह मान अनवाइंड टेबल में मौजूद जानकारी का उपयोग करके आसानी से गणना किया जा सकता है, उनके उपयोग के बारे में एक स्पष्टीकरण यहाँ शामिल नहीं किया जाएगा, लेकिन सारांश के रूप में, ये तालिकाएँ किसी भी अन्य थ्रेड या प्रक्रिया को स्टैक के माध्यम से सही ढंग से स्थानांतरित करने, इसकी सामग्री देखने, अपवादों को संभालने या इसका विश्लेषण करने की अनुमति देने के लिए उपयोग की जाती हैं।
संदर्भ को कैप्चर करना शायद सबसे आसान कामों में से एक है, क्योंकि हम DeathSleep की पहली पंक्ति में, गैर-अस्थायी रजिस्टरों में किसी भी संशोधन से पहले, बस RtlCaptureContext() कर सकते हैं। हमें अभी भी उस संदर्भ में दो संशोधन करने की आवश्यकता है जहाँ हम निष्पादन को पुनर्स्थापित करेंगे।
पहला इसके Rip को संशोधित करना होगा, जैसा कि आपको याद है, यह वह रजिस्टर है जो चलने वाले अगले निर्देश को रखता है, और यदि हम इसे अपरिवर्तित छोड़ देते हैं, तो निष्पादन DeathSleep फंक्शन के अंदर फिर से शुरू होगा। हम जो करेंगे वह Rip को DeathSleep के रिटर्न एड्रेस को रखने के लिए बदलना है, जो वर्तमान Rsp द्वारा उपसंहार में बढ़ाए गए आकार (उपरोक्त चित्रों में हरा + बैंगनी क्षेत्र) के विस्थापन पर इंगित किया गया है।
दूसरा संशोधन तब किया जाएगा जब थ्रेड पुनर्स्थापित किया जाता है, और इसमें Rsp को हमारे पुनर्स्थापित स्टैक के शीर्ष पर इंगित करने के लिए सेट करना शामिल है; यह पुनर्स्थापना चरण के दौरान किया जाएगा, क्योंकि हम नहीं जानते कि हमारा नया स्टैक कहाँ रखा जाएगा। मान बस हमारे पुनर्स्थापित स्टैक का अंतिम पता होगा, क्योंकि जैसा कि हमने बाद में चर्चा की, हमने DeathSleep के कॉलर द्वारा आरक्षित शैडो स्पेस को भी कॉपी किया, और यह DeathSleep को कॉल करने से पहले RSP का मान है।
एक बार जब हम जागने के बिंदु पर पहुँच जाते हैं, निष्पादन फिर से शुरू करने से ठीक पहले, हमें अपने सहेजे गए स्टैक को जगह में रखना होगा। जैसा कि हम पहले से जानते हैं, हमारा सहेजा गया स्टैक awake फंक्शन द्वारा कैप्चर किए गए पते से शुरू होता है, इसलिए नया कैप्चर किया गया पता वह शुरुआती बिंदु होगा जहाँ हम अपना सहेजा गया स्टैक रखेंगे, लेकिन यह एक समस्या पैदा करता है, हमारे पुराने स्टैक को रखने के बाद किसी भी फंक्शन का कोई भी कॉल इसे संशोधित करेगा और इसे तोड़ देगा, और यहाँ सफाई करना वास्तव में सुविधाजनक है, विशेष रूप से हमारे स्टैक बैकअप को रखने के लिए उपयोग किए जाने वाले हीप को मुक्त करना। इसका मतलब है कि हमें अपने वर्तमान Rsp और स्टैक के उन हिस्सों को भी स्थानांतरित करने की आवश्यकता है जिनका हम वर्तमान में उपयोग कर रहे हैं, उस स्थान के बाहर जहाँ हम अपना पुनर्स्थापित स्टैक रखेंगे। इसे स्पष्ट करने का प्रयास करते हुए यहाँ समस्या है:

और यहाँ मेरा समाधान है, बस सब कुछ दूर ले जाएँ:

सारी मेहनत करने के बाद, अंतिम चीज NtContinue का उपयोग करना है, यह फंक्शन हमें अपने पहले कैप्चर किए गए और संशोधित संदर्भ के साथ वर्तमान संदर्भ को बदलने की अनुमति देता है, RIP को DeathSleep कॉल के ठीक बाद सेट करते हुए, सभी रजिस्टरों के मान वही होने चाहिए जो DeathSleep को कॉल करते समय थे, और RSP को स्टैक के शीर्ष पर इंगित करना चाहिए।
ठीक है, हम वर्तमान थ्रेड को संग्रहीत और पुनर्स्थापित करने के लिए आवश्यक मूल बातें जानते हैं, लेकिन हमें किसी तरह यह सब चलाने में सक्षम होने की आवश्यकता है भले ही हमारे पास कोई थ्रेड न हो। यहाँ हम अपने प्यारे थ्रेड पूल API से मिलते हैं, विंडोज द्वारा दिया गया एक उपकरण, जो हमें थ्रेड्स के एक समूह (एक पूल) में कार्यों (एक तर्क के साथ अधिकतम फंक्शन) को कतारबद्ध करने की अनुमति देगा, जो पूरी तरह से ऑपरेटिंग सिस्टम द्वारा प्रबंधित किया जाएगा। यदि आपने Ekko देखा है, तो आप देख सकते हैं कि यह इस API का उपयोग करता है, तो... चलिए इसे उसी तरह लागू करते हैं।
सब कुछ ठीक काम कर रहा था, लेकिन एक समस्या थी, एक कार्यकर्ता अपने कतारबद्ध कार्यों के निष्पादन को समाप्त करने के बाद भी ऊपर था। यह एक समस्या थी, क्योंकि मैं हमारे प्रोग्राम द्वारा उत्पन्न सभी थ्रेड्स को नष्ट करना चाहता था, इसलिए यह जाने का रास्ता नहीं था।
थोड़ा खोदाई करने के बाद, मैंने पाया कि Ekko में उपयोग किया जाने वाला थ्रेड पूल API, एक पुराना संस्करण था, और कुछ और क्षमताओं के साथ एक नया था, और उनमें से, एक फंक्शन जो प्रभावी रूप से हमारी समस्या को हल करेगा: CloseThreadPool()। यह नया API हमें अपना स्वयं का पूल बनाने, और उनका उपयोग करने के बाद उन्हें नष्ट करने, सभी उपयोग किए गए कार्यकर्ताओं को समाप्त करने की अनुमति देता है। यह दो और फायदे देता है: थ्रेड्स की अधिकतम संख्या निर्धारित करना और सफाई समूहों को साफ करना। थ्रेड्स की अधिकतम संख्या निर्धारित करने से हमें अपने सभी कार्यों को अनुक्रमिक रूप से निष्पादित करने की अनुमति मिलेगी, जब तक कि वे किसी भी समय अंतर के साथ कतारबद्ध हों। सफाई समूह सब कुछ हो जाने के बाद सफाई को आसान बनाने के लिए उपयोगी होते हैं।
तो... सब कुछ हो गया? खैर, इस बिंदु पर थ्रेड समाप्त हो गया है, और हम रीबर्थ फंक्शन को कतारबद्ध कर रहे हैं जो awake को प्रवेश बिंदु के रूप में नया थ्रेड बनाता है, पिछली स्थिति को पुनर्स्थापित करता है और पूल को बंद करता है, अब तक सब ठीक है!
जब मैंने उन सभी चीजों के साथ समाप्त किया जिनकी हमने पहले चर्चा की थी, मैंने सोचा कि कठिन हिस्सा हल हो गया था, क्योंकि यह भाग पिछली तकनीकों द्वारा पहले ही हल कर लिया गया था, लेकिन ओह बॉय, मुझे नहीं पता था कि क्या आने वाला था।
मुख्य समस्या यह है कि हमें इसे अपने कोड के बाहर उतारने की आवश्यकता है, क्योंकि हम मेमोरी सुरक्षा को RW (रीड-राइट) में बदल रहे हैं, यदि हम VirtualProtect() को कॉल करते हैं, जब फंक्शन वापस आता है, तो हमारी प्रक्रिया क्रैश हो जाएगी (हम RW पृष्ठों में निर्देश निष्पादित नहीं कर सकते), इसलिए हमें इसे कहीं और से निष्पादित करने का एक तरीका खोजने की आवश्यकता है, और इसे कुछ RX (रीड-एक्ज़ीक्यूट) पृष्ठों पर भी वापस करना होगा (और वापस आने पर भी ऐसा ही होता है)। जाहिर है, हम इसके लिए भी थ्रेड पूल API का उपयोग करेंगे, लेकिन एक समस्या है, हम अपने कार्यों को केवल एक तर्क दे सकते हैं, और VirtualProtect() 4 लेता है।
इसके लिए हम फिर से NtContinue() का उपयोग करेंगे, पहली बार मैंने इस फंक्शन के लिए इस उपयोग को Foliage पर देखा था, लेकिन इसका उपयोग Ekko में भी किया जाता है। NtContinue(), जैसा कि हमने पहले देखा, हमें उस थ्रेड के लिए कुछ संदर्भ सेट करने की अनुमति देता है जो इसे कॉल करता है, और कुछ चतुर समायोजन के साथ, यह केवल एक का उपयोग करके (थ्रेड पूल API के लिए बहुत सुविधाजनक) एकाधिक तर्कों के साथ एक फंक्शन को "कॉल" कर सकता है। मुख्य विचार RIP को फंक्शन के प्रारंभ पते पर सेट करना है, और, चूँकि windows x64 कॉलिंग कन्वेंशन पहले चार तर्कों को रजिस्टरों में पास करता है (rcx, rdx, r8, r9, उस क्रम में), बस अपने तर्कों को उस संदर्भ संरचना पर रखें जिसे आप NtContinue पर पास करेंगे, और यह प्रभावी रूप से एक फंक्शन कॉल का अनुकरण करेगा। NtContinue का उपयोग करते समय हमें अंतिम चीज का ध्यान रखना है Rsp, क्योंकि, जैसा कि हमने पहले देखा, इस पते में रिटर्न एड्रेस होना चाहिए जब एक फंक्शन को कॉल किया जाता है।
तो NtContinue को काम करने के लिए हमें पहली चीज जो चाहिए वह है एक संदर्भ, हम इसे मैन्युअल रूप से तैयार कर सकते हैं, लेकिन हमें एक समस्या का सामना करना पड़ेगा, Rsp के लिए मान ढूँढना, जो हमारे फंक्शन को पास होने पर, उस पते की ओर इशारा करेगा जिसका उपयोग RET द्वारा वापस लौटने के लिए किया जाएगा। हमारे कार्य एक अलग थ्रेड में काम करेंगे, इसलिए हम नहीं जानते कि इसका स्टैक कहाँ रखा जाएगा। समाधान (सावधानीपूर्वक Ekko से चुराया गया, बहुत-बहुत धन्यवाद :P) एक कार्यकर्ता के अंदर RtlCaptureContext() के साथ संदर्भ की एक प्रति लेना है, और प्राप्त संदर्भ के स्टैक पॉइंटर को 8 से बढ़ाना है, ताकि यह CALL RtlCaptureContext() द्वारा स्टैक में डाले गए पते की ओर इशारा करे, और जो इस अंतिम फंक्शन का रिटर्न एड्रेस है, और हम इसका उपयोग अपने सभी फंक्शनों के रिटर्न एड्रेस के रूप में कर सकते हैं।
ठीक है यह अच्छा है, लेकिन क्या होता है जब हम Rsp में यह संशोधन नहीं कर सकते? ऐसा तब होता है जब हम अस्पष्टीकरण करते हैं, हम एक नए थ्रेड में होंगे, इसलिए पुराने संदर्भ का Rsp बेकार है। हमें नए थ्रेड से लिया गया एक नया संदर्भ चाहिए, लेकिन हम सही पते पर इंगित करने के लिए Rsp को संशोधित करने के पुराने ट्रिक का उपयोग नहीं कर सकते।
इसलिए हम प्राप्त संदर्भ को संशोधित नहीं कर सकते, लेकिन इसका मतलब यह नहीं है कि यह बेकार है, वास्तव में हम इसका उपयोग करेंगे, लेकिन एक अलग तरीके से। यदि हम उस संदर्भ को NtContinue() के साथ बिना इसके Rip को संशोधित किए पुनर्स्थापित करते हैं, तो यह निष्पादन को RtlCaptureContext() कॉल के बाद अगले निर्देश पर पुनर्निर्देशित करेगा, और एक सही Rsp के साथ, इसलिए हम संशोधित संदर्भों के साथ अपने NtContinue() कॉल के बाद इसका उपयोग कर सकते हैं, ताकि अपने कार्यों के निष्पादन को सही ढंग से समाप्त करने में सक्षम हो सकें। ऐसा करने के लिए हम एक Rop चेन का उपयोग करेंगे, अपने पहले संदर्भ के Rsp को मैन्युअल रूप से तैयार किए गए स्टैक पर इंगित करके, जो वह सब कुछ रखेगा जिसकी हमें निष्पादन को तब तक पुनर्निर्देशित करने की आवश्यकता है जब तक कि दूसरा NtContinue() कॉल समाप्त करने के लिए सही संदर्भ सेट नहीं कर देता।
हमारा तैयार स्टैक इस तरह दिखना चाहिए:

हम 2 rop गैजेट का उपयोग कर रहे हैं, एक हमारे फंक्शन के शैडो स्पेस को ठीक करने या "कूदने" के लिए, और दूसरा NtContinue के लिए तर्क को rcx में रखने और फिर उस पर वापस लौटने के लिए जिम्मेदार है।
इन 2 rop गैजेट को ढूँढना काफी आसान है, शैडो स्पेस को ठीक करने वाला लगभग किसी भी फंक्शन का उपसंहार है (मुझे केवल Ntdll में 500 से अधिक हिट मिले), क्योंकि जैसा कि हमने पहले देखा, उपसंहार मुख्य रूप से Rsp को कम करने के लिए डिज़ाइन किए गए हैं, और दूसरा सिर्फ pop rcx; ret; है, जो 2 बाइट्स है, और Ntdll और Kernel32 dll के बीच कुछ जोड़े भी मिले।
जैसा कि हमने देखा, NtContinue का उपयोग करने के लिए केवल इसका पहला तर्क भरा होना चाहिए, और यह पुराने थ्रेड पूल API के साथ एकदम सही है, लेकिन नए थ्रेड पूल API में, तर्क दूसरे स्थान पर पारित किए जाते हैं, इसलिए हाँ, यह अकेला काम नहीं करेगा।
इस अंतिम समस्या को हल करने का तरीका जाने बिना कुछ घंटों के बाद, मेरे दिमाग में यह आया कि दोनों एपीआई कुछ मामलों में समान कार्यों का उपयोग करते हैं, और इसने मुझे सोचने पर मजबूर कर दिया कि वे दिखने से अधिक समान हो सकते हैं, इसलिए मैंने उनके बीच संबंध की जांच करने का फैसला किया।
पुराने एपीआई के लिए हम अपने कार्यों को कतारबद्ध करने के लिए CreateTimerQueueTimer() का उपयोग कर रहे हैं, और नए में, हमें ऐसा करने के लिए दो कार्यों की आवश्यकता है: CreateThreadpoolTimer(), जो कॉलबैक फंक्शन और इसे पारित करने के लिए तर्क लेगा, और एक TP_TIMER संरचना के लिए एक सूचक लौटाएगा जो कार्य का वर्णन करता है, और कार्य को कतारबद्ध करने के लिए एक दूसरा फंक्शन: SetThreadpoolTimer(), जो पिछला सूचक और एक FILETIME संरचना के लिए एक सूचक लेगा जो वर्णन करता है कि कार्य कब निष्पादित किया जाएगा।
यदि हम इन फंक्शनों को रिवर्स करते हैं, तो हम यह पाएंगे:

तो जैसा कि हम देख सकते हैं CreateThreadpoolTimer() TpAllocTimer() के लिए सिर्फ एक फैंसी रैपर है, और SetThreadpoolTimer() TpSetTimer() के लिए सिर्फ एक फॉरवर्डर है।
अब CreateTimerQueueTimer() के अंदरूनी हिस्से की जाँच करते हैं। पहली नज़र में, यह Ntdll में एक फंक्शन, RtlCreateTimer() के लिए सिर्फ एक और फैंसी रैपर है, और यहीं पर जादू होता है। यह एक बड़ा फंक्शन है लेकिन यहाँ वह सोना है जिसे हम देख रहे थे:

जैसा कि आप देख सकते हैं, इस फंक्शन के अंदर प्रभावी रूप से TpAllocTimer() और TpSetTimer() के लिए एक कॉल है, जो यह कहने के समान है कि यह अपने अंदर CreateThreadpoolTimer() और SetThreadpoolTimer() को कॉल कर रहा है। जैसा कि हम देख सकते हैं, जिस फंक्शन को हम कतारबद्ध कर रहे हैं वह सीधे वह कॉलबैक नहीं है जो हमने फंक्शन को दिया है, यह RtlpTpTimerCallback() को कॉलबैक के रूप में सेट कर रहा है। यदि आपको अभी तक एहसास नहीं हुआ कि इस सबका क्या मतलब है, तो यह है कि हम CreateThreadpoolTimer() का उपयोग एक फंक्शन को कतारबद्ध करने के लिए कर रहे हैं जो अपने तर्क दूसरे स्थान पर प्राप्त करता है, RtlpTpTimerCallback(), जो अपने तर्कों को पहले स्थान पर प्राप्त करने वाले दूसरे फंक्शन को निष्पादित करेगा।
तो एकमात्र चीज जो हमें अभी भी समझने की आवश्यकता है वह यह है कि कॉलबैक जानकारी RtlpTpTimerCallback() को कैसे पारित की जाती है, और कुछ रिवर्सिंग के बाद मैं निम्नलिखित संरचना के साथ समाप्त हुआ, जो आश्चर्य आश्चर्य, काम करता है!

अब हम उन फंक्शनों को कॉल कर सकते हैं जो अपने तर्क पहले स्थान पर प्राप्त करते हैं और साथ ही हम अपने पूल को बंद करने में सक्षम हैं, और कोई थ्रेड नहीं छोड़ते, जीत जीत। यह ध्यान रखना महत्वपूर्ण है कि यह फंक्शन Ntdll में निर्यात नहीं किया गया है, इसलिए मैंने इसे dll के अंदर इसके बाइट रूप से खोजने का फैसला किया।
तो यह अंत है, और सब कुछ समीक्षा करने के बाद, मुझे लगता है कि मैंने मुख्य विचार दिए जो इस POC को विकसित करते समय मेरे दिमाग में आए, और सब कुछ उस तरह से क्यों किया गया जैसा मैंने किया।
यह कोड केवल MSCV कंपाइलर और लिंकर के साथ परीक्षण किया गया था, क्योंकि यह POC इस बात पर अत्यधिक निर्भर है कि इसे कैसे संकलित किया गया था, मैं इसी उपकरण का उपयोग करने की सलाह देता हूँ, और मैं यह सुनिश्चित नहीं करता कि यह अन्य कंपाइलरों के साथ बिना किसी समस्या के काम करेगा।