
बाइनरी में फ़ंक्शन का मिलान उनके कार्य के आधार पर करें, न कि उनके बाइट्स की संरचना के आधार पर। माइक्रोएक्ज़ीक्यूशन के माध्यम से व्यवहार-आधारित फ़ंक्शन फ़िंगरप्रिंटिंग।
fnprint फ़ंक्शन्स को बाइनरीज़ में इस आधार पर मैच करता है कि वे क्या करते हैं, न कि उनके बाइट्स या कंट्रोल-फ़्लो ग्राफ़ कैसे दिखते हैं। यह प्रत्येक फ़ंक्शन को एक छोटे एमुलेटर में बनाए गए इनपुट्स के साथ चलाता है, उसके द्वारा उत्पन्न साइड इफ़ेक्ट्स को रिकॉर्ड करता है, और उस व्यवहार को एक फ़िंगरप्रिंट में हैश करता है। दो फ़ंक्शन्स जो एक जैसा व्यवहार करते हैं, उन्हें समान फ़िंगरप्रिंट मिलते हैं, भले ही वे किसी भिन्न कंपाइलर द्वारा, किसी भिन्न ऑप्टिमाइज़ेशन स्तर पर, या किसी भिन्न CPU के लिए बनाए गए हों।
इसे इस तरह करने का उद्देश्य: बाइट सिग्नेचर्स (FLIRT, FunctionID) कोड के रीकंपाइल होते ही टूट जाते हैं, और CFG मैचर्स (BinDiff, Diaphora) -O0 बनाम -O3 में डगमगा जाते हैं और आर्किटेक्चर के पार हार मान लेते हैं। व्यवहार इन सब में बहुत बेहतर तरीके से टिका रहता है। कोई ट्रेनिंग डेटा नहीं, कोई मॉडल नहीं, एक स्टैटिक बाइनरी।
तीन चीज़ें जो यह करता है जो अन्य नहीं करते:
-O0 बनाम aarch64 -O0: 97.7% फ़ंक्शन्स सही नामित।triage आपको एक निर्णय देता है, डिफ़ नहीं। दो कॉर्पोरा (ज्ञात-भेद्य, पैच किया गया), एक मार्जिन, और उन फ़ंक्शन्स की समीक्षा कतार जो भेद्य की ओर झुके हैं। यही n-day वर्कफ़्लो एक कमांड के रूप में है।x86-64 और aarch64 ELF। इस पर भरोसा करने से पहले सीमाएँ देखें।
इसे एक स्ट्रिप्ड बाइनरी और उन चीज़ों के कॉर्पस पर इंगित करें जिनके नाम आपके पास पहले से हैं:
$ strip --strip-all mystery.so
$ nm mystery.so
nm: mystery.so: no symbols
$ fnprint index libz.so -o corpus.db # a build you have symbols for
$ fnprint query mystery.so --corpus corpus.db --threshold 0.6
named 27 function(s):
addr sim graph name
0x000022f9 100.0% 0% adler32_z (libz.so)
0x00002a8d 100.0% 0% compress2 (libz.so)
0x00004500 100.0% 0% deflateStateCheck (libz.so)
0x0000ad5d 100.0% 0% inflateBackEnd (libz.so)
0x0000b69c 100.0% 0% inflateStateCheck (libz.so)
0x00012f8c 100.0% 0% _tr_tally (libz.so)
...
वह रन एक पूरी तरह से स्ट्रिप्ड -O0 बिल्ड है जिसे -O2 कॉर्पस से नाम दिया गया है। भिन्न ऑप्टिमाइज़ेशन स्तर, शून्य सिंबल्स शेष, 27 नाम वापस आते हैं और सभी 27 सही हैं। यह उस पर नाम देता है जिस पर उसे भरोसा है और बाकी पर चुप रहता है। (ऊपर का gif 0.5 से है, जिसने 9 पाए; 0.6 उन स्टैटिक फ़ंक्शन्स को भी पुनः प्राप्त करता है जिन्हें एक स्ट्रिप्ड .so अपने एक्सपोर्ट्स के पीछे छिपाता है।)
CPU के पार भी वही चीज़। x86-64 बिल्ड से कॉर्पस, लक्ष्य उसी सोर्स का aarch64 बिल्ड है, सिंबल्स गए:
$ fnprint index libz_x86_64.so -o corpus.db
$ fnprint query libz_arm64_stripped.so --corpus corpus.db
named 42 function(s):
addr sim graph name
0x00001af4 100.0% 0% adler32_z (libz_x86_64.so)
0x0000263c 100.0% 0% compress2 (libz_x86_64.so)
0x00002958 100.0% 100% x2nmodp (libz_x86_64.so)
0x00002a98 100.0% 0% crc32_z (libz_x86_64.so)
0x000034c4 100.0% 100% crc32_combine64 (libz_x86_64.so)
...
42 नामित, 42 सही, एक भिन्न CPU के लिए बनाए गए कॉर्पस से।
दूसरी चीज़ जो यह करता है वह दो बिल्ड्स को डिफ़ करना और आपको बताना है कि कौन से फ़ंक्शन्स का व्यवहार बदला, जो तब उपयोगी होता है जब कोई विक्रेता नया फ़र्मवेयर जारी करता है और आप जानना चाहते हैं कि वास्तव में क्या बदला। यह सिंबल नामों के साथ या बिना काम करता है: उनके साथ यह नाम से अलाइन करता है, उनके बिना यह दोनों बिल्ड्स को व्यवहार और कॉल ग्राफ़ पर अलाइन करता है, इसलिए दो पूरी तरह से स्ट्रिप्ड फ़र्मवेयर इमेजेज़ भी डिफ़ होती हैं:
$ fnprint match old.so new.so
aligned 38 function pairs by behavior + call graph (no symbol names needed)
unchanged: 31
changed: 7
low-signal: 57 (too small to judge)
changed behavior (lowest similarity first):
30.5% compress_block
43.0% deflate_rle -> deflate_huff
68.8% deflate_slow
वास्तविक n-day काम के लिए triage है। किसी फ़ंक्शन के ज्ञात भेद्य संस्करण से एक कॉर्पस और पैच किए गए संस्करण से एक कॉर्पस बनाएँ, फिर एक अज्ञात बिल्ड को दोनों के विरुद्ध रैंक करें। भेद्य पक्ष के करीब और पैच किए गए पक्ष से स्पष्ट रूप से अलग एक फ़ंक्शन वही है जो आप किसी मानव के सामने रखना चाहते हैं, न कि केवल एक मैच स्कोर जिसकी आपको व्याख्या करनी पड़े:
$ fnprint index vuln.so -o vuln.db
$ fnprint index patched.so -o patched.db
$ fnprint triage mystery.so --vuln vuln.db --patched patched.db
43 functions triaged: 1 look vulnerable, 0 patched, 42 inconclusive
review queue (vuln-leaning, strongest first):
addr vuln% patched% margin matches
0x000022f9 100.0 27.3 +72.7 adler32_z vs crc32_z
जो 42 फ़ंक्शन्स दोनों संस्करणों में समान हैं वे जानबूझकर अनिर्णायक वापस आते हैं, उन्हें किसी भी पक्ष से नहीं जोड़ा जा सकता और उन्हें फ़्लैग नहीं किया जाना चाहिए। --margin और --min-sim नियंत्रित करते हैं कि प्रतिबद्ध होने से पहले दोनों पक्षों को कितनी कठोरता से अलग होना है।
प्रत्येक फ़ंक्शन के लिए:
strip में बचता है। आंतरिक कॉल्स प्रिंट में अनाम रहते हैं और इसके बजाय कॉल-ग्राफ़ एज बन जाते हैं, बॉडी के एक स्टैटिक स्वीप से।-O2 फ़ंक्शन्स को जो सभी एक ही स्टेट चेक को इनलाइन करते हैं, सभी को उस पर दावा करने से रोकता है।कोई ट्रेनिंग डेटा नहीं, कोई मॉडल नहीं। यही विचार साहित्य में Blanket Execution (Egele et al, USENIX Security 2014) के रूप में दिखाई देता है; fnprint इसका एक व्यावहारिक, मेंटेन किया गया रूप है जिसमें एक CLI है जिसे आप वास्तव में उपयोग कर सकते हैं। इफ़ेक्ट मॉडल निर्माण से ही आर्क-न्यूट्रल है, यही कारण है कि एक ही सोर्स के x86-64 और aarch64 प्रिंट्स बिना कुछ सीखे मेल खाते हैं।