unstrip 1.2.0

Recover symbols, types, and method signatures from stripped Go binaries. Ghidra/IDA/Binary Ninja exporters included.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# unstrip

[![Crates.io](https://img.shields.io/crates/v/unstrip.svg)](https://crates.io/crates/unstrip)
[![CI](https://github.com/riven-labs/unstrip/actions/workflows/ci.yml/badge.svg)](https://github.com/riven-labs/unstrip/actions/workflows/ci.yml)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
[![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org)
[![Go versions](https://img.shields.io/badge/go-1.18--1.25-00ADD8.svg)](https://go.dev)
[![Platforms](https://img.shields.io/badge/platforms-linux%20%7C%20macos%20%7C%20windows-lightgrey.svg)](#install)

> Recover function names, method signatures, types, and interface dispatch tables from stripped Go binaries, with inlined call stacks the leaf-only tools miss.

Stripped Go binaries still carry `pclntab`, moduledata, typelinks, and itablinks because the runtime needs them for stack traces and reflection. `unstrip` reads all of it: every function with file and line, every method with its Go-syntax signature where the linker kept the type record, every type with full struct layout, every `(interface, concrete)` pair the dispatcher uses, and the module dependency tree. ELF, Mach-O, PE. amd64 and arm64. Go 1.18 through 1.25.

## Install

```
cargo install unstrip
```

Prebuilt binaries for Linux, macOS, and Windows are on the [releases page](https://github.com/riven-labs/unstrip/releases).

## Quick start

```
unstrip ./bin                       # function names, files, lines
unstrip ./bin --info                # container, Go version, garble heuristic
unstrip ./bin --format ghidra > apply.py    # Python script for Ghidra Script Manager
```

`unstrip -h` prints a one-screen cheat sheet. `unstrip --help` lists every flag grouped by section. The full reference with one paragraph per flag lives in [docs/USAGE.md](./docs/USAGE.md).

## Why unstrip

Method signatures attached to recovered symbols. Every method on a type the Go linker emitted a `_type` record for gets a Go-syntax signature like `(_0 []uint8) (int, error)` appended to its name in the default listing, in `--addr` lookups, and in the Ghidra/IDA/Binja exporter comments. Other Go-binary symbol-recovery tools do not recover signatures at all.

Inlined call stacks, not just leaf functions. `--addr` returns the full inline tree from `FUNCDATA_InlTree`, so a PC that lands in inlined code resolves to the chain that led there, with file and line for every frame.

Built-in Ghidra, IDA, and Binary Ninja Python exporters via `--format`, with C struct decls and correct field offsets. One Rust binary emits scripts for all three RE tools; `--install-plugin <ida|ghidra|binja>` drops a wrapper plugin into the user's plugin directory.

`--data-at <addr>` interprets bytes at a data address through the recovered itab table and function table. Iface headers resolve to concrete type plus dispatched method body. Slice headers expand to ptr/len/cap with the data pointer symbolized. String headers print a quoted preview.

`--xref <symbol>` enumerates every call site targeting a function, on amd64 and arm64 binaries alike. Direct CALL/BL coverage is solid on both. Indirect dispatch through itab method slots is reported alongside direct calls with the resolved itab and method name carried inline.

A 1.4 MiB static Rust binary, no runtime deps. Go 1.18 through 1.25, ELF + Mach-O + PE, amd64 + arm64, PIE + non-PIE, one tool.

## Compared to

Categorical comparison against GoReSym, gore, and redress lives in [COMPARISON.md](./COMPARISON.md). Headline: similar core function recovery; the tools differ on what they include in the type catalog, on which RE-tool integrations they ship, and on how they handle obfuscated binaries.

A short verified-only table follows. The longer differences and the "when to reach for which" guidance are in COMPARISON.md.

| Capability                              | unstrip          | GoReSym    | redress  | gore     |
|-----------------------------------------|------------------|------------|----------|----------|
| CLI vs library                          | CLI              | CLI        | CLI      | library  |
| Go 1.18 through 1.25                    | yes              | yes        | yes      | yes      |
| Pre-Go-1.18                             | no               | Go 1.2+    | Go 1.5+  | Go 1.5+  |
| Method-signature recovery               | yes              | no         | no       | no       |
| Ghidra + IDA + Binja exporters          | yes              | yes        | no       | no       |
| Inlined call stacks on PC lookup        | yes              | unverified | no       | no       |
| `--data-at` symbolic data inspection    | yes              | no         | no       | no       |
| `--xref` on amd64 and arm64             | yes              | n/a        | n/a      | n/a      |
| Static binary, no runtime deps          | yes (Rust)       | yes (Go)   | yes (Go) | n/a      |

## Use

```
unstrip ./samples/hello.stripped
```

Function names, source files, line numbers, one function per line. Methods on types the binary carries a `_type` record for get their Go-syntax signature appended:

```
$ unstrip ./crypto-pipeline.stripped
0x00000000004b3f20  main.(*HashingReader).Read                                                    main.go
0x00000000004b4000  main.(*CountingWriter).Write(_0 []uint8) (int, error)  (itab thunk)           main.go
0x00000000004b4280  main.(*AesGcm).Encrypt(_0 []uint8) ([]uint8, error)  (itab thunk)             main.go
0x00000000004b4820  main.(*Pipeline).ProcessFrame                                                 main.go
0x00000000004b4a40  main.(*Pipeline).StreamThrough                                                main.go
0x0000000000465b40  errors.(*errorString).Error() string                                          errors/errors.go
0x0000000000479e00  os.(*File).Write(_0 []uint8) (int, error)                                     os/file.go
0x00000000004b4d40  main.main                                                                     main.go
...
```

Parameter names are not in the binary; positional placeholders (`_0`, `_1`, ...) keep the shape correct. Coverage scope and what to expect:

- **Methods that implement an interface** (`CountingWriter.Write`, `AesGcm.Encrypt`, `errorString.Error`, `os.File.Write` above): signatures recover reliably. The `(itab thunk)` marker shows the iface dispatch path; the signature on that row comes from the interface's declared funcType.
- **Methods on types reached only through internal calls** (`HashingReader.Read`, `Pipeline.ProcessFrame`, `Pipeline.StreamThrough` above): the Go linker may have elided the type's `_type` record because no runtime path needs it. The function name is recovered from `pclntab`; the signature is not in the binary. unstrip prints the bare name for those.
- **Free top-level functions** (`main.main`, `main.NewAesGcm`, etc.): never had per-function signature records to begin with. Bare names.

Pass `--no-signatures` for the older shorter listing.

### Container, version, garble check

```
$ unstrip hello.stripped --info
go version:    go1.22.2
container:     ELF (amd64, little-endian)
pclntab:       0x00000000000c0c40 (405 KB)
functions:     1556
text start:    0x0000000000401000
ptr size:      8
quantum:       1
```

When the binary's been obfuscated, `--info` says so instead of pretending the symbols are real:

```
$ unstrip suspicious.bin --info
go version:    (not detected)
...
garble heuristic: likely garbled
  - pclntab magic is not the standard 0xfffffff1 (garble rewrites it)
  - runtime.buildVersion is missing or non-standard (garble overwrites it)
  - 554/4310 user-package function names look hashed
```

For a structured verdict and exit-code semantics (0 garbled, 1 clean, 2 indeterminate), use `--detect-garble`.

### Module dependency tree

```
$ unstrip ./samples/myapp --buildinfo
go version:    go1.22.2
path:          example.com/myapp
main module:   example.com/myapp  v1.4.2

dependencies (12)
  github.com/spf13/cobra          v1.8.0
  github.com/aws/aws-sdk-go-v2    v1.30.5
  github.com/jmespath/go-jmespath v0.4.0
  ...

build settings
  buildmode      exe
  GOOS           linux
  vcs            git
  vcs.revision   3f8a912...
  vcs.modified   false
```

### Types

From a stripped binary, no debug info, no SDK installed:

```
$ unstrip ./samples/myapp --types --filter cobra.Command
0x00000000005c01a0  struct     size=728     *cobra.Command
    +0000  Use: type@0x589ec0
    +0010  Aliases: type@0x588960
    +0028  SuggestFor: type@0x588960
    +0040  Short: type@0x589ec0
    +0050  GroupID: type@0x589ec0
    ...
    +02c8  TraverseChildren: type@0x58a2c0
    +02c9  Hidden: type@0x58a2c0
    +02ca  SilenceErrors: type@0x58a2c0
    +02d0  SuggestionsMinimumDistance: type@0x58a100
```

Full struct layouts, including unexported fields. Field types are cross-referenced by address so you can navigate the type graph. JSON output is wired for tooling.

### Interfaces

The `(interface, concrete)` pairs the runtime uses for dispatch, what your dynamic dispatch sites actually go to:

```
$ unstrip ./samples/myapp --itabs --filter Writer
0x00000000005a18c8  *io.Writer        =>  *os.File
0x00000000005a1948  *io.Writer        =>  *bytes.Buffer
0x00000000005a19a8  *io.Writer        =>  *gzip.Writer
0x00000000005a1a08  *io.StringWriter  =>  *bufio.Writer
```

### Reverse PC lookup, with inline call stacks

For use inside a debugger, a crash dump parser, or a disassembly comment generator. When the PC lands in inlined code, you get the full call stack from the inline tree, not just the leaf:

```
$ unstrip caddy --addr 0x4151a4
(inlined)  runtime.evacuated  /usr/lib/go-1.22/src/runtime/map.go:205
(physical) runtime.mapaccess2  /usr/lib/go-1.22/src/runtime/map.go:457
```

For batch lookups against a crash dump, use `--addr-file`. One parse, N lookups:

```
$ cat crash.txt | unstrip suspicious.bin --addr-file -
(inlined)  io.copyBuffer  /usr/lib/go-1.22/src/io/io.go:418
(physical) io.Copy        /usr/lib/go-1.22/src/io/io.go:388
0x000000000045a200  net.(*conn).Read  /usr/lib/go-1.22/src/net/net.go:179
0x00000000004b8c40  main.handleClient  /tmp/build/main.go:42
```

If the runtime addresses came from an ASLR-rebased process, pass `--rebase <DELTA>` (the difference between actual load base and link-time preferred base) and unstrip translates back to the link-time VA on disk.

### Apply to your RE tool of choice

`--format ida`, `--format ghidra`, or `--format binja` emits a Python script that, run inside that tool, applies every recovered function (with file:line as a function comment) **and every recovered struct type** as a C declaration the tool's parser can ingest:

```
$ unstrip ./samples/myapp --format ghidra > apply.py
# In Ghidra: Window -> Script Manager -> Run apply.py
```

Function names sanitize down to valid disassembler labels; the original Go name (with parens, brackets, generics arguments) is preserved as a function comment.

For a menu-item install instead of running the script every time:

```
unstrip --install-plugin ida      # ~/.idapro/plugins/, registers as Edit -> Plugins -> Load Go symbols (unstrip), Ctrl-Shift-G
unstrip --install-plugin ghidra   # ~/ghidra_scripts/, run from Script Manager
```

A Binary Ninja installer (`--install-plugin binja`) drops the wrapper into the Binary Ninja plugins directory and registers a menu item under Plugins -> Load Go symbols (unstrip).

### Bake symbols into a runnable binary

`--symbols-as elf` writes a new ELF with a populated `.symtab` + `.strtab` built from the recovered functions. The result is a strict superset of the input that still runs, but `nm`, `gdb`, `objdump --syms`, `perf`, `addr2line`, eBPF stack traces, and `delve` all see the names.

```
$ nm helm
nm: helm: no symbols

$ unstrip --symbols-as elf -o helm.symbols helm
wrote 75630 symbols to helm.symbols

$ nm helm.symbols | head -3
0000000000401000 T fatalf
00000000004012b0 T _cgo_get_context_function
00000000004011e0 T _cgo_set_stacklo

$ gdb -q helm.symbols -ex 'info functions main.main' -ex quit
0x00000000027f50e0  main.main
0x00000000027f53a0  main.main.func1
```

Use `--in-place --yes` to overwrite the input file. Today supports ELF64 little-endian only; Mach-O and PE rewrite ships later.

### Port symbols between builds

`--diff <OLD_BINARY>` is a name-port helper, not a structural binary differ. It pairs functions across two builds in two passes: first by exact address match (`identical`), then by exact Go symbol name at a new address (`renamed`). Everything else falls into `added` or `removed`. It does not hash basic blocks, fingerprint control-flow graphs, or follow functions across inlining, splitting, or compiler-driven renames. If a function was inlined away, renamed by the toolchain, or restructured, this tool will report it as `removed` plus `added` rather than matching it.

For real structural diffing across optimization and inlining changes, reach for BinDiff or Diaphora. `--diff` here exists for the narrow but common case where you annotated v1.0 in your disassembler, v1.1 just shipped, and you want the symbol names you already recovered to land on the obvious counterparts in the new build:

```
$ unstrip ./malware-v1.1.bin --diff ./malware-v1.0.bin
old:        4687 functions
new:        4823 functions

identical:  4421
renamed:    198
moved addr: 12 (same address, different name)
added:      192 (in new, not in old)
removed:    68 (in old, not in new)
```

Pair with `--port-symbols ida|ghidra|binja` to emit a script that renames every paired function in the new binary using whatever name it had in the old one:

```
$ unstrip ./malware-v1.1.bin --diff ./malware-v1.0.bin --port-symbols ghidra > port.py
# In Ghidra (with v1.1 loaded): Script Manager -> port.py
```

### Cross-references and call graph

`--xrefs` scans `.text` for direct CALL/BL targets and resolves each pair against the recovered function set. Combined with `--from`, `--to`, `--depth`, or `--callgraph`, it answers the questions an analyst opens IDA to ask:

```
$ unstrip ./helm --xrefs --from main.main --depth 1 | head
main.main -> github.com/spf13/cobra.(*Command).ExecuteC
main.main -> main.newRootCmd
main.main -> main.warning
main.main -> os.Exit

$ unstrip ./helm --xrefs --to crypto/rsa.SignPSS
crypto/tls.(*signOpts).Sign -> crypto/rsa.SignPSS

$ unstrip ./helm --xrefs --callgraph > graph.dot && dot -Tsvg graph.dot -o graph.svg
```

250,000+ caller-callee edges enumerated on a 65 MiB helm binary in under 300 ms. Direct calls only; virtual dispatch through itabs lives in `--itabs`.

### Targeted xref by symbol

`--xref <SYMBOL>` answers "who calls this," scoped to one target. Faster than building the whole graph when you only have one question, and grouped by caller so the output reads the way a human reads xrefs in IDA:

```
$ unstrip ./helm --xref runtime.mallocgc | head
runtime.makechan @ 0x000000000040a260:
  0x000000000040a2ee  direct
  0x000000000040a340  direct
  0x000000000040a380  direct
runtime.convT @ 0x000000000040fb00:
  0x000000000040fb29  direct
```

Accepts a function name (resolved through pclntab) or a hex address (`0x...`). When the target is an interface-method implementation, indirect dispatch sites of the form `CALL [rip+itab+slot]` are reported alongside direct calls with the resolved itab and method name carried inline. The compiler usually loads the slot pointer into a register first; those register-indirect sites do not show up here because resolving them requires basic-block tracking we deliberately don't ship. In practice the indirect-itab scanner finds matches on hand-written assembly and unusual codegen; expect zero hits from stock Go compiler output. The strict-encoding hits that do appear carry zero false positives.

amd64 and arm64. Direct-CALL coverage is solid on both; indirect-itab on arm64 catches the canonical `LDR Xt, [Xn, #slot]; BLR Xt` pair but misses other dispatch patterns the compiler emits, pending a real disassembler integration.

### Goroutines and deferred calls

`--goroutines` lists every `runtime.newproc` and `runtime.deferproc` call site in `.text` and resolves the target function the goroutine or deferred call will run when possible. Surfaces the control flow hidden behind `go func() { ... }` and `defer` in Go programs that other tools don't:

```
$ unstrip ./sliver-client --goroutines | head
0x0000000000424798  runtime.newproc         -> runtime.runFinalizers (0x424880)        runtime/mfinal.go:169
0x00000000004259c0  runtime.newproc         -> (target unresolved)                     runtime/mgc.go:209
0x00000000004479d5  runtime.newproc         -> runtime.forcegchelper (0x447a00)        runtime/proc.go:360
0x000000000045e480  runtime.newproc         -> runtime.ensureSigM.func1 (0x4767e0)     runtime/signal_unix.go:1061
0x000000000049c4ae  runtime.newproc         -> (target unresolved)                     sync/waitgroup.go:235
```

When the target resolves (40-50% of sites in real binaries), you see exactly which function the goroutine runs. When the LEA pattern doesn't match the heuristic (the funcval came from a register or stack slot), the call site and source file:line still show so you can open the source. amd64 and arm64 only today.

### Dispatch resolver for Ghidra

`--dispatch-resolver ghidra` emits a Ghidra Python script that embeds the recovered itab dispatch table and, when invoked at a virtual call site, prints every `(interface, concrete impl)` pair whose method table contains the dereferenced slot:

```
$ unstrip ./target --dispatch-resolver ghidra > unstrip_dispatch.py
# Inside Ghidra: Script Manager -> add unstrip_dispatch.py
# Place cursor on `CALL qword ptr [reg + 0x18]` and run the action.
# Output (console):
#   unstrip dispatch: looking for itabs with a method at slot 3 (offset 0x18)
#     io.Writer => *os.File :: Write -> 0x4b9020
#     io.Writer => *bytes.Buffer :: Write -> 0x4c1a40
#     io.Writer => *bufio.Writer :: Write -> 0x4cd900
#   unstrip dispatch: 3 candidates
```

This turns virtual dispatch through an itab, the worst class of stripped-Go reversing target, into a five-second lookup. The slot-offset heuristic pulls the integer scalar from the call's second operand; combined with the itab table unstrip already recovers, you get the candidate target set without re-running any analysis. amd64 today; arm64 BLR/BR-through-register support is straightforward to add once we have a test fixture.

### Capabilities

`--capabilities` matches the recovered types, itabs, and function names against a curated rule set and reports what the binary appears to do. First-pass answer to "what is this?":

```
$ unstrip ./sliver-client --capabilities
[crypto]
  TLS client/server
  X.509 / PKI
  RSA, ECDSA, AES
[network]
  HTTP server, HTTP client, gRPC, DNS, raw TCP, raw UDP
[offensive-hint]
  shell command execution
  raw socket / ICMP
  Sliver C2 implant
[process]
  child process spawn
  syscall direct invocation
  signal handling
[serialization]
  JSON encoding
  Protocol Buffers
```

The rules cover HTTP servers and clients, TLS, X.509, RSA/ECDSA/AES, AWS/GCP/Azure SDKs, Kubernetes client, Docker/containerd, SQL/SQLite/Postgres/Redis/MongoDB, JSON/proto/YAML, file and child-process operations, raw sockets, and known offensive frameworks (Sliver). Each match includes up to five evidence strings showing exactly which recovered type or function triggered it.

### Triage hints

`--info` includes a count of stdlib interface implementations the binary ships. Useful for the first-pass question "does this binary speak HTTP, do crypto, write files?"

```
stdlib interface implementations:
  *error            142
  *io.Reader         47
  *io.Writer         38
  *http.Handler      14
  *net.Conn          11
  *crypto.Signer      3
```

(`--fingerprint --behavioral` exists for hashing that counter into a SHA-256; coarser than `--fingerprint` and not garble-stable, included for completeness.)

### Inlined call graph (library API)

For tools that want to consume unstrip's recovery from Rust, the `unstrip::inline` module exposes the inlined call graph the compiler recorded in `FUNCDATA_InlTree`. One `Node` per physical function; one `Edge` per inlined call site the compiler kept after optimization:

```rust
use unstrip::gobin::GoBinary;
use unstrip::inline::{inline_callgraph, EdgeKind, NodeKind};
use unstrip::moduledata::ModuleData;
use unstrip::pclntab::Pclntab;

let bin = GoBinary::open("./target")?;
let md = ModuleData::locate(&bin)?;
let pcln = Pclntab::parse(&bin)?.with_gofunc(md.gofunc);
let graph = inline_callgraph(&bin, &pcln)?;

for e in &graph.edges {
    if matches!(e.kind, EdgeKind::Inlined) {
        let caller = graph.node(e.from).unwrap();
        let callee = graph.node(e.to).unwrap();
        println!("{} -> {} @ 0x{:x}", caller.name, callee.name, e.call_site);
    }
}
```

`NodeKind::Physical` nodes are real pclntab functions with their entry PC as `addr`. `NodeKind::AnonymousInline` nodes carry their parent's PC plus the inline-tree start line, and their `addr` is a synthetic high-bit-tagged ID (`Node::is_anonymous_addr`) so they cannot collide with a real text VA. Anonymous-inline records appear when the compiler kept the inlined call's topology but the binary was built with `-tiny`, `-ldflags=-s -w`, or garble's name obfuscation, all of which zero the callee's `name_off`. The topology is intact in all these cases; only the name is gone.

The `runtime.inlinedCall` record is byte-identical across Go 1.20 through 1.26 (the layout has not drifted since `startLine` was added in Go 1.20). The decoder runs unchanged across that range. Garble's `entryoff` XOR rewrite does not touch the inline-tree FUNCDATA section, so the decoder also runs unchanged on garble-obfuscated binaries; callee names come back obfuscated, not stripped.

For direct (non-inlined) call edges and full-graph traversal, use `--xrefs` (CLI) or `unstrip::xrefs` (library). `EdgeKind::Direct` is reserved on the inlined call graph for a future fold of those edges; today only `EdgeKind::Inlined` is emitted by `inline_callgraph`.

## How it works

Every Go binary built since 1.2 carries a `pclntab`, a self-describing table that maps program counters to function names, source files, and lines. The runtime uses it for stack traces, so `strip` leaves it alone.

`unstrip` does five things, layered:

1. **Find the pclntab.** Look up `.gopclntab` / `__gopclntab` by name; if the section table's stripped or the section was renamed, fall back to a magic-byte scan with a structural sanity filter (so we don't false-positive on random `0xfffffff1` sequences inside other data).
2. **Walk the function table.** For each function entry, resolve the name through the funcnametab, the file through the cutab -> filetab indirection, and the start line through the pcln pc-value table.
3. **Find the moduledata.** Scan `.noptrdata` / `.data` for a pointer to the pclntab, that's the first field of `runtime.firstmoduledata`. Verify by checking that the next five slice headers all point inside the pclntab (the corroborating-references trick that defeats false positives).
4. **Walk the type graph.** Start from `typelinks` (the linker's list of every type the program references), parse each `_type` header, resolve its name through the names blob relative to `moduledata.types`, decode kind-specific extra data (pointer.elem, slice.elem, struct fields, ...), and follow the references to pull in types `typelinks` itself missed. Same trick for `itablinks`.
5. **Parse buildinfo.** Locate the `\xff Go buildinf:` marker, decode the inline string format introduced in Go 1.18, strip the 16-byte sentinel framing, parse the tab-separated `path`/`mod`/`dep`/`build` records.

No relocations. No rewriting. Read-only output. Pipe it into IDA, Ghidra, gdb, or a script.

## Scope

What works:

- Go 1.18 through 1.25
- ELF, Mach-O, and PE containers
- amd64 and arm64
- PIE and non-PIE
- Garble-obfuscated binaries (detected and flagged; structural data still recovered)
- Function names, file paths, line numbers
- Method signatures in Go syntax for every method whose type the Go linker kept
- Full type recovery (names, kinds, sizes, struct fields with offsets and embedded flags)
- Interface to concrete type recovery via itabs
- Module dependency tree, build settings, VCS info
- PC-to-symbol reverse lookup with inlined call stacks
- Direct CALL/BL call-site enumeration on amd64 and arm64; indirect-itab dispatch reporting alongside
- IDA, Ghidra, Binary Ninja, JSON, and human-readable output

Tested against: Linux ELF amd64 and arm64 (PIE and non-PIE), Windows PE amd64, on Go versions in the supported range. Mach-O code paths are exercised by unit tests; the integration suite has no macOS-built fixture yet. Open an issue if it breaks. Garble heuristic detection works and structural recovery survives the default mode (string literals, inline trees, and the funcdata array all stay walkable).

What's out of scope, and where to look instead:

- **Go < 1.18**: pre-1.18 pclntab layouts are unsupported. Use [GoReSym](https://github.com/mandiant/GoReSym) for those.
- **Generic ELF symbol recovery**: not this tool. Use `eu-unstrip` from elfutils.
- **Decompilation**: not this tool. This gives you names; a decompiler gives you code.

Known gaps in v1.0:

- Type recovery surfaces struct package paths as `_pkg_path` (read-and-discarded). Function-type argument types (in/out parameter type pointers) aren't enumerated, you get the count + variadic flag but not the signature types.
- Free top-level function signatures: the binary records signatures for methods (on types reached at runtime) but not for free top-level functions; their signatures are absent without DWARF.

## Roadmap

Work we have not landed in v1.0. Ordered roughly by how often we expect it to come up.

Pre-Go-1.18 pclntab parsing. The Go 1.13 through 1.17 layouts are different enough that the current parser does not even try. GoReSym covers the older corpus; we do not yet. Until we do, that is the gap.

Free-function signature recovery. Methods ship today (we walk `_type.uncommon().methods` and the itab method tables). Free top-level functions do not appear in either table, so their signatures are absent from a stripped binary unless the compiler emitted a `funcType` for reflection or interface storage. Closing that gap means either reading DWARF when present or recognizing reflective uses and back-resolving.

i386 and 32-bit arm. The pclntab layout drifts on 32-bit targets. No demand yet; the day a 32-bit IoT Go binary becomes a real corpus problem, this jumps to the top.

`--symbols-as` for Mach-O. ELF and PE write today. The Mach-O equivalent (`LC_SYMTAB` patching) ships once we have a tested fixture; we do not currently have one we can publish.

Rust binary symbol recovery. Not an unstrip mode. If we build it, it ships as a separate tool. DWARF parsing is its own problem and conflating the two would be a mistake.

## Safety posture

unstrip parses attacker-controlled bytes. We treat that as the default operating assumption.

What we do:

- Every offset read is bounds-checked against the source slice; reads that would run past the end return an error rather than panicking.
- Every length field decoded from the binary is range-capped at a sane ceiling (function counts, type counts, method counts, slice lengths). A binary that claims `nfunc = 0xffffffff` rejects rather than allocating.
- `Result`-returning APIs throughout the library; no `unwrap()` on attacker-controlled paths.
- Per-type recovery is best-effort; one malformed record drops that record, not the whole pass.

What we do not claim:

- We have not run a structured fuzzing campaign against the pclntab and moduledata parsers. The bounds-checking discipline above is the substitute today; a real fuzz runner is post-launch work.
- Garble-obfuscated inputs are detected and flagged, but garble's full transform surface evolves; an obfuscator update that goes further than current behavior may produce output we mis-parse before we patch.
- Mach-O code paths are exercised by unit tests, not by a full integration fixture matrix yet.

If you find a crash, an out-of-bounds read, or any other safety issue: see [SECURITY.md](./SECURITY.md) for the private reporting contact.

## Contributing

PRs welcome. Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening one. Good first issues are tagged [`good first issue`](https://github.com/riven-labs/unstrip/labels/good%20first%20issue). For vulnerability reports see [SECURITY.md](./SECURITY.md).

If `unstrip` can't recover symbols from a binary in the supported matrix, that's a bug. Open an issue with the binary attached if you can share it, or the Go version and target triple if you can't.

## License

Dual-licensed under MIT or Apache-2.0 at your option. Pick whichever fits your downstream needs:

- [`LICENSE-MIT`](./LICENSE-MIT)
- [`LICENSE-APACHE`](./LICENSE-APACHE)

Contact: mohamed@riven-labs.com.

---

A [Riven Labs](https://github.com/riven-labs) project.