fnprint

behavioral function fingerprinting

Name functions in stripped ELF binaries by what they do, not their bytes. x86-64 and aarch64, across compilers, optimization levels and CPUs. Built for n-day triage when the builds don't line up, safe to point at malware.

$ cargo install fnprint

show me

fnprint naming functions in a stripped, differently-compiled binary

Point it at a stripped binary, get names back from a corpus you built. The demo names functions in a clang -O3 build using a gcc -O0 corpus, so the byte-level code is completely different.

what it does

name stripped functions

Build a corpus from binaries you have symbols for, then query an unknown stripped build. fnprint returns the closest-behaving named function for each unknown one.

rank n-day patch diffs

Give it a vuln build and a patched build. It ranks which functions in an unknown target lean vulnerable, so you open the right ones first.

survives recompiles, and CPUs

It compares behavior, so a compiler swap or an optimization-level gap that wrecks a CFG diff (bindiff, diaphora) matters much less here. Index the x86-64 build, name the aarch64 firmware: zlib -O0, 42 named, 42 right.

runs untrusted code safely

Microexecution happens in a seccomp + rlimit jailed worker on a no-JIT emulator, so nothing the target does can escape or execute.

how it works

fnprint microexecutes each function on a no-JIT unicorn fork, records the effects it produces (memory reads and writes, calls, syscalls, return behavior), and turns that effect trace into a MinHash/LSH fingerprint. Naming and diffing are a global 1:1 alignment of the two function sets, rescored with the call graph, so two behavioral twins get told apart by who calls them. The emulated code never runs with executable memory and never leaves a locked-down worker process.

a real n-day: CVE-2018-25032

The zlib memory-corruption bug fixed between 1.2.11 and 1.2.12. Corpora built at -O0, the target at -O2, so a CFG diff burns its time on optimization noise. Actual triage output:

52 functions triaged: 5 look vulnerable, 0 patched, 47 inconclusive

review queue (vuln-leaning, strongest first):
   addr         vuln%  patched%  margin  matches
   0x00002d40  100.0     69.5   +30.5  crc32_big vs adler32_z
   0x0000d7e0  100.0     40.6   +59.4  _tr_tally vs _tr_tally
   0x00004270   79.7     66.4   +13.3  deflate_stored vs deflate_stored
   0x0000ce50   78.9     66.4   +12.5  _tr_stored_block vs _tr_stored_block
   0x0000c860   68.8     30.5   +38.3  compress_block vs compress_block

Four of the five it surfaced (_tr_tally, deflate_stored, _tr_stored_block, compress_block) are exactly the CVE-changed functions, found across the -O0 to -O2 gap. The 47 unchanged functions stayed inconclusive instead of cluttering the queue. Full writeup with reproduce steps.

what it is bad at