
Сопоставляйте функции в бинарных файлах по тому, что они делают, а не по тому, как выглядят их байты. Поведенческое фингерпринтирование функций через микроисполнение.
fnprint сопоставляет функции в бинарных файлах по тому, что они делают, а не по тому, как выглядят их байты или графы потока управления. он запускает каждую функцию в миниатюрном эмуляторе с выдуманными входными данными, записывает производимые побочные эффекты и хэширует это поведение в отпечаток. две функции, ведущие себя одинаково, получают похожие отпечатки, даже если они собраны другим компилятором или с другим уровнем оптимизации.
смысл такого подхода: байтовые сигнатуры (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. другой уровень оптимизации, ноль оставшихся символов — а имена возвращаются правильные. он называет то, в чём уверен, и молчит об остальном.
вторая его возможность — сравнить две сборки и сказать, какие функции изменили поведение; это удобно, когда вендор выпускает новую прошивку, а вы хотите знать, что на самом деле изменилось:
$ 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. постройте один корпус из известной уязвимой версии функции и один — из пропатченной, затем ранжируйте неизвестную сборку относительно обоих. функция, близкая к уязвимой стороне и чётко отделённая от пропатченной, — это то, что нужно показать человеку, а не просто одна оценка совпадения, которую приходится интерпретировать:
$ 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 функции, идентичные в обеих версиях, намеренно возвращаются как неопределённые: их нельзя привязать ни к одной из сторон, и помечать их не следует. параметры --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, либо .db, который вы собрали с помощью index, так что вы можете один раз отпечатать корпус и переиспользовать его.
точность rank-1: для функции из сборки A ранжируем все функции из сборки B по сходству — попадает ли верхний результат в правильную функцию. это ровно та задача, что и именование бинарника без символов. измерено на zlib 1.3.1 (84 функции), воспроизводимо с помощью bench/run.sh:
полная таблица, а также вторая библиотека (lua), — в bench/NUMBERS.md.
eval также сообщает recall@3 / recall@5 и долю воздержаний, поскольку один лишь rank-1 многое скрывает. на gcc O0 -> O2 верхний результат верен в 93% случаев, но правильная функция входит в топ-5 в 96.6% случаев, так что небольшой бюджет ручной проверки закрывает бо́льшую часть разрыва. он также воздерживается (отказывается от уверенного ответа «одинаковые») на парах, в которых не уверен, вместо того чтобы гадать; именно поэтому точность остаётся высокой, тогда как полнота при том же пороге низкая.
честная оценка: когда хотя бы одна сторона обладает достаточной поведенческой насыщенностью (всё, что собрано с -O0/-O1, или кросс-компиляторная пара на -O0), результат попадает в диапазон 80–98%. когда обе стороны сильно оптимизированы, наблюдаемое поведение становится слишком бедным, и точность падает до подбрасывания монетки. это тяжёлый рубеж для однопроходного матчера без обучения, и инструмент не делает вид, что это не так.
match. он ловит структурные изменения и изменения на ранних путях, а не каждую глубокую правку.fnprint — это вариант без обучения и с приоритетом поведения. модель эффектов уже архитектурно-нейтральна, что закладывает основу для сопоставления между разными CPU.
explore_depth, выключено по умолчанию), но оно добавляет специфичный для сборки шум на невозможных путях и в тестах вредит точности сравнения между сборками, так что ему нужен фильтр согласованности путей, прежде чем он себя оправдает.MIT. см. LICENSE.
| пара | rank-1 | точность |
|---|
| 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% |