
Corresponder funções em binários pelo que elas fazem, não pela aparência de seus bytes. Impressão digital comportamental de funções via microexecução.
fnprint corresponde funções em binários pelo que elas fazem, não pela aparência dos seus bytes ou grafos de fluxo de controle. ele executa cada função em um pequeno emulador com entradas inventadas, registra os efeitos colaterais que ela produz e transforma esse comportamento em um hash de fingerprint. duas funções que se comportam da mesma forma recebem fingerprints semelhantes, mesmo que tenham sido compiladas por um compilador diferente, em um nível de otimização diferente ou para uma CPU diferente.
o motivo de fazer assim: assinaturas de bytes (FLIRT, FunctionID) quebram no momento em que o código é recompilado, e matchers de CFG (BinDiff, Diaphora) ficam instáveis entre -O0 e -O3 e desistem entre arquiteturas. o comportamento sobrevive a tudo isso muito melhor. sem dados de treinamento, sem modelo, um único binário estático.
três coisas que ele faz que os outros não fazem:
-O0 vs aarch64 -O0: 97,7% das funções nomeadas corretamente.triage dá um veredito, não um diff. dois corpora (vulnerável conhecido, corrigido), uma margem e uma fila de revisão das funções que pendem para o lado vulnerável. esse é o fluxo de trabalho de n-day como um único comando.ELF x86-64 e aarch64. veja limites antes de confiar nele.
aponte-o para um binário sem símbolos e um corpus de coisas para as quais você já tem nomes:
$ 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)
...
essa execução é um build -O0 totalmente sem símbolos nomeado a partir de um corpus -O2. nível de otimização diferente, zero símbolos restantes, 27 nomes retornam e todos os 27 estão certos. ele nomeia aquilo sobre o que tem confiança e fica quieto sobre o resto. (o gif acima é da versão 0.5, que encontrou 9; a 0.6 também recupera as funções estáticas que um .so sem símbolos esconde atrás das suas exportações.)
a mesma coisa entre CPUs. corpus do build x86-64, o alvo é o build aarch64 do mesmo código-fonte, sem símbolos:
$ 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 nomeadas, 42 certas, a partir de um corpus construído para uma CPU diferente.
a outra coisa que ele faz é comparar dois builds e dizer quais funções mudaram de comportamento, o que é útil quando um fornecedor lança um novo firmware e você quer saber o que realmente mudou. funciona com ou sem nomes de símbolos: com eles, alinha por nome; sem eles, alinha os dois builds por comportamento mais o grafo de chamadas, então duas imagens de firmware totalmente sem símbolos ainda podem ser comparadas:
$ 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
para trabalho real de n-day existe o triage. construa um corpus a partir da versão conhecida como vulnerável de uma função e outro a partir da versão corrigida, depois classifique um build desconhecido contra ambos. uma função próxima do lado vulnerável e claramente separada do lado corrigido é o que você quer colocar na frente de um humano, não apenas uma única pontuação de correspondência que você precisa interpretar:
$ 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
as 42 funções idênticas em ambas as versões retornam como inconclusivas de propósito, elas não podem ser atribuídas a nenhum dos lados e não deveriam ser sinalizadas. --margin e --min-sim controlam o quão forte os dois lados precisam se separar antes de ele se comprometer.
para cada função:
strip. chamadas internas são anônimas na impressão e se tornam arestas do grafo de chamadas, a partir de uma varredura estática do corpo.-O2 que todas fazem inline da mesma verificação de estado reivindiquem todas a mesma coisa.