
Presented at Recon Montreal 2018
या मूर्खों और विद्रोहियों के लिए एडवांस्ड शॉटगन पार्सिंग
एक समय की बात है, प्रिय पाठक, एक अंधेरी और तूफानी रात में, आपका विश्वासयोग्य लेखक एक हैरान करने वाली और रहस्यमय स्थिति पर ठोकर खाई - एक ऐसी प्रणाली जहाँ केवल चिप-ऑफ मेमोरी विश्लेषण और रिवर्सिंग को रोकने वाली चीज़ एक अज्ञात खंडित मालिकाना फ़ाइल सिस्टम थी, साथ ही एम्बेडेड Java के उपयोग के कारण संपीड़न, जो फ़ाइल कार्विंग के मौजूदा उपकरणों की प्रभावशीलता को कम कर रहा था।
उस समय कुछ तदर्थ (ad hoc) समाधान मैन्युअल रूप से उन हिस्सों को जोड़कर बनाया गया होगा जो एक साथ फिट होते दिखते थे, कुछ भयानक python कंसोल काम और बदसूरत ad hoc bash स्क्रिप्टिंग के साथ। यह पर्याप्त अच्छा था, लेकिन समय-गहन।
जबकि चिप-ऑफ विश्लेषण में सादा असम्पीडित डेटा निकालना एक हार्डवेयर हैकर के दिन में काफी सामान्य काम है, संपीड़न महत्वपूर्ण समस्याएँ पैदा करता है जब इसके टुकड़े एक अतार्किक और अप्रिय तरीके से हर जगह बिखरे होते हैं, भले ही निष्कर्षण के खिलाफ कोई वास्तविक अन्य सुरक्षा मौजूद न हो।
Zip फ़ाइलों के बारे में विशेष रूप से कुछ दिलचस्प चीज़ें हैं (जो JAR फ़ाइलों के लिए उपयोग किया जाने वाला मूल प्रारूप है)। एक गंभीर छात्र के रूप में International Journal of PoC||GTFO का काफी समय से और विशेष रूप से फ़ाइल प्रारूप स्टंटिंग में Ange Albertini के काम का अनुसरण करते हुए, मैंने सोचा कि एक Zip फ़ाइल के अंदर Zip फ़ाइल के बारे में पर्याप्त डेटा हो सकता है कि आप इसे फिर से एक साथ जोड़ने का अच्छा काम कर सकें।
तो संदर्भ बिंदुओं को हटाकर, आइए तकनीकी विवरणों में आते हैं।
सबसे पहले, हम फ़ाइल सिस्टम की विशिष्टताओं को नहीं जान सकते हैं (और अपने शोध के दृष्टिकोण से, जिस सिस्टम पर मुझे समस्या मिली थी, उससे स्वतंत्र रूप से, मैंने फैसला किया कि इसके बारे में परवाह न करना ही बेहतर है)। लेकिन हम अधिकांश फ़ाइल सिस्टम लागू करने के तरीके के बारे में एक-दो बातें जानते हैं। विशेष रूप से हम जानते हैं कि वे चंक्स में लिखे जाते हैं। चंक्स का एक न्यूनतम आकार होता है, जिसे पृष्ठ (pages) कहा जाता है, और हम डंप ब्राउज़ करके और लिखे गए न्यूनतम ब्लॉक आकार की पहचान करके उस पृष्ठ आकार की पहचान कर सकते हैं।
इनमें से कुछ सन्निहित (contiguous) रूप से चल सकते हैं, कुछ नहीं, जिसमें ब्लॉकों के सन्निहित होने का कोई स्पष्ट पैटर्न नहीं होता।
इसका सीधा सा मतलब है कि हमारे सामने समस्या यह है कि डेटा पृष्ठों को इस तरह कैसे पुनर्व्यवस्थित किया जाए कि वे हमें उन फ़ाइलों की वैध (या लगभग वैध) छवियाँ प्रदान करें जिन्हें हम निकालना चाहते हैं।
Zip फ़ाइलें इस तरह लिखी जाती हैं कि वे एक प्रकार का उल्टा पदानुक्रम लागू करती हैं। पहले संपीड़ित फ़ाइल डेटा (जो उसका वर्णन करने वाले लोकल फ़ाइल हेडर में लिपटा होता है)। फिर एक सेंट्रल डायरेक्टरी (जो लोकल फ़ाइल हेडर के ऑफसेट की सूची देती है) और फिर एक एंड ऑफ सेंट्रल डायरेक्टरी रिकॉर्ड (जो अन्य चीजों के साथ zip में संग्रहीत फ़ाइलों की संख्या, सेंट्रल डायरेक्टरी की शुरुआत का ऑफसेट और सेंट्रल डायरेक्टरी का आकार बताता है)।
आइए इसे उल्टा करके देखें, थोड़ा और विवरण में जाते हुए:
एंड ऑफ सेंट्रल डायरेक्टरी हमें बताता है:
प्रत्येक सेंट्रल डायरेक्टरी रिकॉर्ड हमें बताता है:
प्रत्येक लोकल फ़ाइल रिकॉर्ड हमें बताता है:
उपरोक्त सब कुछ हमें इस ओर ले जाता है कि अधिकांश फ़ाइल का पुनर्निर्माण हो चुका है।
सबसे पहले हमें विभिन्न फ़र्मवेयर से डेटा को अलग करने के लिए एक कदम की आवश्यकता है। इसका कारण यह है कि सभी ऑफसेट केवल अपनी संबंधित zip फ़ाइलों के भीतर ही प्रासंगिक होते हैं -- किसी भी टकराव से zip स्ट्रीम मेल नहीं खाएँगी और भ्रष्टाचार होगा, और हम जोर देकर कहते हैं कि जितना संभव हो उतना असंदूषित डेटा बाहर निकालें। साथ ही हमें अच्छा आश्वासन चाहिए कि, जैसे लक्षित फ़र्मवेयर में हम जो भी कमज़ोरियाँ खोजते हैं, वे उसी को प्रभावित करें जिसे हम सामान्य रूप से चलते देखते हैं, और वह किसी अन्य फ़ाइल को नहीं जो वहाँ पड़ी रह गई हो।
यहाँ आवश्यक समाधान kmeans एल्गोरिदम है (उर्फ "लॉयड का एल्गोरिदम")। यहाँ एक बहुत अच्छा वीडियो है जो बताता है कि यह कैसे काम करता है। SciPy के पास एक अच्छा संस्करण तैयार था, लेकिन मुझे पहचानना/ पैच एल्गोरिदम को लागू करने वाला एकमात्र क्लस्टरिंग/एनालिटिक्स क्रेट, ताकि यह Rust कार्यान्वयन के लिए काम कर सके। सौभाग्य से मुझे इसे शुरू से नहीं लिखना पड़ा।
उसके बाद काम आसान हो जाता है।
हम ऐसा करने के लिए कई सुविधाओं का उपयोग कर सकते हैं। Flags, Method और Version फ़ील्ड सभी फ़ाइल को संपीड़ित करने के लिए उपयोग किए गए Zip स्टैक के आधार पर भिन्न होते हैं। इसके अलावा, हेडर में टाइमस्टैम्प होते हैं, और सामान्यतः यह संभावना नहीं है कि सभी फ़र्मवेयर एक ही समय में संकलित और संपीड़ित किए गए हों।
वैसे, यह ध्यान देने योग्य है कि Zip फ़ाइलें MS-DOS प्रारूप टाइमस्टैम्प का उपयोग करती हैं, जो बिट-पैक्ड शॉर्ट्स होते हैं जो वर्ष-महीना-दिन और घंटा-मिनट-दो-सेकंड दर्शाते हैं। यदि इन्हें वर्गीकरण डेटा के रूप में उपयोग करने से पहले एक निरपेक्ष स्केलर मान में परिवर्तित नहीं किया गया, तो आप एक वर्ष के अंतर को उतना ही महत्व दे सकते हैं जितना एक सेकंड को, और यह बिल्कुल अच्छा नहीं है!
हम इन्हें एक यूक्लिडियन वेक्टर (जो ℝ में मानों की एक n-आयामी सरणी, या फ्लोट कोऑर्डिनेट्स के लिए एक फैंसी शब्द है, लेकिन ऊपर दिया गया वीडियो शायद सबसे सीधा स्पष्टीकरण है) में परिवर्तित करते हैं और क्लस्टरिंग एल्गोरिदम हमारे लिए बाकी सब कुछ कर देता है, सभी पार्स किए गए हेडर को अपेक्षित संख्या में बकेट्स में इकट्ठा करता है।
हालाँकि मैंने इस विधि के प्रोटोटाइप के लिए इसे काफी अधूरे Python स्क्रिप्ट में लिखा है, PoC के साथ लगभग 70-80% JAR सामग्री पुनर्प्राप्ति दर तक पहुँचने के बाद मैंने वहीं रुकने और Rust में एक तेज़ संस्करण लागू करने का फैसला किया है।
Rust में nom नामक एक क्रेट है जो पार्सर-वेरिफायर लिखने के लिए बिल्कुल शानदार है।
इसे Rust में फिर से लिखने का यह एक मुख्य आकर्षण था, fwiw।
स्पष्ट और अत्यंत कठोर पार्सर लिखने की क्षमता इसे कुछ मायनों में
कहीं अधिक आसान बनाती है, उसकी तुलना में जब यह सब Python में संभालने की कोशिश की जा रही थी (जो
कहीं अधिक सहिष्णु होता है, इतना कि कभी-कभी यह निश्चित होना
एक चुनौती होती है कि आप किसी एज केस को पकड़ने में कोई त्रुटिपूर्ण विफलता
अनदेखा नहीं कर रहे हैं।
यदि शानदार तेज़ और सुपाठ्य पार्सर दिलचस्प लगते हैं, तो देखें
इसके अलावा, इस तरह का विश्लेषण Python में चलाना स्वाभाविक रूप से धीमा है, और वास्तव में इस दृष्टिकोण की व्यवहार्यता का पता लगाने के लिए एक PoC तक पहुँचने का मार्ग होने के अलावा इसका बहुत अधिक उद्देश्य कभी नहीं था।
खैर, इतना ही काफी...
शेष चंक्स के पुनर्निर्माण के तरीकों में पहले उच्च एन्ट्रॉपी वाले उम्मीदवारों के लिए शेष पृष्ठों को फ़िल्टर करना, पहले छोटे अंतरालों को भरना शामिल है (खोज सूची से जितने संभव हो उतने पृष्ठों को हटाना, क्योंकि इन पर क्रमपरिवर्तन का परीक्षण करना सबसे खराब स्थिति में एक घातीय समय वाला कार्य है, इसलिए आसान मामलों को जल्दी हल करना प्राथमिकता है और जैसे-जैसे हम आगे बढ़ते हैं, यह हमारी समस्या को घातीय रूप से सरल बनाता है!)।
हम उन उदाहरणों को खोजकर भी तेज़ सफलता प्राप्त कर सकते हैं जहाँ एक लोकल फ़ाइल हेडर पेज बाउंड्री के कारण पार्स नहीं हो पाता (हमें इसे समान रूप से संरेखित समकक्ष से मिलाने में सक्षम होना चाहिए, कम से कम उन मामलों के लिए जहाँ समान संरेखण अद्वितीय हैं और अन्य भ्रष्टाचार कलाकृतियों से टकराते नहीं हैं)।
हम लापता पृष्ठों के उम्मीदवारों की जाँच कैसे करें? खैर हमारे पास फ़ाइलों के लिए CRC32 चेकसम हमारी सेंट्रल डायरेक्टरी में मौजूद हैं! फ़ाइल पर CRC32 की गणना करने के बजाय, शायद इसे करने का सबसे अच्छा तरीका ज्ञात चंक्स पर CRC32 की गणना करना है (हमारे अंतराल की शुरुआत से पहले पृष्ठ के अंत में डेटा से आगे की ओर, और डेटा (या deflate स्ट्रीम के बाद DataDescriptor चंक) से उल्टी दिशा में) और इनसे यह पता लगाना कि लापता पृष्ठों के प्रत्येक ब्लॉक के लिए हमें किस मध्यवर्ती CRC32 की उम्मीद करनी चाहिए।
अनिवार्य रूप से, जब भी हम हल करने के लिए सबसे सरल/सबसे तेज़ समस्या चुनते हैं, हम अपशिष्ट (chaff) को हटाकर कठिन समस्याओं को काफी सरल बना देते हैं। यही कारण है कि शैनन एन्ट्रॉपी का उपयोग करके खाली या लगभग खाली पृष्ठों को पूरी तरह से बाहर फेंक दिया जाता है -- यह गारंटी नहीं है कि हमारे पास केवल उच्च-एन्ट्रॉपी zip पृष्ठ होंगे लेकिन भले ही आउटलायर हों, शुरुआत में उस जटिलता से निपटने से बचना एक बड़ी गति वृद्धि है।
यदि आप इसमें छेड़छाड़ करना चाहते हैं, तो rust स्थापित करें (शानदार rustup nightly के साथ अनुशंसित)। फिर:
$ git clone [repo]
...
$ cd zipdefrag
...
$ cargo build --release
आप डिबगिंग सक्षम करने के लिए release फ्लैग से बच सकते हैं।
बिल्ड आर्टिफैक्ट /target/{debug,release} में होंगे
cargo doc के साथ दस्तावेज़ बनाएं (यह क्रेट भारी मात्रा में प्रलेखित है। मुझे लिखना पसंद है।)
वर्तमान में Rust संस्करण के लिए डिफ़ॉल्ट रूप से कोई टर्मिनल आउटपुट नहीं है, यदि आप
cli हार्नेस चलाना चाहते हैं तो आपको पर्यावरण चर RUST_LOG=zipdefrag सेट करना होगा
जो अब तक के विश्लेषण को प्रदर्शित करने वाली विस्तृत टर्मिनल लॉगिंग सक्षम करता है।
पहेली-समाधान के लिए एक तेज़ पोर्टेबल नेटिव निष्पादनयोग्य (python हुक के साथ) अज्ञात फ़ाइल सिस्टम से zip डंप।
एक प्रदर्शन डंप
rust कार्यान्वयन के लिए प्रदर्शन वर्तमान में खराब है, क्योंकि संबंधित LFH चंक्स खोजने में बेकार व्यवहार होता है। इसे ठीक करूँगा और अपना सबक सीखूँगा।
यह तकनीक तब अच्छी तरह काम नहीं करती जब JAR में कई फ़ाइलें पृष्ठ आकार से काफी बड़ी होती हैं। चूँकि यह zip फ़ाइलों में निहित संरचना के भारी उपयोग पर निर्भर करती है, डेटा-भारी फ़ाइलें अच्छी तरह काम नहीं करतीं।
सुविधाजनक रूप से, J2ME मिडलेट्स के लिए class फ़ाइलें आम तौर पर काफी छोटी होती हैं, लेकिन अंदर पैक किए गए बड़े बाइनरी शायद पुनर्प्राप्त नहीं हो पाएँगे।
साथ ही, python PoC में कई अंकगणितीय बग हैं।