Go
disrobe recovers symbols from stripped and garbled Go binaries across PE, ELF, and Mach-O by parsing the Go runtime's own metadata tables. The deliverable is symbols, types, and embedded data, not source bodies.
At a glance
| Layer | Coverage |
|---|---|
| Binary formats | PE, ELF, Mach-O |
| pclntab | Header eras go1.2, go1.16, go1.18, and go1.20, located structurally even when the magic word has been stomped |
| Symbol recovery | pclntab function table, moduledata, typelinks/itablinks type metadata, buildversion |
| Obfuscation | garble report graded None / Detected / Partial / Full, with per-scheme literal-recovery statistics |
| Embedded data | embed.FS usage report and directive extraction |
| Debug info | DWARF report when the sections survive |
Recovering a binary
disrobe go recover app --out app-go.json
disrobe go info app
recover writes the full analysis JSON (default ./out/<stem>-go.json); info prints the fingerprint without writing anything. Output shape (illustrative):
go recover: OK
input: app
image kind: elf
ptr size: 8
pclntab ver: go1.20
buildversion: go1.26.3
funcs: ...
packages: ...
garble: None
embed.FS: used=true directives=...
wrote: ./out/app-go.json
info adds the stripped-binary fingerprint: whether the symbol table was stripped, how many functions were still recovered from pclntab, and the stdlib-name ratio that feeds the garble grading.
Garble
The garble report separates a real wall from a tooling boundary. Standard-library names survive in pclntab and are recovered, while hashed user identifiers stay walled: garble hashes them with a keyed HMAC-SHA256 over a build seed that is not in the binary, so the original names are information-theoretically gone and are reported as a name_recovery_wall rather than guessed at.
The garble -literals string encryption is a different story and is not a one-time pad. Each literal is decrypted by an init-time thunk whose key material is itself compiled into the binary, so the plaintext is statically recoverable by emulating that thunk. For the simple obfuscator disrobe does exactly this: garble emits both the key[] and data[] byte arrays as rodata []byte literals and the runtime loop computes plaintext = data <op> key (op in XOR/ADD/SUB), so disrobe locates the adjacent equal-length blob pair, applies the inverse op, strips the junk padding, and bridges the scattered external-key byte-mutations. Recovery is counted per scheme (plain_ascii, single_xor, single_add, single_sub, repeating_xor, garble_simple) and the simple-scheme scan only runs once garble is positively detected, so a normal Go binary is never mined for phantom literals. The residual boundary is concrete rather than information-theoretic: the external-key mutation index and op live as immediate operands in the decrypt thunk's machine code (not in rodata), and the swap/split/shuffle/seed obfuscators wrap each literal in its own control flow, so byte-exact recovery of heavily mutated or control-flow obfuscated literals needs full decrypt-thunk emulation over the .text immediates. The key material is present in the file in every case, which is what makes this a depth boundary and not a wall.
Validation and chaining
The pass is validated against a go1.26.3 fixture, and the test suite gates type-name recovery at >= 85% on that fixture. UPX-on-Go chains automatically: disrobe auto unpacks the UPX layer first, then recovers the Go symbols underneath.