
बाइनरी में फ़ंक्शन का मिलान उनके कार्य के आधार पर करें, न कि उनके बाइट्स की संरचना के आधार पर। माइक्रोएक्ज़ीक्यूशन के माध्यम से व्यवहार-आधारित फ़ंक्शन फ़िंगरप्रिंटिंग।
fnprint बाइनरीज़ में फ़ंक्शन्स को उनके बाइट्स या कंट्रोल-फ़्लो ग्राफ़ कैसे दिखते हैं, इससे नहीं, बल्कि वे क्या करते हैं इससे मैच करता है। यह प्रत्येक फ़ंक्शन को एक छोटे एमुलेटर में बनाए गए इनपुट्स के साथ चलाता है, उसके द्वारा उत्पन्न साइड इफ़ेक्ट्स को रिकॉर्ड करता है, और उस व्यवहार को एक फ़िंगरप्रिंट में हैश करता है। दो फ़ंक्शन्स जो एक जैसा व्यवहार करते हैं, उन्हें समान फ़िंगरप्रिंट मिलते हैं, भले ही वे किसी भिन्न कंपाइलर द्वारा या किसी भिन्न ऑप्टिमाइज़ेशन लेवल पर बनाए गए हों।
इसे इस तरह करने का उद्देश्य: बाइट सिग्नेचर्स (FLIRT, FunctionID) उसी क्षण टूट जाते हैं जब कोड रीकंपाइल होता है, और CFG मैचर्स (BinDiff, Diaphora) -O0 बनाम -O3 में अस्थिर हो जाते हैं। व्यवहार दोनों को बहुत बेहतर तरीके से झेल लेता है।
अभी के लिए केवल x86-64 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
named 9 function(s):
0x000022f9 100.0% adler32_z
0x00002a8d 100.0% compress2
0x0000ad5d 100.0% inflateBackEnd
0x0000adc4 86.7% inflate_fast
0x00002e67 80.5% crc32_z
...
वह रन एक पूरी तरह से स्ट्रिप्ड -O0 बिल्ड है जिसे -O2 कॉर्पस से नाम दिया गया है। भिन्न ऑप्टिमाइज़ेशन लेवल, शून्य सिंबल्स बचे, और नाम सही तरीके से वापस आ जाते हैं। यह उन चीज़ों को नाम देता है जिनके बारे में उसे भरोसा है और बाकी के बारे में चुप रहता है।
दूसरी चीज़ जो यह करता है वह है दो बिल्ड्स को डिफ़ करना और आपको बताना कि कौन से फ़ंक्शन्स का व्यवहार बदला, जो तब उपयोगी होता है जब कोई विक्रेता नया फ़र्मवेयर भेजता है और आप जानना चाहते हैं कि वास्तव में क्या बदला:
$ fnprint match old.so new.so
compared 84 functions present in both
unchanged: 53
changed: 1
low-signal: 31 (too small to judge)
changed behavior (lowest similarity first):
61.7% deflate_stored
वास्तविक 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 फ़ंक्शन्स दोनों संस्करणों में समान हैं, वे जानबूझकर inconclusive लौटाते हैं, उन्हें किसी भी पक्ष से नहीं जोड़ा जा सकता और उन्हें फ़्लैग नहीं किया जाना चाहिए। --margin और --min-sim नियंत्रित करते हैं कि कमिट करने से पहले दोनों पक्षों को कितना अलग होना पड़ेगा।
प्रत्येक फ़ंक्शन के लिए:
कोई ट्रेनिंग डेटा नहीं, कोई मॉडल नहीं। यही विचार साहित्य में Blanket Execution (Egele et al, USENIX Security 2014) के रूप में मिलता है; fnprint इसका एक व्यावहारिक, मेंटेन किया गया रूप है जिसमें एक CLI है जिसे आप वास्तव में उपयोग कर सकते हैं।
एक rust टूलचेन और unicorn + capstone लाइब्रेरीज़ चाहिए।
# debian/ubuntu/kali
sudo apt install libunicorn-dev libcapstone-dev
cargo install --path cli
# or just
cargo build --release # binary at target/release/fnprint
fnprint index <binary> [-o out.db] fingerprint every function, optionally to a db
fnprint match <a> <b> diff two binaries (or .db files) by behavior
fnprint query <target> --corpus <db> name unknown functions from a corpus
fnprint triage <t> --vuln <db> --patched <db> rank a build against vuln vs patched corpora
fnprint eval <a> <b> accuracy metrics using symbol names as truth
fnprint dump <binary> <func> print the recorded effect trace (debugging)
match और query या तो एक ELF लेते हैं या एक .db जिसे आपने index से बनाया है, ताकि आप एक कॉर्पस को एक बार फ़िंगरप्रिंट कर सकें और उसे दोबारा उपयोग कर सकें।
प्रत्येक कमांड एक ग्लोबल --format लेता है:
fnprint query mystery.so --corpus corpus.db --format json > names.json
fnprint query mystery.so --corpus corpus.db --format r2 > fnprint.r2
json स्क्रिप्ट्स के लिए एक स्थिर स्कीमा है (और contrib/ में Ghidra इम्पोर्ट स्टब), r2 afn रीनेम कमांड्स उत्सर्जित करता है जिन्हें आप rizin/radare2 के अंदर . fnprint.r2 के साथ चलाते हैं। टारगेट से सिंबल नामों को दोनों में से किसी में भी पहुँचने से पहले सैनिटाइज़ किया जाता है, ताकि एक तैयार किया गया नाम r2 कमांड्स इंजेक्ट न कर सके। स्कीमाज़ और सेटअप docs/integrations.md में हैं।
इंडेक्सिंग डिफ़ॉल्ट रूप से सिंगल-प्रोसेस है। FNPRINT_SHARDS=N fnprint index ... एक बड़े इंडेक्स को N jailed वर्कर्स में फैला देता है; कॉर्पस बाइट-समान रहता है चाहे N कुछ भी हो। यह केवल बड़ी, फ़ंक्शन-समृद्ध बाइनरीज़ पर मदद करता है, इसलिए जब तक आप इसे न माँगें यह बंद रहता है।
rank-1 सटीकता यह है: बिल्ड A के किसी फ़ंक्शन के लिए, बिल्ड B के प्रत्येक फ़ंक्शन को समानता से रैंक करें, क्या शीर्ष हिट सही है। यह ठीक वही स्ट्रिप्ड-नेमिंग कार्य है। zlib 1.3.1 (84 फ़ंक्शन्स) पर मापा गया, bench/run.sh के साथ पुनरुत्पादनीय:
| pair | rank-1 | precision |
|---|---|---|
| gcc O0 -> O1 | 97.1% | 93.8% |
| gcc O0 -> O2 | 93.1% | 83.3% |
| gcc O0 -> O3 | 91.3% | 100.0% |
| gcc/clang O0 | 97.7% | 97.1% |
| gcc O2 -> O3 | 56.5% | 66.7% |
| gcc/clang O2 | 59.1% | 50.0% |
पूरी तालिका और एक दूसरी लाइब्रेरी (lua) bench/NUMBERS.md में।
eval recall@3 / recall@5 और एक abstention दर भी रिपोर्ट करता है, क्योंकि अकेले rank-1 बहुत कुछ छिपा देता है। gcc O0 -> O2 पर शीर्ष हिट 93% समय सही होता है लेकिन सही फ़ंक्शन 96.6% समय शीर्ष 5 में होता है, इसलिए एक छोटा समीक्षा बजट अधिकांश अंतर को भर देता है। यह उन जोड़ों पर जिनके बारे में इसे भरोसा नहीं है, अनुमान लगाने के बजाय abstain भी करता है (एक आत्मविश्वासी "same" कॉल से इनकार करता है), यही कारण है कि precision ऊँची बनी रहती है जबकि उसी थ्रेशोल्ड पर recall कम होता है।
ईमानदार पाठ: जब कम से कम एक पक्ष में कुछ व्यवहारिक समृद्धि होती है (कुछ भी -O0/-O1 के साथ, या -O0 पर एक क्रॉस-कंपाइलर जोड़ा) तो यह 80-98% रेंज में आता है। जब दोनों पक्ष भारी रूप से ऑप्टिमाइज़्ड होते हैं तो जो व्यवहार हम देख सकते हैं वह पतला हो जाता है और यह सिक्का उछालने की ओर गिर जाता है। यह एक सिंगल पास, ट्रेनिंग-रहित मैचर के लिए कठिन सीमा है और यह इसके विपरीत होने का दिखावा नहीं करता।
match में नहीं दिखेगा। यह संरचनात्मक और शुरुआती-पथ परिवर्तनों को पकड़ता है, हर गहरे बदलाव को नहीं।fnprint ट्रेनिंग-रहित, व्यवहार-प्रथम विकल्प है। इफ़ेक्ट मॉडल पहले से ही आर्किटेक्चर-न्यूट्रल है, जो CPU के आर-पार मैचिंग के लिए आधार है।
explore_depth, डिफ़ॉल्ट रूप से बंद) लेकिन यह असंभव पथों पर बिल्ड-विशिष्ट शोर जोड़ता है और परीक्षण में क्रॉस-बिल्ड सटीकता को नुकसान पहुँचाता है, इसलिए इसके अपना स्थान अर्जित करने से पहले एक पथ-संगति फ़िल्टर चाहिए।--format r2); एक असली ghidra प्लगइन अगला है। इस बीच contrib/ में एक प्रायोगिक jython इम्पोर्ट स्टब है।MIT। LICENSE देखें।