Reversing Rust and Go
TL;DR: Rust monomorphises generics into a flood of similarly-named symbols and embeds panic messages as strings; Go has its own ABI, runtime, and goroutine scheduler — both demand workflow tweaks vs C/C++.
What it is
Rust and Go produce native binaries that look superficially like C but bring runtime structures that confuse generic tooling. Knowing the patterns saves enormous time. Complements csharp-python-reverse for managed runtimes and reverse-engineering-overview for the generic workflow.
Preconditions / where it applies
- Native binary written in Rust or Go (commonly statically linked, large, and stripped).
- Disassembler with the right plugins (ghidra-decompiler GolangAnalyzer, IDA
golang_loader_assist, binary-ninja Go workflow plugins).
Technique
Rust
Triage signs:
__rust_alloc,core::panicking::panic_fmt,rust_begin_unwindimports.- Long mangled symbols (
_ZN4core3fmt9Formatter3pad17h<hash>E). - Panic strings with file paths like
src/main.rs:42:18.
Workflow:
- Run
rustfilt(orc++filt --hint=rust) to demangle. - Look at panic strings — they include source file + line numbers in debug or non-stripped release builds and provide a map back to source structure.
- Generic functions are monomorphised:
Vec<u8>::pushandVec<String>::pushare two distinct compiled functions. Expect duplication. - Trait dispatch becomes vtable lookups in
.rodata; tag the vtables. Result/Optionshow up as small structs returned by value ({tag, data}); ABI returns inrax/rdx.
Common runtime fingerprints:
__rustc_debug_gdb_scripts_section__section in non-stripped builds.- Use of
jemalloc/mimallocallocators changes heap layout. - LLVM features (autovectorisation, MIR optimisation) leave many small functions.
Go
Triage signs:
runtime.morestack_noctxt,runtime.goexit,go.buildid,go.itab.*, densegopclntabsection.- File paths like
_cgo_export.c, package pathsgithub.com/.... - All-in-one static binary, several MB even for “hello world”.
Key runtime structures:
pclntab(runtime.pclntab/gopclntab) — function names + file/line tables. Recover symbols even from stripped binaries; tools (redress,golang_loader_assist, GolangAnalyzer) parse it and restore names.moduledatapoints topclntab, types, itabs.itab— interface dispatch tables.- String =
(ptr, len)pair. - Slice =
(ptr, len, cap).
Calling convention:
- Pre-Go 1.17: all args on the stack.
- Go 1.17+: register-based ABI (
AX, BX, CX, DI, SI, R8, R9, R10, R11for ints on amd64). Decompiler output looks much cleaner.
Workflow:
- Identify Go version (look for
go1.NN.Mstring). - Run
redressor the GolangAnalyzer Ghidra plugin to restore function names + types. main.mainis the user entry point (notruntime.main).- Goroutines complicate dynamic tracing;
delve(dlv) is the language-aware debugger and beats raw gdb here.
1
2
strings -n 8 sample | grep -E '^go1\.[0-9]+\.[0-9]+'
redress -p sample > funcs.txt
Detection and defence
- Rust: strip aggressively (
strip -s,panic = "abort",lto = true,codegen-units = 1) to remove panic file/line strings. - Go:
-ldflags="-s -w"strips DWARF + symbol table butpclntabremains by design.-trimpathremoves absolute build paths. - Neither hides logic from a determined analyst — they merely raise cost.
References
- Reversing Go binaries (Mandiant) — gopclntab walkthrough
- Rust binary triage (CheckPoint) — demangling and panic-string recovery