go - 1-3-7/disrobe GitHub Wiki

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.

At a glance

Layer Coverage
Binary formats PE, ELF, Mach-O, on little- and big-endian targets
Architectures little-endian amd64/arm64 and big-endian s390x, ppc64, and mips, with the pclntab, type, and itab tables read in the image's own byte order
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 maps located through the compiler's own slice header, every member recovered byte for byte and checked against its stored digest, plus //go:embed directive extraction
Debug info DWARF report when the sections survive

Commands

disrobe go recover app --out app-go.json
disrobe go info app
disrobe auto packed-app --out recovered/    # unpack the UPX layer first, then recover the Go symbols

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.

UPX-on-Go chains automatically: disrobe auto unpacks the UPX layer first, then recovers the Go symbols underneath.

Coverage and fidelity

The garble report separates a real wall from a tooling boundary. garble -literals is handled separately from names. Simple rodata schemes are recovered by pairing adjacent key/data blobs and applying the inverse XOR/ADD/SUB or repeating-key operation. Full-key literals are recovered when their code and ciphertext are present: the thunk scanner follows bounded x86-64 init thunks or inline materializers, emulates the decrypt path, and accepts only UTF-8/readable plaintext. The tests assert the source strings are absent as cleartext before requiring byte-exact recovery, so the oracle is not circular.

An embed.FS map is located through the layout the Go compiler is required to emit rather than by matching a pattern. The compiler writes the slice header and the file records into one symbol and sets the records pointer to that symbol plus three pointer-sized words, so the header points at itself and the length equals the capacity. Discovery keys on that identity, which also fixes the record count from the header instead of reading records until one fails to parse. Ordering is not assumed: real compiler output places frontend/dist/style.css before frontend/dist/assets/, so a scanner that requires a sorted run rejects every genuine map.

Each file record carries the first sixteen bytes of a digest the compiler computed over the member contents, and the pass recomputes it. Members at or below 1024 bytes take a one-shot form and larger members take a streaming form, a boundary measured from real toolchain output rather than assumed. The toolchain generation is identified by which construction verifies the records, so no digest depends on trusting the version string an image reports, and a map whose members are all above the boundary is reported as unable to distinguish two generations rather than being assigned one. Recovery is graded against the exact files each fixture was built from, across PE, ELF, and Mach-O at both pointer widths, both byte orders, and both position-dependent and position-independent links. Go's internal linker materializes the record pointers even in a position-independent image, so the same discovery path serves both. A directory record, a zero-length member, and a member whose contents are not valid UTF-8 are all covered.

The type-name gate requires 100% of the independently named descriptors on the committed go1.26.3 linux/amd64 fixture: 115 of 115 names and addresses match go tool nm output from the unstripped compiler build after its symbols, string table and DWARF are removed. Name and address mutations must make the grade fail. The matrix also covers Go 1.17, 1.18 and 1.20, PIE and arm64; this population does not include every type in the program. Big-endian recovery has its own oracle: a cross-built stripped linux/s390x binary is parsed as a big-endian ELF and its named type and itab pairs are recovered by back-searching the metadata tables, graded against the build (go_bigendian_recovery.rs).

An independent grade over the unstripped PE fixture compares descriptor addresses and names with Go's DW_AT_go_runtime_type records. Typelinks recovery matches 485 of 699 DWARF runtime types. This Windows fixture and its DWARF population are separate from the Linux nm-addressed population above.

The committed Go-version matrix additionally checks Go 1.17, 1.18, 1.20, and the current toolchain on linux/amd64, linux/amd64 PIE, and linux/arm64. Its unstripped ELF binaries carry the matching go tool nm and go tool objdump records; recovery must retain the reported user method names and the Parcel and Ledger type and field names from each build.

Limits

  • The deliverable is symbols, types, and embedded data, not source bodies.
  • garble's hashed user identifiers are reported as a name_recovery_wall when the required build seed or name mapping is unavailable. The pass does not infer original names from the hashes. Surviving standard-library names in pclntab are recovered.
  • The remaining limits on literal recovery are concrete: missing bytes, runtime-only key material, unsupported architectures, exhausted budgets, or ambiguous short plaintext.
⚠️ **GitHub.com Fallback** ⚠️