
Сопоставляйте функции в бинарных файлах по тому, что они делают, а не по тому, как выглядят их байты. Поведенческое фингерпринтирование функций через микроисполнение.
fnprint сопоставляет функции в бинарниках по тому, что они делают, а не по тому, как выглядят их байты или графы потока управления. Он запускает каждую функцию в крошечном эмуляторе с выдуманными входными данными, записывает побочные эффекты, которые она производит, и хеширует это поведение в отпечаток. Две функции, ведущие себя одинаково, получают похожие отпечатки, даже если они были собраны другим компилятором, с другим уровнем оптимизации или для другого процессора.
Смысл такого подхода: байтовые сигнатуры (FLIRT, FunctionID) ломаются в момент перекомпиляции кода, а сопоставители CFG (BinDiff, Diaphora) становятся ненадёжными при переходе от -O0 к -O3 и вовсе сдаются при смене архитектуры. Поведение переживает всё это гораздо лучше. Никаких обучающих данных, никакой модели, один статический бинарник.
Три вещи, которые он делает, а другие — нет:
-O0 против aarch64 -O0: 97.7% функций названы правильно.triage даёт вам вердикт, а не diff. Два корпуса (известно уязвимый, пропатченный), запас и очередь на проверку функций, склоняющихся к уязвимым. Это рабочий процесс n-day одной командой.x86-64 и aarch64 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 --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)
...
Этот запуск — полностью лишённая символов сборка -O0, названная по корпусу -O2. Другой уровень оптимизации, ноль оставшихся символов, возвращаются 27 имён, и все 27 верны. Он называет то, в чём уверен, и молчит об остальном. (gif выше — из 0.5, где нашлось 9; 0.6 также восстанавливает статические функции, которые лишённый символов .so прячет за своими экспортами.)
То же самое между процессорами. Корпус из сборки x86-64, цель — сборка aarch64 того же исходника, символы удалены:
$ 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 названы, 42 верны, по корпусу, собранному для другого процессора.
Ещё одна вещь, которую он делает, — сравнивает две сборки и говорит, какие функции изменили поведение, что удобно, когда вендор выпускает новую прошивку и вы хотите знать, что реально сдвинулось. Это работает с именами символов и без них: с ними он выравнивает по имени, без них — выравнивает две сборки по поведению плюс граф вызовов, так что два полностью лишённых символов образа прошивки всё равно сравниваются:
$ 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
Для настоящей работы с 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 функции, идентичные в обеих версиях, намеренно возвращаются как inconclusive: их нельзя привязать ни к одной из сторон, и их не следует помечать. --margin и --min-sim управляют тем, насколько сильно две стороны должны разойтись, прежде чем он вынесет решение.
Для каждой функции:
strip. Внутренние вызовы анонимны в отпечатке и вместо этого становятся рёбрами графа вызовов из статического обхода тела.-O2, которые все инлайнят одну и ту же проверку состояния, претендовать на неё все сразу.