
Compara funciones en binarios por lo que hacen, no por cómo se ven sus bytes. Fingerprinting conductual de funciones mediante microejecución.
fnprint empareja funciones en binarios por lo que hacen, no por cómo se ven sus bytes o sus grafos de flujo de control. ejecuta cada función en un pequeño emulador con entradas inventadas, registra los efectos secundarios que produce y convierte ese comportamiento en una huella hash. dos funciones que se comportan igual obtienen huellas similares, incluso si fueron compiladas por un compilador diferente, con un nivel de optimización distinto o para una CPU diferente.
el objetivo de hacerlo así: las firmas de bytes (FLIRT, FunctionID) se rompen en el
momento en que el código se recompila, y los emparejadores de CFG (BinDiff, Diaphora) se vuelven inestables entre
-O0 y -O3 y se rinden entre arquitecturas. el comportamiento sobrevive a todo eso
mucho mejor. sin datos de entrenamiento, sin modelo, un único binario estático.
tres cosas que hace que los demás no:
-O0 vs aarch64 -O0: 97.7% de funciones nombradas correctamente.triage te da un veredicto, no un diff. dos corpus (vulnerable conocido,
parcheado), un margen y una cola de revisión de las funciones que se inclinan a vulnerable.
ese es el flujo de trabajo n-day como un solo comando.ELF x86-64 y aarch64. ver límites antes de confiar en él.
apúntalo a un binario sin símbolos y a un corpus de cosas para las que ya tienes nombres:
$ 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)
...
esa ejecución es una compilación -O0 totalmente sin símbolos nombrada desde un corpus -O2. nivel de
optimización diferente, cero símbolos restantes, 27 nombres vuelven y los 27 son
correctos. nombra aquello de lo que está seguro y se calla sobre el resto.
(el gif de arriba es de la 0.5, que encontró 9; la 0.6 también recupera las funciones
estáticas que un .so sin símbolos esconde detrás de sus exportaciones.)
lo mismo entre CPUs. corpus de la compilación x86-64, el objetivo es la compilación aarch64 de la misma fuente, sin 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 nombradas, 42 correctas, desde un corpus construido para una CPU diferente.
la otra cosa que hace es comparar dos compilaciones y decirte qué funciones cambiaron de comportamiento, lo cual es útil cuando un proveedor publica un nuevo firmware y quieres saber qué se movió realmente. funciona con o sin nombres de símbolos: con ellos alinea por nombre, sin ellos alinea las dos compilaciones por comportamiento más el grafo de llamadas, así que dos imágenes de firmware totalmente sin símbolos también se comparan:
$ 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 trabajo n-day real está triage. construye un corpus a partir de la versión
vulnerable conocida de una función y otro a partir de la versión parcheada, luego clasifica una
compilación desconocida contra ambos. una función cercana al lado vulnerable y claramente
separada del lado parcheado es lo que quieres poner delante de un humano, no solo una
puntuación de coincidencia única que tengas que 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
las 42 funciones que son idénticas en ambas versiones vuelven como inconclusas a
propósito, no se pueden fijar a ninguno de los dos lados y no deberían marcarse. --margin
y --min-sim controlan cuánto tienen que separarse los dos lados antes de comprometerse.
para cada función:
strip. las llamadas internas son anónimas en la
impresión y se convierten en aristas del grafo de llamadas, a partir de un barrido estático del cuerpo.-O2 que todas insertan
la misma comprobación de estado la reclamen todas.