When to use / when not to use
- Use: after confirming it is a Rust-compiled binary (symbols carry the
_ZN/_RNvprefix), demangle to restore symbols; identify duplicate code produced by generic instantiation (monomorphization); understand object lifetimes through ownership/destructor (drop) patterns; analyze async/await state machines and library patterns such as tokio/serde - Use: malicious Rust samples ([[re-malware]] static recovery—Rust-rewritten trojans/ransomware are increasing)
- Do not use: non-Rust native programs (C/C++/Go go directly to [[re-binary-core]]; Go goes to [[re-go]])
- Do not use: if packing is confirmed, first go to [[re-anti-analysis]] (Rust binaries are generally unpacked, but large in size)
- Note: dynamic steps default to sandbox ([[re-analyze/platform-tips]] highest principle); static first (principle for large samples)
Tool preparation
Refer to [[re-analyze/platform-tips]]—decompilation/symbols are static steps, no sandbox needed; Rust binaries are usually 1-10MB+, so static locating comes first.
Rust toolchain (rustc/cargo—install the symbol tools, optional)
- rustup recommended (official installer, latest on all platforms):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Debian/Ubuntu:
apt install rustc cargo(official package, version may lag); Fedora:dnf install rust cargo;Arch:pacman -S rust - macOS:
brew install rust;Windows:choco install rustor rustup - Verify:
rustc --version && cargo --version
rustfilt (Rust symbol demangle, required for the new v0 _RNv )
- Debian/Ubuntu:
apt install rustfilt(official package) - Other distros have no official package → official install:
cargo install rustfilt - Verify:
echo '_ZN3foo3barE' | rustfiltoutputfoo::bar
c++filt(legacy _ZN can be demangled; v0 cannot)
- Linux: bundled with binutils (
apt install binutils/dnf install binutils/pacman -S binutils) - macOS:
brew install binutils(the command isg++filt) orbrew install llvm(llvm-cxxfilt) - Windows: Linux version inside WSL or MinGW binutils
- Verify:
c++filt _ZN3foo3barEoutputfoo::bar
Ghidra / IDA (decompilation base, see the corresponding skill for installation)
- Ghidra 11+ has a built-in Rust demangler (analysis option "Demangler Rust", see [[re-ghidra]])
- IDA has none built in → community Rust demangler plugin (e.g. idarustdemangler), or preprocess the symbol table with rustfilt first
- Verify: decompiler function names appear in readable form (rather than
_ZN.../_RNv...gibberish)
nm / readelf (symbol table and sections, binutils)
- Install the same way as c++filt (bundled with binutils)
- Verify:
nm --version
rustc --emit (understanding compiler output, optional)
cargo rustc -- --emit=asm/--emit=llvm-ir/--emit=mir: recompile the sample with a matching version to obtain symbol-bearing assembly/IR/MIR, and compare to understand compiler expansion (monomorphization generic instances, async state machines, drop glue insertion points)--emit=linkis the default output;--emit=metadatacontains no code but contains type/dependency information- Verify:
rustc --emit=asm hello.rsproduces.sfiles
bloaty (size analysis, used in pitfall 2; for installation see [[re-go]] tool preparation)
- Purpose:
bloaty -d symbols samplecount size proportion by symbol (monomorphization expansion volume is considerable) - Verify:
bloaty --version
Operating steps
Execute in order, recording evidence at each step (path + sha256, see [[re-triage]]).
-
Identify Rust:
file sample nm sample | head -20 # 出现 _ZN...(legacy)或 _RNv...(v0)即 Rust strings sample | grep -iE 'rustc|rust_begin_unwind|panicking|__rust_alloc'- Characteristic symbols:
_ZN4core9panicking...E(panic infrastructure),_ZN3std2rt...E(std runtime),__rust_alloc/__rust_dealloc(allocator replacing malloc/free) - The std program entry point is
main; no_std (embedded) uses_start .commentsection fingerprint:readelf -p .comment sampledirectly givesrustc version <版本> (<commit>)and the linker version (actual 1.95 artifacts containrustc version 1.95.0 (59807616e 2026-04-14)andLinker: LLD 22.1.2) — check here first for the version fingerprint, faster than guessing the panic style- panic messages leak source paths:
strings sample | grep 'panicked at'the path after it looks like/rustc/<commit>/library/std/src/...,<commit>i.e. the exact toolchain version; business code paths are likewise in the messages
- Characteristic symbols:
-
Symbol demangling (demangle):
nm -C sample | grep main # 内置 demangle(仅 legacy _ZN 可解) nm sample | rustfilt > symbols.txt # 全量 demangle(v0/legacy 通吃) grep -iE 'main|tokio|serde' symbols.txt- v0 new style (
_RNv) can only be demangled by rustfilt/Ghidra 11+, c++filt spits it out as-is (pitfall 5) - Ghidra 11+ auto-demangles after import: the function tree is directly readable; confirm symbol retention: no output → pitfall 1
- After v0 becomes the default the artifact is still mixed naming: actual 1.95 artifacts are overwhelmingly v0 symbols (455 vs legacy 30), but
mainand some std internal symbols (including.llvm.suffixed instances) are still legacy_ZN— cover both prefixes when identifying and batch demangling
- v0 new style (
-
monomorphization identification:
grep -oE '::[a-z_]+<[^>]*>' symbols.txt | sort | uniq -c | sort -rn | head- The same generic name with multiple type parameters = multiple instance copies of the code (e.g.:
Vec<u8>andVec<i32>each have a complete expansion) - Analysis strategy: pick the representative instance of the most common type parameter, compare only type-related branches between instances in the same group; skip duplicate code (pitfall 2)
- The same generic name with multiple type parameters = multiple instance copies of the code (e.g.:
-
Ownership/borrow patterns:
- drop:
core::ptr::drop_in_place::<T>call = object destructor point (vtable drop glue), follow it to locate struct lifetimes and fields - Allocation:
__rust_alloc/__rust_dealloccorresponds to malloc/free;alloc::boxed::Box(exclusive heap),alloc::sync::Arc(reference count increment/decrement),alloc::vec::Vec/alloc::string::String(capacity change triggers realloc) core::mem::replace/take/swapcalls are common operations forced by the borrow checker- Large amounts of memcpy + size parameters in the decompilation → struct copy/Clone (derive(Clone))
- drop:
-
Common library patterns:
- tokio/async:
tokio::runtime::Runtime::block_on、tokio::task/executor symbols; async fn expands into a state machine (pitfall 4) - serde:
serde::de::/serde::ser::anddeserializesymbols — the entry point for config/protocol parsing, follow it to find the data format - Network:
std::net::TcpStream、tokio::net::; logging/config:log::/tracing::、serde_json - Error messages/URLs/config strings in strings help locate the target logic
- tokio/async:
Cross-domain union
- [[re-binary-core]]: this skill is referenced by the core gateway — initial survey [[re-triage]], decompilation base [[re-ghidra]]/[[re-ida]], library fingerprinting [[re-imports]]
- [[re-ghidra]]: prefer the built-in Rust demangler in 11+; [[re-ida]]: Rust demangler plugin or rustfilt preprocessing
- [[re-emulation]]: can assist with simulated execution when no_std/embedded Rust firmware has no runtime environment
- Malicious scenarios: for Rust malware samples (many ransomware/trojan rewrites), statically follow this skill → for behavior analysis switch to [[re-malware]]
- [[re-analyze/platform-tips]] related branches: static-first (large samples), dynamic default sandbox, Wine/QEMU branches for cross-platform samples
Common pitfalls and traps
- Symbols are stripped, extremely hard to recover: symptom —
nmno output, all functionssub_*; Cause — release profile setsstrip = trueor the release process strips (Rust has no recovery structure like Go's pclntab); Countermeasure — first confirm whethernmis really empty (most release builds keep symbols by default, see step 2 for troubleshooting); after stripping, use strings to find panic messages/error info (containing file paths and type clues),.eh_frame/.gcc_except_table's FDEs can recover function boundaries (enable unwind parsing in Ghidra analysis options) - monomorphization produces massive duplicated code: Symptom — the same logic function appears dozens to hundreds of times, differing only by type, causing the function list to explode; Cause — generics are instantiated per type parameter, one complete copy of the code per instance (size also balloons); Countermeasure — after demangling, group and count by generic name, pick one representative instance to analyze, and diff within the same group only comparing type-related branches; confirm size/duplication issues with bloaty (
bloaty -d symbols); ignore the remaining instances during analysis - panic paths interleaved with normal paths: Symptom — in the decompilation every index/
unwrap/assertion comes with branches and panic calls, drowning out readability; Cause — Rust insertsunwrap()、assert!checks at out-of-bounds indexing andpanic_bounds_check/core::panicking::panicsites (index checks are retained even in release), plusunreachable!dead paths; Countermeasure — identify and skipcore::panicking::/std::panicking::/panic_impl-related calls and exception branches; panic messages are visible in strings and can help locate call sites in reverse - async/await state machine expansion: Symptom — an async fn becomes a struct + a huge
matchstate loop, with function names containing{{closure}}/{{opaque}}, and no linear flow can be found; Cause — async fn compiles into a Future state machine, where each await point is a state and save point, and transitions happen by state inside poll; Countermeasure — trace transition logic by state number (0..N), focus on reading the poll method; identifycore::future/std::future::poll_with_tls_contextcalls; for tokio programs, look fortokio::runtimesymbols andblock_onto locate the entry point - v0 new-style naming cannot be demangled by c++filt: Symptom —
c++filt/nm -Coutputs_RNv...as-is, and the symbols remain garbled; Cause — v0 mangling has been optional since 1.37 (-C symbol-mangling-version=v0), and became the default from the end of 2025 (before that, most binaries from 2019-2025 were still legacy_ZN,nm -Cand could be demangled); Countermeasure — uniformly batch-processapt install rustfiltwith rustfilt (official Debian/Ubuntu packagecargo install rustfilt, othersnm sample | rustfilt); Ghidra 11+ has a built-in demangler that directly demangles automatically - panic strategy determines artifact form (unwind vs abort): Symptom — some artifacts have no
rust_begin_unwindsymbols,.eh_frameis very small or missing,core::panickingcall sites in the decompilation are directly termination paths, and during behavior analysis the program exits entirely; Cause —-C panic=abort(common in embedded/size-sensitive/security-hardened builds) removes unwind tables and recovery paths, while only the defaultpanic=unwindretains them; Countermeasure — usereadelf -Sto check.eh_frameexistence +nmto findrust_begin_unwind/rust_panicto determine the strategy; in abort artifacts there is no need to look for recovery logic after panic, since panic means termination; panic messages (panicked at) exist under both strategies - no_std artifacts have no std runtime symbols: Symptom — cannot find
_ZN3std2rt...E、__rust_allocalso sometimes present, sometimes absent; reason—no_std (embedded/kernel) does not link std, only core + a custom allocator; countermeasure—identify instead by looking at core features (_ZN4core...)+.commentversion fingerprint; the entry point is_startrather thanmain, allocator symbols (__rust_allocor a custommalloc) confirmed as needed; such firmware is often configured with [[re-emulation]] for environment-free emulation