
Abbina le funzioni nei binari in base a ciò che fanno, non a come appaiono i loro byte. fingerprinting comportamentale delle funzioni tramite microesecuzione.
fnprint abbina le funzioni nei binari in base a ciò che fanno, non a come appaiono i loro byte o i loro grafi di flusso di controllo. esegue ogni funzione in un piccolo emulatore con input inventati, registra gli effetti collaterali che produce e calcola un hash di quel comportamento in un'impronta. due funzioni che si comportano allo stesso modo ottengono impronte simili, anche se sono state compilate da un compilatore diverso, a un livello di ottimizzazione diverso o per una CPU diversa.
il motivo di farlo in questo modo: le firme sui byte (FLIRT, FunctionID) si rompono nel momento in cui il codice viene ricompilato, e i matcher CFG (BinDiff, Diaphora) vacillano tra -O0 e -O3 e si arrendono tra architetture diverse. il comportamento sopravvive a tutto questo molto meglio. nessun dato di addestramento, nessun modello, un singolo binario statico.
tre cose che fa e che gli altri non fanno:
-O0 vs aarch64 -O0: 97,7% delle funzioni nominate correttamente.triage ti dà un verdetto, non un diff. due corpora (noto-vulnerabile, patchato), un margine e una coda di revisione delle funzioni che pendono verso il vulnerabile. questo è il workflow n-day come un solo comando.ELF x86-64 e aarch64. vedi limiti prima di fidarti.
puntalo contro un binario strippato e un corpus di cose di cui hai già i nomi:
$ 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)
...
quella esecuzione è una build -O0 completamente strippata nominata da un corpus -O2. livello di ottimizzazione diverso, zero simboli rimasti, 27 nomi tornano indietro e tutti e 27 sono corretti. nomina ciò di cui è sicuro e tace sul resto.
(la gif sopra è dalla 0.5, che ne trovava 9; la 0.6 recupera anche le funzioni statiche che un .so strippato nasconde dietro le sue export.)
la stessa cosa tra CPU diverse. corpus dalla build x86-64, il target è la build aarch64 dello stesso sorgente, simboli spariti:
$ 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 nominate, 42 corrette, da un corpus costruito per una CPU diversa.
l'altra cosa che fa è confrontare due build e dirti quali funzioni hanno cambiato comportamento, cosa comoda quando un fornitore rilascia un nuovo firmware e vuoi sapere cosa si è effettivamente mosso. funziona con o senza nomi dei simboli: con essi allinea per nome, senza allinea le due build su comportamento più grafo delle chiamate, quindi due immagini firmware completamente strippate si confrontano comunque:
$ 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
per il vero lavoro n-day c'è triage. costruisci un corpus dalla versione nota-vulnerabile di una funzione e uno dalla versione patchata, poi classifica una build sconosciuta rispetto a entrambi. una funzione vicina al lato vulnerabile e chiaramente separata dal lato patchato è ciò che vuoi mettere davanti a un umano, non solo un singolo punteggio di corrispondenza da interpretare:
$ 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
le 42 funzioni identiche in entrambe le versioni tornano inconcludenti di proposito, non possono essere attribuite a nessuno dei due lati e non dovrebbero essere segnalate. --margin e --min-sim controllano quanto i due lati devono separarsi prima che si pronunci.
per ogni funzione:
strip. le chiamate interne sono anonime nella stampa e diventano invece archi del grafo delle chiamate, da una scansione statica del corpo.-O2 che inlinano tutte lo stesso controllo di stato di rivendicarlo tutte.