A reverse engineer's binary Swiss-army knife in Rust: triage, disassembly, function/CFG recovery, crypto-constant + YARA scanning for PE/ELF/Mach-O. Installs as 'knife'.
knife
Find the bug, not just the binary. A reverse engineer's toolkit in Rust.
Parse, triage, disassemble, and audit PE, ELF, and Mach-O from one small binary. Static only: it reads the bytes on disk and never runs the target.
Most tools tell you a binary imports memcpy. knife reads the call sites and tells you which one takes its length from a subtraction:
$ knife sec 7z.dll
[-] stack cookies (/GS) disabled no _securitycookie in the load config
linear stack overflows reach the saved return address unchecked
[-] CFG disabled GUARD_CF clear
WEAK exposure score 7 ยท 3 of 6 mitigations missing or weakened
$ knife audit 7z.dll ยง AUDIT (145 FINDINGS) [-] copy-underflow memcpy sub_1000a6b4 @ 0x1000a72a copy length computed by subtraction (integer underflow to a huge size?) [-] copy-underflow memset sub_1000b028 @ 0x1000b0a2 copy length computed by subtraction (integer underflow to a huge size?)
Those addresses are real functions in a stripped C++ parser that most disassemblers never even recover: knife seeds function discovery from the PE exception directory, taking 7z.dll from 87 functions to 6472, and reads the argument that reaches each dangerous call to rank the ones that look exploitable. It works the same on a Linux daemon or a macOS dylib.
Why
Pulling apart an unknown binary usually means five tools: one for the headers, one for strings and IOCs, one for imports, one for entropy, one to disassemble. knife is all of them in a single command, across three formats, with a transparent triage verdict on top. Everything is static: it reads the bytes on disk and never runs the file.
But the reason to reach for it is the next step, the one the other tools leave to you: it says what the target is protected by, where its dangerous calls are, which of them look actually wrong, and what can reach them. See for vulnerability research.
Install
# from crates.io (installs the knife command)
cargo install reknife
prebuilt binary, no toolchain needed (cargo-binstall)
cargo binstall reknife
latest from git
cargo install --git https://github.com/bl4ckr0ss3/knife
Or grab a prebuilt archive for Linux, macOS, or Windows from the Releases page and drop knife on your PATH.
Commands
One tool, many jobs, which is the point of a Swiss-army knife:
| command | what you get | |---|---| | knife FILE | full triage: verdict, hashes, sections, mitigations, capabilities, IOCs, artifacts, entry disasm | | knife sec FILE | exploit mitigations, and what each missing one buys an attacker | | knife sinks FILE [--class C] [--all] | dangerous-API call sites, grouped by bug class | | knife audit FILE [--reachable] | sink call sites whose arguments look exploitable | | knife xrefs FILE TARGET | what references a function, import, or address | | knife xrefs FILE --str TEXT | what references the strings matching TEXT | | knife paths FILE TARGET [--from F] | call chains that reach a sink from entry points and exports | | knife tui FILE | interactive: functions, listing, xrefs, naming and notes | | knife mcp | Model Context Protocol server over stdio (tools for agents) | | knife name FILE ADDR NAME | name an address; every later command uses it | | knife note FILE ADDR TEXT | annotate an address; shows up in the disassembly | | knife db FILE | everything you have stored for this binary | | knife funcs FILE [--by-refs] | recover functions via control-flow analysis | | knife dis FILE --func NAME | disassemble a whole function with labels and xrefs | | knife dis FILE [--vaddr X \| --off Y] [--count N] | linear disassembly (x86/x64, AArch64) | | knife pseudo FILE --func NAME | pseudocode view: lifted statements, calls with arguments | | knife sections FILE | sections/segments with per-section entropy bars | | knife imports FILE | imported libs and functions, suspicious APIs flagged | | knife exports FILE | exported symbols | | knife caps FILE | capabilities inferred from the symbol surface | | knife strings FILE --min N | ASCII and UTF-16 strings | | knife iocs FILE | URLs, IPs, domains, emails, wallets, reg keys, paths, defanged | | knife hashes FILE | MD5 / SHA-1 / SHA-256 and imphash | | knife scan FILE | crypto constants, packer markers, embedded formats | | knife yara RULES FILE | match YARA rules (RULES is a file or a directory) | | knife map FILE | whole-file entropy sparkline, packed regions flagged | | knife hex FILE --off O --len L | hex dump | | knife ls FILE | archive (.a/.lib) members | | knife completions SHELL | shell completion script (bash, zsh, fish, powershell, elvish) | | knife diff A B | compare two binaries' functions, imports, sections; exit 1 on any change |
Add --json to any analysis command for machine-readable output. knife FILE is shorthand for knife info FILE, and knife FILE --rules DIR folds a YARA pass into the verdict.
For vulnerability research
Triage asks whether a binary is hostile. Research asks a different question, where this binary can be broken, and four commands answer it.
What is it protected by. knife sec reads the mitigations out of the container and says what each missing one costs you. It separates a claim from a fact: a PE with DYNAMIC_BASE set but no .reloc section still loads at its preferred base, a GUARD_CF flag with an empty guard function table checks nothing, and an ELF with no PTGNUSTACK header at all gets an executable stack rather than a hardened one. Each line carries the consequence, so no _stackchk_fail reference is followed by what that means for a linear overflow.
Where the surface is. knife sinks matches the binary against a catalogue grouped by the mistake each API enables, unbounded copies, format strings, input-sized stack allocation, command execution, temp-file races, weak randomness, then resolves every one to concrete call sites. The output is addresses in named functions, not an import list: on kernel32.dll that is 327 call sites across 17 APIs, and on a vmlinux image it finds 204 strcpy and 220 sprintf sites in named kernel functions. Statically linked targets work too, because a defined symbol is matched the same way an import is.
Which ones are actually wrong. knife sinks still leaves you reading every call. knife audit reads them first: for each catalogued call it recovers where the interesting argument came from, and keeps only the sites whose provenance matches a bug pattern. A memcpy whose length was just computed by a subtraction (underflow to a huge size), an allocation sized by a multiply (integer overflow), a printf whose format string is loaded from memory rather than pointed at a constant, an unbounded strcpy reachable from an export. Each finding names the function, the address, and what is wrong, ranked so the exploitable-looking ones come first. The provenance is an intra-block backward walk over the argument registers (x86/x64): shallow on purpose, it stops at the previous call and stays quiet when it cannot see an origin, so a finding is worth the look. On a signed, hardened kernel32.dll it surfaces a dozen sites, not a thousand.
What reaches what. knife xrefs answers "who calls this" for a function, an imported API, or an address, and --str answers the other direction, which code touches this string. knife paths walks the call graph backwards from a sink to the entry point and the exports, printing the shortest chains, which is the reachability question that decides whether a sink is worth your afternoon.
knife sec ./target # what am I up against
knife sinks ./target --class memory # where could the bug be
knife audit ./target --reachable # which sites look actually exploitable
knife xrefs ./target --str "/tmp/" # who builds that path
knife paths ./target system # can anything reach it
For a worked example on a real, shipped binary, see finding CVE-2017-11882 with knife: sec shows the Equation Editor has no mitigations, audit flags the font-name copy, and dis confirms it is an lstrcpy of attacker data into a stack buffer.
What you worked out. Everything above is derived from the bytes and can be recomputed at any time. What cannot be recomputed is what you understood, so knife name and knife note write it down, and every later command reads it back. Naming an address is not cosmetic: it tells the engine there is a function there, which is how you make progress on a stripped binary.
knife name ./target 0x4017a0 parserecord # sub4017a0 is now parse_record
knife note ./target 0x4017c4 "len from packet" # shows up beside the instruction
knife funcs ./target | grep parse_record # and in every other command
knife dis ./target --func parse_record
Somewhere to do it. knife tui puts the same analysis behind a keyboard: the function list on the left, the listing and cross-references on the right. โต opens a function or follows the call under the cursor, โซ returns to where you followed from, / filters the function list (or searches the code when the listing is focused), g goes to an address or a symbol, d toggles the listing between disassembly and decompiled pseudocode, and n and c name and annotate whatever the cursor is on. s swaps the left pane for the ranked attack surface, the sink call sites the audit found worst first, and โต jumps to one. The xrefs pane is a list of its own: tab into it, and โต jumps to the reference's site. Operands that point at a literal are annotated with the string itself, in the listing and in the printed dis --func, and following one opens its bytes as a hex dump. The mouse works too: the wheel scrolls the focused pane, a click focuses and selects. Names and notes go to the same database the command line writes, as you make them, so quitting is not a save step and knife funcs in another terminal already agrees with you.
knife tui ./target
The database is keyed by the file's SHA-256, so it follows the binary rather than the path, and pointing knife at a different build never silently applies the wrong names. Addresses are stored relative to the image base, in hex, as a flat list, which means a database survives rebasing and can be diffed, hand-edited, or sent to somebody else who has the same sample. It lives under your platform's data directory by default; --db PATH puts it wherever you want, and knife db FILE says which file is in use.
What makes it more than objdump
Cross-format, one model. goblin parses PE/ELF/Mach-O into a single neutral model, so every command works on every format. Validated on a signed Windows DLL, a stripped RISC-V ELF, and a macOS x86-64 Mach-O.
A real analysis engine. knife funcs runs recursive-descent disassembly seeded from the entry point and every named symbol, follows calls and branches, splits basic blocks, resolves jump tables, builds a control-flow graph, and counts cross-references. knife dis --func prints a whole function with loc_ labels, resolved call targets, and xrefs-to. On kernel32.dll it recovers 2605 functions, 1515 of them named. Everything is in virtual-address space, so the address column, branch operands, and symbol names all agree.
It finds code control flow cannot reach. A stripped C++ binary reaches most of its functions only through vtables and function pointers, which recursive descent cannot follow, so descent alone sees a fraction of the code. On x64 Windows the PE exception directory lists every non-leaf function with its start address, so knife seeds from it: on 7-Zip's 7z.dll that is the difference between recovering 87 functions and recovering 6472, and between knife sinks finding one memcpy call site and finding 503. Chained unwind entries are continuations, not functions, and are skipped, so the seeds are one per real function.
Imports resolve to names. A call to a library function never goes there directly: ELF routes it through a .plt stub that jumps via a GOT slot, and PE linkers emit the same shape as jmp [IAT] thunks. knife follows both, so a call site reads call strcpy@plt or call KERNELBASE!CreateFileW instead of an anonymous sub_. That naming is what makes the sink and cross-reference commands point at code rather than at an import table.
A decompiler engine. knife pseudo --func NAME runs a small IR over each function: it lifts every instruction, propagates expressions (across block boundaries too) so a run of mov/lea/add collapses into what it computes, eliminates the dead intermediate assignments with a whole-function liveness pass, and folds constants. It runs a stack-frame analysis, so [ebp - 0x28] becomes the named local var28 and [ebp + 8] the argument arg8, and the frame bookkeeping (mov ebp, esp, the sub esp allocation, push ebp, leave) is dropped. The CVE-2017-11882 overflow comes out as the single line lstrcpyA(&var28, arg8 + 0x1c); with nothing around it. It then structures the control flow: dominators and post-dominators drive a recursive emitter that rebuilds nested if/else and while, and an indexed jump through a table becomes a switch on its selector with each case structured. A function reads as C rather than a goto chain. Conditions are recovered from whatever set the flags, including the arithmetic ops (dec, sub, ...) that branch without a cmp, and the compare is followed across blocks so a multi-way dispatch reads as a real if/else-if chain. The handful of edges that break nesting (a jump into a common handler) become an explicit goto to a labelled block, so the flow is preserved exactly, never approximated. It is not a full decompiler and does not pretend to be: there is no type recovery, and any instruction the lifter does not model is printed verbatim so you always see where the clean lift stops.
Constant scanning. knife scan fingerprints crypto and structure by their byte signatures: AES S-boxes (generated from the GF(2โธ) definition, not stored as literals), SHA-1/256/MD5/CRC32 constants in both byte orders, Base64 alphabets, packer markers, and embedded formats (zip, 7z, gzip, PNG, PDF, CLR metadata). Embedded PEs are confirmed by walking the DOS to PE header chain.
YARA built in. knife yara runs rules through yara-x, VirusTotal's pure-Rust engine, so there is no libyara C dependency. Matches can feed the triage verdict.
Transparent triage. The verdict (CLEAN / LOW RISK / SUSPICIOUS / MALICIOUS) is an additive score where every point is a named signal you can read. It weights concealment and anomaly (packing, RWX sections, high-entropy overlays, tiny import tables) above raw capability, because a system DLL legitimately exports powerful APIs. It shows what a binary can do and how it is built, and leaves intent to you.
Examples
# triage a sample and fold a rule directory into the verdict
knife sample.exe --rules ~/rules/
busiest functions, then read the hot one
knife funcs sample.exe --by-refs
knife dis sample.exe --func sub_401240
what crypto is this stripped blob doing?
knife scan blob.bin
pull defanged network indicators as JSON
knife iocs sample.exe --json | jq '.[] | select(.kind=="url").value'
Roadmap
- [x] Multi-format parsing (PE / ELF / Mach-O)
- [x] Static triage with a transparent verdict
- [x] Strings, IOCs, imphash, entropy map
- [x] Crypto/packer/embedded constant scanner
- [x] YARA (yara-x) matching
- [x] Analysis engine: functions, CFG, xrefs
- [x] IAT and PLT import-name resolution in disassembly
- [x] Exploit-mitigation audit (
knife sec) - [x] Sink call sites, code and data xrefs, call-graph reachability
- [x] Persistent analysis database: your names and notes, kept between sessions
- [x] Interactive TUI (function list / listing / xrefs, decompiled pseudocode, naming and notes)
- [x] Library-function identification (FLIRT-style)
- [x] AArch64 disassembly and PLT-veneer resolution
- [x] Argument-provenance bug audit (
knife audit) - [x] Function discovery from the PE exception directory (stripped C++ coverage)
- [ ] Function discovery from ELF
.eh_frame(the same win for ELF) - [x] Pseudocode view (
pseudo): lifted statements, calls with arguments - [x] Structured decompiler (
pseudo): if/else and while reconstruction,
Build from source
cargo build --release # target/release/knife
cargo test
Needs Rust 1.88 or newer (2021 edition). No system libraries beyond the platform default; the YARA engine and the terminal interface are both pure Rust.
Contributing
Issues and pull requests are welcome. Please run cargo fmt, cargo clippy, and cargo test before opening a PR; CI enforces all three across Linux, macOS, and Windows. See CONTRIBUTING.md.
License
MIT. See LICENSE.