
バイナリ内の関数を、バイト列の見た目ではなく動作内容でマッチングします。マイクロ実行による挙動ベースの関数フィンガープリンティング。
fnprint は、バイナリ内の関数を、そのバイト列や制御フローグラフの見た目ではなく、関数が何をするかでマッチングします。各関数を小さなエミュレータ上ででっち上げの入力とともに実行し、生じる副作用を記録し、その振る舞いをフィンガープリントへとハッシュ化します。同じように振る舞う2つの関数は、たとえ異なるコンパイラや異なる最適化レベルでビルドされていても、似たフィンガープリントを得ます。
この方法を取る理由: バイトシグネチャ (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 のコーパスから命名したものです。最適化レベルが異なり、シンボルは一切残っていないのに、名前が正しく戻ってきます。自信のあるものには名前を付け、それ以外については黙っています。
もう一つの機能は、2つのビルドを差分比較し、どの関数の振る舞いが変わったかを教えることです。ベンダーが新しいファームウェアを出荷し、実際に何が変わったのか知りたいときに便利です:
$ 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 があります。ある関数の既知の脆弱なバージョンから1つのコーパスを、パッチ済みバージョンからもう1つを構築し、未知のビルドを両方に対してランク付けします。脆弱な側に近く、パッチ済み側から明確に分離している関数こそ、人間の前に出すべきものです。解釈が必要な単一のマッチスコアだけではありません:
$ 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 か、index で構築した .db のどちらでも受け取れるので、コーパスを一度フィンガープリントして再利用できます。
すべてのコマンドはグローバルな --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 は rizin/radare2 内で . fnprint.r2 として実行する afn リネームコマンドを出力します。ターゲットからのシンボル名はどちらに渡る前にもサニタイズされるので、細工された名前が r2 コマンドを注入することはできません。スキーマとセットアップは docs/integrations.md にあります。
インデックス作成はデフォルトでシングルプロセスです。FNPRINT_SHARDS=N fnprint index ... は大きなインデックスを N 個の隔離されたワーカーに分散します。コーパスは 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% |
完全な表と2つ目のライブラリ (lua) は bench/NUMBERS.md にあります。
eval は recall@3 / recall@5 と棄権率も報告します。rank-1 だけでは多くを隠してしまうからです。gcc O0 -> O2 ではトップヒットが正しいのは 93% の確率ですが、正しい関数がトップ5に入るのは 96.6% の確率なので、小さなレビュー予算でギャップのほとんどを埋められます。また、確信が持てないペアについては推測する代わりに棄権 (自信のある「同じ」判定を控える) するので、同じ閾値で recall が低いまま precision が高く保たれます。
正直な評価: 少なくとも片側に何らかの振る舞いの豊かさがある場合 (-O0/-O1 のもの、または -O0 でのクロスコンパイラペア) は 80-98% の範囲に収まります。両側が高度に最適化されていると、観察できる振る舞いが薄くなり、コイントスに近づきます。これは単一パスで学習不要のマッチャーにとっての難しいフロンティアであり、本ツールはそうでないふりをしません。
match に現れません。構造的および初期パスの変更は捉えますが、すべての深い微調整を捉えるわけではありません。fnprint は学習不要で振る舞い優先の選択肢です。効果モデルはすでにアーキテクチャ中立で、これは CPU をまたいだマッチングへの土台です。
explore_depth、デフォルトはオフ) が、到達不可能なパスにビルド固有のノイズを加え、テストではクロスビルド精度を損なったので、その価値を発揮する前にパス整合性フィルタが必要です。--format r2)。次は本物の ghidra プラグインです。それまでの間、contrib/ に実験的な jython インポートスタブがあります。MIT。LICENSE を参照してください。