
바이너리의 함수를 바이트 형태가 아닌 동작 방식으로 매칭합니다. 마이크로실행을 통한 동작 기반 함수 핑거프린팅.
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개의 함수는 의도적으로 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% |
전체 표와 두 번째 라이브러리(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, 기본 꺼짐) 불가능한 경로에 빌드별 노이즈를 추가하고 테스트에서 크로스 빌드 정확도를 해쳤으므로, 제 역할을 하려면 경로 일관성 필터가 필요합니다.--format r2); 실제 ghidra 플러그인이 다음입니다. 그동안 contrib/에 실험적인 jython 임포트 스텁이 있습니다.MIT. LICENSE를 참조하세요.