Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
fnprint — 바이너리의 함수를 바이트 형태가 아닌 동작 방식으로 매칭합니다. 마이크로실행을 통한 동작 기반 함수 핑거프린팅. | Kitploit
도구/GitHubGitHub/1rhino2/fnprint
Vulnerability AnalysisDynamic Code Analysis (DAST)Reverse EngineeringMalware AnalysisBinary AnalysisFirmware Analysis
GitHub1rhino2/fnprint

fnprint

바이너리의 함수를 바이트 형태가 아닌 동작 방식으로 매칭합니다. 마이크로실행을 통한 동작 기반 함수 핑거프린팅.

저장소 보기
5547113일 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
웹사이트
공유

fnprint

fnprint은 바이트나 제어 흐름 그래프(control-flow graph)가 어떻게 생겼는지가 아니라 함수가 무엇을 하는지를 기준으로 바이너리 내의 함수를 매칭합니다. 각 함수를 임의의 입력을 넣은 작은 에뮬레이터에서 실행하고, 발생하는 부작용(side effect)을 기록한 뒤, 그 동작을 지문(fingerprint)으로 해싱합니다. 동일하게 동작하는 두 함수는 서로 다른 컴파일러로 빌드되었거나, 최적화 수준이 다르거나, 다른 CPU용으로 만들어졌더라도 유사한 지문을 갖습니다.

이렇게 하는 이유: 바이트 시그니처(FLIRT, FunctionID)는 코드가 재컴파일되는 순간 깨지고, CFG 매처(BinDiff, Diaphora)는 -O0와 -O3 사이에서 흔들리며 아키텍처를 넘어가면 포기합니다. 동작(behavior)은 이 모든 것을 훨씬 잘 견딥니다. 학습 데이터도, 모델도 필요 없고, 정적 바이너리 하나면 됩니다.

다른 도구들이 하지 못하는 세 가지:

  • 악성코드에 겨눠도 안전합니다. 에뮬레이터는 exec, 소켓, 파일 열기, 실행 가능 메모리를 원천적으로 금지하는 seccomp 감옥 아래 권한이 분리된 워커에서 실행됩니다(unicorn은 no-JIT 인터프리터로 빌드되어 실행 가능 메모리가 전혀 필요 없습니다). 에뮬레이터를 뚫도록 조작된 바이너리가 얻는 것은 계산만 할 수 있는 프로세스뿐입니다. SECURITY.md를 참고하세요. BinDiff, Diaphora, BSim, FLIRT, 그리고 ML 매처들은 모두 적대적인 바이트를 프로세스 내에서 파싱합니다.
  • 아키텍처를 넘어 매칭합니다. 심볼이 있는 x86-64 빌드를 인덱싱하고, 스트립된 aarch64 펌웨어에 이름을 붙이세요. 효과 모델(effect model)은 ISA를 전혀 보지 않습니다. zlib x86-64 -O0 대 aarch64 -O0: 함수의 97.7%에 올바른 이름을 붙였습니다.
  • triage는 diff가 아니라 판정을 내려줍니다. 두 코퍼스(알려진 취약 버전, 패치 버전), 마진, 그리고 취약 쪽으로 기우는 함수들의 검토 큐를 제공합니다. 이것이 n-day 워크플로우를 하나의 명령으로 만든 것입니다.

x86-64와 aarch64 ELF를 지원합니다. 신뢰하기 전에 한계를 확인하세요.

show me

fnprint naming functions in a stripped, differently-compiled binary

스트립된 바이너리와 이미 이름을 알고 있는 것들의 코퍼스를 지정하세요:

$ 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는 9개를 찾은 0.5 버전에서 나온 것입니다; 0.6은 스트립된 .so가 export 뒤에 숨긴 정적 함수들도 복구합니다.)

CPU를 넘어서도 마찬가지입니다. 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)
  ...

다른 CPU용으로 만든 코퍼스로부터 42개에 이름을 붙였고 42개 모두 정확합니다.

또 다른 기능은 두 빌드를 diff하여 어떤 함수의 동작이 바뀌었는지 알려주는 것으로, 벤더가 새 펌웨어를 출시했을 때 실제로 무엇이 바뀌었는지 알고 싶을 때 유용합니다. 심볼 이름이 있든 없든 동작합니다: 있으면 이름으로 정렬하고, 없으면 동작과 호출 그래프를 기준으로 두 빌드를 정렬하므로 완전히 스트립된 두 펌웨어 이미지도 diff할 수 있습니다:

$ 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은 두 쪽이 얼마나 강하게 분리되어야 판정을 내릴지 제어합니다.

how it works

각 함수에 대해:

  • 바이너리를 매핑하고 인자 레지스터에 쓰레기 값을 넣은 채로 함수로 점프합니다.
  • 우리가 설정하지 않은 메모리에서의 읽기는 결정론적 값을 반환하고 해당 페이지는 즉석에서 매핑됩니다. 와일드 포인터가 실행을 절대 크래시시키지 않으며, 같은 입력은 항상 같은 트레이스를 줍니다. 이것이 Godefroid의 마이크로실행(microexecution) 기법입니다.
  • 다른 함수로의 호출은 스텁 처리(기록 후 건너뜀)되어 libc로 뛰어들지 않고 실행이 이 함수에 집중되도록 합니다.
  • 아키텍처 중립적인 효과 스트림을 기록합니다: 어떤 인자 버퍼와 구조체 필드를 읽고 쓰는지, 어떤 값 클래스(입력의 복사본, 작은 상수, 포인터)를 쓰는지, 어떤 호출을 하는지, 어떤 분기를 타는지, 무엇을 반환하는지. 절대 주소는 버려지고 오프셋과 형태만 유지됩니다.
  • 그 스트림은 shingle과 minhash 시그니처로 변환됩니다. 유사도는 일치하는 minhash 슬롯의 비율로, 두 함수의 동작이 얼마나 겹치는지 추정합니다. LSH 밴드 인덱스가 쿼리마다 모든 것을 모든 것과 비교하는 일을 막아줍니다.
  • 호출은 import 테이블(plt 스텁, got 슬롯)에서만 이름이 붙습니다. 그것이 strip 후에도 살아남는 부분이기 때문입니다. 내부 호출은 출력에서 익명이며 대신 본문의 정적 스윕으로부터 호출 그래프 엣지가 됩니다.
  • 네이밍과 diffing은 함수별 최적 히트 조회가 아니라 전역 1:1 정렬입니다: 코퍼스 함수는 한 번만 차지될 수 있고, 최선의 쌍이 먼저이며, 그 다음 할당이 호출 그래프 항(쌍의 짝지어진 호출자와 피호출자가 서로 얼마나 짝지어졌는지)으로 두 번 재점수화됩니다. 이것이 동작이 쌍둥이인 두 함수를 구별하고, 같은 상태 검사를 모두 인라인한 27개의 -O2 함수가 모두 그것을 차지하는 것을 막습니다.

학습 데이터도, 모델도 없습니다. 같은 아이디어가 문헌에서는 Blanket Execution(Egele et al, USENIX Security 2014)으로 등장합니다; fnprint는 실제로 사용할 수 있는 CLI를 갖춘 실용적이고 유지 관리되는 접근입니다. 효과 모델은 구조적으로 아키텍처 중립적이며, 그래서 같은 소스의 x86-64와 aarch64 프린트가 아무것도 학습하지 않고도 정렬됩니다.

install

rust 툴체인, cmake, C 툴체인이 필요합니다. unicorn(no-JIT 포크)과 capstone은 cargo가 소스에서 빌드하므로 apt로 설치할 것은 없습니다.

cargo install --path cli
# or just
cargo build --release   # binary at target/release/fnprint

usage

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)
fnprint completions <shell>             print a shell completion script to stdout

match와 query는 ELF 또는 index로 만든 .db를 받으므로, 코퍼스를 한 번 지문 채취하고 재사용할 수 있습니다. index -o는 추가(append)하므로 하나의 코퍼스가 여러 바이너리를 담을 수 있습니다.

match --align은 심볼이 있는 쌍에 대해 이름 없는 정렬을 강제하여 동작만 보는 관점이 무엇을 말하는지 확인합니다. --graph-weight(전역, 기본값 1.0)는 정렬 시 일관된 호출 그래프 이웃이 쌍을 얼마나 끌어올리는지 설정합니다; 0은 모든 함수를 자체 동작만으로 점수화합니다.

match, query, triage는 --limit N을 받아 사람용 테이블이 출력하는 행 수를 제한합니다(0 = 전부). json과 r2 출력은 절대 제한되지 않으므로 스크립트는 모든 것을 받습니다.

completions는 bash, zsh, fish, powershell, elvish용 스크립트를 작성합니다, 예를 들어 fnprint completions bash > ~/.local/share/bash-completion/completions/fnprint.

어떤 출력이든 읽기를 일찍 멈추는 것으로 파이프하면(| head) 조용히 종료되므로, 테이블을 --limit 없이도 안전하게 훑어볼 수 있습니다.

machine output

모든 명령은 전역 --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 import 스텁)를 위한 안정적인 스키마이고, r2는 rizin/radare2 안에서 . fnprint.r2로 실행하는 afn 이름 변경 명령을 출력합니다. 대상의 심볼 이름은 둘 중 어디로 가든 전에 정제되므로, 조작된 이름이 r2 명령을 주입할 수 없습니다. 스키마와 설정은 docs/integrations.md에 있습니다.

인덱싱은 기본적으로 단일 프로세스입니다. FNPRINT_SHARDS=N fnprint index ...는 큰 인덱스를 N개의 감옥에 갇힌 워커로 분산합니다; 코퍼스는 N이 무엇이든 바이트 단위로 동일합니다. 크고 함수가 많은 바이너리에서만 도움이 되므로, 요청하지 않으면 꺼져 있습니다.

accuracy

쌍마다 두 개의 숫자. rank-1은 프린트 단독입니다: 빌드 A의 함수에 대해, B의 모든 함수를 유사도로 순위 매겨 최상위 히트가 올바른 것인지. aligned는 query와 match가 실제로 하는 것입니다: 호출 그래프를 동반한 1:1 할당, 파트너가 맞는 빈도, 그리고 "same" 임계값에서 확정한 쌍의 정밀도/재현율. zlib 1.3.1(84개 함수)과 lua 5.4.6(신호가 있는 약 600개 함수), bench/run.sh로 재현 가능:

도구 다운로드