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

LayerCoverage
Binary formatsPE, ELF, Mach-O
pclntabHeader eras go1.2, go1.16, go1.18, and go1.20, located structurally even when the magic word has been stomped
Symbol recoverypclntab function table, moduledata, typelinks/itablinks type metadata, buildversion
Obfuscationgarble report graded None / Detected / Partial / Full, with per-scheme literal-recovery statistics
Embedded dataembed.FS usage report and directive extraction
Debug infoDWARF 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.