SuperAPI AI Directory

re-rust

Coding & development

View repository

Updated 10-16 05:06

About this skill

Rust binary reverse engineering (symbol demangling / monomorphization / ownership)

When to use / when not to use

  • Use: after confirming it is a Rust-compiled binary (symbols carry the _ZN/_RNv prefix), 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 rust or 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' | rustfilt output foo::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 is g++filt) or brew install llvm(llvm-cxxfilt)
  • Windows: Linux version inside WSL or MinGW binutils
  • Verify: c++filt _ZN3foo3barE output foo::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=link is the default output;--emit=metadata contains no code but contains type/dependency information
  • Verify: rustc --emit=asm hello.rs produces .s files

bloaty (size analysis, used in pitfall 2; for installation see [[re-go]] tool preparation)

  • Purpose: bloaty -d symbols sample count 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]]).

  1. 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
    • .comment section fingerprint:readelf -p .comment sample directly gives rustc version <版本> (<commit>) and the linker version (actual 1.95 artifacts contain rustc version 1.95.0 (59807616e 2026-04-14) and Linker: 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
  2. 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 main and some std internal symbols (including .llvm. suffixed instances) are still legacy _ZN— cover both prefixes when identifying and batch demangling
  3. 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> and Vec<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)
  4. 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_dealloc corresponds 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/swap calls are common operations forced by the borrow checker
    • Large amounts of memcpy + size parameters in the decompilation → struct copy/Clone (derive(Clone))
  5. 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:: and deserialize symbols — 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

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 —nm no output, all functions sub_*; Cause — release profile sets strip = true or the release process strips (Rust has no recovery structure like Go's pclntab); Countermeasure — first confirm whether nm is 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 and panic_bounds_check/core::panicking::panic sites (index checks are retained even in release), plus unreachable! dead paths; Countermeasure — identify and skip core::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 match state 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; identify core::future/std::future::poll_with_tls_context calls; for tokio programs, look for tokio::runtime symbols and block_on to locate the entry point
  • v0 new-style naming cannot be demangled by c++filt: Symptom —c++filt/nm -C outputs _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 -C and could be demangled); Countermeasure — uniformly batch-process apt install rustfiltwith rustfilt (official Debian/Ubuntu package cargo install rustfilt, others nm 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_unwind symbols,.eh_frame is very small or missing, core::panicking call 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 default panic=unwind retains them; Countermeasure — use readelf -S to check .eh_frame existence + nm to find rust_begin_unwind/rust_panic to 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_alloc also 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...)+ .comment version fingerprint; the entry point is _start rather than main, allocator symbols (__rust_alloc or a custom malloc) confirmed as needed; such firmware is often configured with [[re-emulation]] for environment-free emulation