r2gopclntabParser: A radare2-based Go gopclntab parser for recovering function symbols from Go binaries, including fully stripped ones.
quality 7/10 · good
0 net
# r2gopclntabParser
A radare2-based Go `gopclntab` parser (`r2_gopclntab.py`) for recovering
function symbols from Go binaries, including fully stripped ones. Supports ELF,
Mach-O, and PE binaries across Go versions 1.2, 1.16, 1.18, and 1.20+.
Every Go binary embeds a data region called `gopclntab` (Program Counter Line
Table) that the Go runtime uses for stack traces, panic messages, garbage
collection, and debugger support. This data survives stripping
(`-ldflags="-s -w"`), making it the single most valuable source of symbol
information in stripped Go binaries.
`r2_gopclntab.py` reads this region through radare2, parses the
version-specific structures, and either prints the recovered function list
(with addresses, source files, and line numbers) or applies the recovered names
back into the open radare2 session as function definitions, flags, and
comments.
---
## Table of Contents
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [CLI Reference](#cli-reference)
- [Output Modes and Examples](#output-modes-and-examples)
- [Default Mode (Header + Function List)](#default-mode-header--function-list)
- [Verbose Mode (-v)](#verbose-mode--v)
- [Search Mode (-n)](#search-mode--n)
- [JSON Output (--json)](#json-output---json)
- [Source File Listing (--files)](#source-file-listing---files)
- [Apply Mode (--apply)](#apply-mode---apply)
- [Results Summary: Mach-O (Stripped Test Binary)](#results-summary-mach-o-stripped-test-binary)
- [PE Test: Greenblood, a Go Ransomware Binary](#pe-test-go-ransomware-binary)
- [Use Cases for Reverse Engineering](#use-cases-for-reverse-engineering)
- [Supported Platforms and Go Versions](#supported-platforms-and-go-versions)
- [Methodology](#methodology)
- [Section Location Strategy](#section-location-strategy)
- [textStart vs .text Section](#textstart-vs-text-section)
- [Version-Aware Struct Parsing](#version-aware-struct-parsing)
- [PC Data Decoding](#pc-data-decoding)
- [Limitations](#limitations)
- [Additional Documentation](#additional-documentation)
- [References](#references)
---
## Prerequisites
| Dependency | Minimum Version |
|---|---|---|
| Python 3 | 3.8+ |
| radare2 | 5.0+ (tested on 6.0.9) |
| r2pipe | any |
No other Python packages are required. The script uses only the standard
library (`struct`, `json`, `argparse`, `os`, `sys`) plus `r2pipe`.
---
## Quick Start
```bash
# List all functions recovered from a Go binary
python3 r2_gopclntab.py -f ./mybinary -l
# Search for a specific function (substring match)
python3 r2_gopclntab.py -f ./mybinary -n main.main
# Verbose header + function list
python3 r2_gopclntab.py -f ./mybinary -v -l
# Apply recovered names into an r2 session
python3 r2_gopclntab.py -f ./mybinary --apply
# JSON output
python3 r2_gopclntab.py -f ./mybinary --json
# From within r2 (attach to running session)
#!pipe python3 r2_gopclntab.py --r2pipe --apply -v
```
---
## CLI Reference
```
usage: r2_gopclntab.py [-h] [-f FILE] [-n FUNCNAME] [-v] [-l]
[--apply] [--json] [--files] [--r2pipe]
```
### Required (one of)
| Flag | Description |
|---|---|
| `-f FILE`, `--file FILE` | Path to the Go binary to analyze. The script spawns its own r2 instance. |
| `--r2pipe` | Attach to an already-running r2 session (for use inside the r2 console). |
### Optional
| Flag | Description |
|---|---|
| `-l`, `--list` | Print every recovered function with its address. |
| `-n NAME`, `--funcname NAME` | Print only functions whose name contains `NAME` (substring match). If an exact match exists, its address is printed separately. |
| `-v`, `--verbose` | Print progress messages, parsed header fields, and internal offsets. |
| `--apply` | Write recovered function names into the radare2 session as function definitions (`af+`), flags in the `go.` flagspace, and comments with the original Go name and source location. |
| `--json` | Output the header and full function list as JSON to stdout. |
| `--files` | Print the list of source file paths extracted from the file table. |
| `-h`, `--help` | Show the help message. |
Flags can be combined freely. When no output flag is given, the default
behavior is to print the header and the full function list.
---
## Output Modes and Examples
All examples below were run against a stripped Go 1.26 Mach-O arm64 binary
(built with `-ldflags="-s -w"`). The test program defines `main.main`,
`main.fibonacci`, `main.helloWorld`, and `main.addNumbers`. The compiler
inlined `helloWorld` and `addNumbers`, so they do not appear in the gopclntab.
### Default Mode (Header + Function List)
Running with no flags (or just `-f`) prints the parsed header followed by the
full function table:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped
```
```
============================================================
Go pclntab Header
============================================================
Magic: 0xFFFFFFF1
Go version: 1.20+
Pointer size: 8
Min LC (quantum):4
Num functions: 2030
Num files: 261
funcnameOffset: 0x48
cuOffset: 0x14D20
filetabOffset: 0x158D8
pctabOffset: 0x19A98
pclnOffset: 0x55E40
============================================================
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x100001000 go:buildid
0x100001070 internal/abi.BoundsDecode (/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/bounds.go:86)
0x100001150 internal/abi.NoEscape (/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/escape.go:19)
0x100001160 internal/abi.Kind.String (/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/type.go:143)
0x1000011E0 internal/abi.TypeOf (/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/type.go:181)
0x1000011F0 internal/abi.(*Type).ExportedMethods (/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/type.go:453)
...
0x1000A0BB0 main.fibonacci (/tmp/gotest/main.go:13)
0x1000A0C20 main.main (/tmp/gotest/main.go:20)
0x1000A0D20 go:textfipsstart
0x1000A0D30 go:textfipsend
[+] 2030 function(s) shown
```
### Verbose Mode (-v)
Adds section scanning progress, textStart resolution details, and internal
offset information:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -v
```
```
[*] Running radare2 analysis...
[*] Binary format: mach0, endian: little, arch: arm, bits: 64
[*] Scanning binary for gopclntab magic bytes...
[*] Scanning section '0.__TEXT.__text' (0x100001000, 0x9FD44)...
[*] Scanning section '1.__TEXT.__symbol_stub1' (0x1000A0D60, 0x2B8)...
[*] Scanning section '2.__TEXT.__rodata' (0x1000A1020, 0xACC2)...
[*] Scanning section '3.__TEXT.__gopclntab' (0x1000ABCE8, 0xA48AE)...
[*] Found magic at vaddr=0x1000ABCE8
[*] Parsed header: PcHeader(magic=0xFFFFFFF1, version=1.20+, ptrSize=8, minLC=4,
nfunc=2030, nfiles=261, textStart=0x0)
[*] textStart is 0, using .text section vaddr: 0x100001000
[*] Parsed 2030 functions
============================================================
Go pclntab Header
============================================================
Magic: 0xFFFFFFF1
Go version: 1.20+
...
```
### Search Mode (-n)
Filters the function list by substring match. If an exact match exists, its
address is printed separately.
Substring search for all functions containing `main.`:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "main."
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x100041920 runtime.main.func2 (/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/proc.go:207)
0x10006CE30 runtime.main.func1 (/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/proc.go:174)
0x1000A0BB0 main.fibonacci (/tmp/gotest/main.go:13)
0x1000A0C20 main.main (/tmp/gotest/main.go:20)
[+] 4 function(s) shown (filtered from 2030 total)
```
Exact match search:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "main.fibonacci"
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x1000A0BB0 main.fibonacci (/tmp/gotest/main.go:13)
[+] 1 function(s) shown (filtered from 2030 total)
[+] Exact match: main.fibonacci @ 0x1000A0BB0
```
Searching for GC-related runtime internals:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "runtime.gc"
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x10001F310 runtime.gcinit (/opt/homebrew/.../src/runtime/mgc.go:179)
0x10001F3C0 runtime.gcenable (/opt/homebrew/.../src/runtime/mgc.go:211)
0x10001F730 runtime.gcStart (/opt/homebrew/.../src/runtime/mgc.go:733)
0x10001FFE0 runtime.gcMarkDone (/opt/homebrew/.../src/runtime/mgc.go:1015)
0x100020A50 runtime.gcMarkTermination (/opt/homebrew/.../src/runtime/mgc.go:1344)
0x100021C10 runtime.gcBgMarkWorker (/opt/homebrew/.../src/runtime/mgc.go:1750)
0x1000223D0 runtime.gcMark (/opt/homebrew/.../src/runtime/mgc.go:1956)
0x1000227A0 runtime.gcSweep (/opt/homebrew/.../src/runtime/mgc.go:2049)
...
0x100076C60 runtime.gcWriteBarrier1 (/opt/homebrew/.../src/runtime/asm_arm64.s:1533)
[+] 73 function(s) shown (filtered from 2030 total)
```
Searching for `fmt.` (standard library printing):
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "fmt."
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x100098AA0 fmt.(*fmt).writePadding (/opt/homebrew/.../src/fmt/format.go:66)
0x100098BF0 fmt.(*fmt).pad (/opt/homebrew/.../src/fmt/format.go:93)
0x100099590 fmt.(*fmt).fmtInteger (/opt/homebrew/.../src/fmt/format.go:197)
0x10009ADF0 fmt.Fprintf (/opt/homebrew/.../src/fmt/print.go:222)
0x10009AED0 fmt.Fprintln (/opt/homebrew/.../src/fmt/print.go:303)
0x10009D3F0 fmt.(*pp).printArg (/opt/homebrew/.../src/fmt/print.go:721)
0x10009D950 fmt.(*pp).printValue (/opt/homebrew/.../src/fmt/print.go:797)
0x10009FA60 fmt.(*pp).doPrintf (/opt/homebrew/.../src/fmt/print.go:1018)
...
```
Searching for `sync.` (concurrency primitives):
```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "sync."
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x10006FFE0 sync.runtime_registerPoolCleanup (/opt/homebrew/.../src/runtime/mgc.go:2150)
0x100070BA0 sync.fatal (/opt/homebrew/.../src/runtime/panic.go:1160)
0x1000714E0 sync.runtime_procPin (/opt/homebrew/.../src/runtime/proc.go:7912)
0x10007B290 internal/sync.(*Mutex).lockSlow (/opt/homebrew/.../src/internal/sync/mutex.go:95)
0x10007B570 internal/sync.(*Mutex).Unlock (/opt/homebrew/.../src/internal/sync/mutex.go:187)
...
```
### JSON Output (--json)
Machine-readable output for scripting and pipeline integration:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped --json
```
```json
{
"header": {
"magic": "0xFFFFFFF1",
"version": "1.20+",
"ptrSize": 8,
"minLC": 4,
"nfunc": 2030,
"nfiles": 261,
"textStart": "0x0"
},
"functions": [
{
"name": "go:buildid",
"addr": "0x100001000",
"args": 0,
"source_file": "",
"start_line": 0
},
{
"name": "internal/abi.BoundsDecode",
"addr": "0x100001070",
"args": 8,
"source_file": "/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/bounds.go",
"start_line": 86
},
{
"name": "main.fibonacci",
"addr": "0x1000A0BB0",
"args": 0,
"source_file": "/tmp/gotest/main.go",
"start_line": 13
},
{
"name": "main.main",
"addr": "0x1000A0C20",
"args": 0,
"source_file": "/tmp/gotest/main.go",
"start_line": 20
}
],
"num_source_files": 261
}
```
### Source File Listing (--files)
Extracts all source file paths embedded in the binary:
```
$ python3 r2_gopclntab.py -f ./gotest_stripped --files
```
```
Source files (261):
------------------------------------------------------------
/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/bounds.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/escape.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/abi/type.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/cpu/cpu.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/internal/cpu/cpu_arm64.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/proc.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/mgc.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/malloc.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/runtime/panic.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/fmt/print.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/fmt/format.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/reflect/value.go
/opt/homebrew/Cellar/go/1.26.1/libexec/src/reflect/type.go
/tmp/gotest/main.go
...
... and 61 more
```
### Apply Mode (--apply)
Writes all recovered function names into the radare2 session. The following
shows a before/after comparison on the stripped binary.
**BEFORE** (r2 native analysis on stripped binary, no gopclntab parsing):
```
Functions found by r2 natively: 1913
Disassembly at 0x1000a0c20 (main.main, unnamed):
; CODE XREF from fcn.1000a0c20 @ 0x1000a0d14(r)
24: fcn.1000a0c20 (int64_t arg1);
0x1000a0c20 900b40f9 ldr x16, [x28, 0x10]
0x1000a0c24 ff6330eb cmp sp, x16
0x1000a0c28 29070054 b.ls 0x1000a0d0c
Disassembly at 0x1000a0bb0 (main.fibonacci, unnamed):
112: fcn.1000a0bb0 (signed int64_t arg1, int64_t arg_8h);
0x1000a0bb0 900b40f9 ldr x16, [x28, 0x10]
0x1000a0bb4 ff6330eb cmp sp, x16
0x1000a0bb8 a9020054 b.ls 0x1000a0c0c
```
r2 found 1913 functions but named none of them (only anonymous `fcn.XXXXXXXX`
labels). Searching for `main.main` or `main.fibonacci` returns nothing.
**Applying gopclntab symbols:**
```
[*] Binary format: mach0, endian: little, arch: arm, bits: 64
[*] Found magic at vaddr=0x1000ABCE8
[*] Parsed header: PcHeader(magic=0xFFFFFFF1, version=1.20+, ...)
[*] textStart is 0, using .text section vaddr: 0x100001000
[*] Parsed 2030 functions
[+] Applied 2030 function names to radare2 (0 skipped)
```
**AFTER** (r2 session with gopclntab symbols applied):
```
r2 function list matching "main" (after --apply):
0x100041510 0 0 runtime.main
0x100041920 0 0 runtime.main.func2
0x10006ce30 0 0 runtime.main.func1
Flags in go.* flagspace (last 20):
0x10009fa60 1 go.fmt._ptr_pp_.doPrintf
0x1000a0930 1 go.fmt._ptr_pp_.doPrintln
0x1000a0bb0 1 go.main.fibonacci
0x1000a0c20 1 go.main.main
0x1000a0d20 1 go.go:textfipsstart
0x1000a0d30 1 go.go:textfipsend
```
Disassembly at `main.main` now shows the recovered name and source location:
```
;-- go.main.main:
24: fcn.1000a0c20 (int64_t arg1);
0x1000a0c20 900b40f9 ldr x16, [x28, 0x10] ; " src: /tmp/gotest/main.go:20"
0x1000a0c24 ff6330eb cmp sp, x16
0x1000a0c28 29070054 b.ls 0x1000a0d0c
```
Disassembly at `main.fibonacci` now shows the recovered name and source location:
```
;-- go.main.fibonacci:
112: fcn.1000a0bb0 (signed int64_t arg1, int64_t arg_8h);
0x1000a0bb0 900b40f9 ldr x16, [x28, 0x10] ; " src: /tmp/gotest/main.go:13"
0x1000a0bb4 ff6330eb cmp sp, x16
0x1000a0bb8 a9020054 b.ls 0x1000a0c0c
```
Seeking by name now works in the r2 session:
```
go.main.main resolves to: 0x1000a0c20
go.main.fibonacci resolves to: 0x1000a0bb0
```
---
## Results Summary: Mach-O (Stripped Test Binary)
| Metric | r2 Native (stripped) | After r2_gopclntab.py |
|---|---|---|
| Functions found | 1913 (anonymous `fcn.XXXX` labels) | 2030 (full Go package-qualified names) |
| User functions identified | 0 | `main.main`, `main.fibonacci` with source + line |
| Source files recovered | 0 | 261 (full absolute paths) |
| Named symbols | Only C import stubs (`sym.imp.mmap`, etc.) | Every Go function labeled (`go.main.main`, `go.runtime.gcStart`, etc.) |
| Source comments | None | Inline `src: /tmp/gotest/main.go:20` in disassembly |
| Navigable by name | No | Yes (`s go.main.main`, `afl~runtime.gc`) |
---
## PE Test: Greenblood, a Go Ransomware Binary
The parser was tested against a real-world PE binary, Greenblood (`greenblood_1`), a Go
ransomware sample compiled as a PE32+ x86-64 Windows executable. PE binaries
do not have a dedicated `.gopclntab` section, so this exercises the magic-byte
scanning fallback path.
### Detection and Header
```
$ python3 r2_gopclntab.py -f ./greenblood_1 -v
```
```
[*] Binary format: pe, endian: little, arch: x86, bits: 64
[*] Scanning binary for gopclntab magic bytes...
[*] Scanning section '.text' (0x401000, 0xF4000)...
[*] Scanning section '.rdata' (0x4F5000, 0x127000)...
[*] Found magic at vaddr=0x568C00
[*] Parsed header: PcHeader(magic=0xFFFFFFF1, version=1.20+, ptrSize=8,
minLC=1, nfunc=2596, nfiles=345, textStart=0x401000)
[*] textStart from header: 0x401000
[*] Parsed 2596 functions
```
The scanner found the gopclntab inside the `.rdata` section at `0x568C00`.
Because this is a standard PE (not PIE), `textStart` is `0x401000` (non-zero),
so the header value is used directly for address computation.
| Field | Value |
|---|---|
| Format | PE32+ x86-64 |
| gopclntab location | `.rdata` at `0x568C00` (found by magic scan) |
| Magic | `0xFFFFFFF1` (Go 1.20+) |
| Pointer size | 8 |
| Quantum (minLC) | 1 (x86) |
| textStart | `0x401000` (from header) |
| Functions recovered | 2596 |
| Source files | 345 |
### Recovered Malware Functions
Searching for the malware's own code (`main.`):
```
$ python3 r2_gopclntab.py -f ./greenblood_1 -n "main."
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x4D91E0 main.init (:1)
0x4D9200 main.map.init.0 (/root/victims/ransom/daf/enc.go:59)
0x4D92C0 main.map.init.1 (/root/victims/ransom/daf/enc.go:126)
0x4D95C0 main.NewKeyManager (/root/victims/ransom/daf/enc.go:146)
0x4D97E0 main.getMachineFingerprint (/root/victims/ransom/daf/enc.go:173)
0x4DA2C0 main.getBIOSUUID (/root/victims/ransom/daf/enc.go:249)
0x4DA3C0 main.NewEncryptionEngine (/root/victims/ransom/daf/enc.go:287)
0x4DA560 main.(*EncryptionEngine).fileWorker (/root/victims/ransom/daf/enc.go:303)
0x4DA660 main.(*EncryptionEngine).processFile (/root/victims/ransom/daf/enc.go:319)
0x4DA740 main.(*EncryptionEngine).encryptFile (/root/victims/ransom/daf/enc.go:334)
0x4DB2A0 main.(*EncryptionEngine).EncryptPath (/root/victims/ransom/daf/enc.go:446)
0x4DB620 main.(*EncryptionEngine).shouldSkipDirectory (/root/victims/ransom/daf/enc.go:495)
0x4DB7C0 main.(*EncryptionEngine).shouldEncryptFile (/root/victims/ransom/daf/enc.go:522)
0x4DB9A0 main.(*EncryptionEngine).placeRansomNote (/root/victims/ransom/daf/enc.go:560)
0x4DBB60 main.(*EncryptionEngine).recordSuccess (/root/victims/ransom/daf/enc.go:636)
0x4DBFA0 main.(*EncryptionEngine).Wait (/root/victims/ransom/daf/enc.go:664)
0x4DC3C0 main.formatBytes (/root/victims/ransom/daf/enc.go:689)
0x4DC500 main.disableRecovery (/root/victims/ransom/daf/enc.go:706)
0x4DC720 main.isAdmin (/root/victims/ransom/daf/enc.go:732)
0x4DC8C0 main.main (/root/victims/ransom/daf/enc.go:760)
0x4DD260 main.getLogicalDrives (/root/victims/ransom/daf/enc.go:863)
0x4DD4A0 main.isAlreadyRunning (/root/victims/ransom/daf/enc.go:892)
0x4DD660 main.getDesktopPath (/root/victims/ransom/daf/enc.go:911)
0x4DD780 main.removeExecutable (/root/victims/ransom/daf/enc.go:932)
...
[+] 43 function(s) shown (filtered from 2596 total)
```
All 43 user functions were recovered from a single source file at
`/root/victims/ransom/daf/enc.go`. The function names immediately reveal the
ransomware's capabilities: key management, machine fingerprinting, file
encryption with path traversal, ransom note placement, recovery disabling,
privilege checking, mutex-based single-instance enforcement, drive enumeration,
and self-deletion.
### Non-Standard-Library Dependencies
Extracting source files that are not part of the Go standard library:
```
/root/go/pkg/mod/golang.org/x/[email protected]/windows/dll_windows.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/registry/key.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/registry/value.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/security_windows.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/str.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/syscall.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/syscall_windows.go
/root/go/pkg/mod/golang.org/x/[email protected]/windows/zsyscall_windows.go
/root/victims/ransom/daf/enc.go
```
The only external dependency is `golang.org/x/[email protected]` for Windows-specific
syscalls (registry access, security tokens, DLL loading).
### Crypto Functions
Searching for `crypto` reveals 137 crypto-related functions, including:
```
$ python3 r2_gopclntab.py -f ./greenblood_1 -n "crypto"
```
```
ADDRESS FUNCTION NAME
----------------------------------------------------------------------
0x4AEC00 crypto/cipher.NewCTR (/usr/local/go/src/crypto/cipher/ctr.go:41)
0x4AF520 crypto/cipher.StreamWriter.Write (/usr/local/go/src/crypto/cipher/io.go:36)
0x4AF840 crypto/aes.NewCipher (/usr/local/go/src/crypto/aes/aes.go:36)
0x4C3760 crypto/rand.(*reader).Read (/usr/local/go/src/crypto/rand/rand.go:45)
0x4DE6C0 crypto/internal/fips140/sha256.New (.../sha256/sha256.go:138)
0x4E3EC0 crypto/internal/fips140/sha3.NewCShake128 (.../sha3/shake.go:134)
0x4EED60 crypto/internal/fips140/hmac.New (.../hmac/hmac.go:131)
0x4EF800 crypto/internal/fips140/aes.newBlock (.../aes/aes_asm.go:59)
0x4F0800 crypto/internal/fips140/aes.(*CBCEncrypter).CryptBlocks (.../aes/cbc.go:26)
0x4F0FE0 crypto/internal/fips140/aes.(*CTR).XORKeyStream (.../aes/ctr.go:41)
...
[+] 137 function(s) shown (filtered from 2596 total)
```
The crypto usage profile: AES (CBC and CTR modes), SHA-256, SHA-512, HMAC,
CSHAKE128, and DRBG (deterministic random bit generator). This is consistent
with ransomware that generates per-machine encryption keys derived from a
machine fingerprint, encrypts files with AES-CTR, and uses HMAC for integrity.
### Apply Mode
```
$ python3 r2_gopclntab.py -f ./greenblood_1 --apply
[+] Applied 2596 function names to radare2 (0 skipped)
[+] Function names applied. Use 'afl' in r2 to see them.
```
All 2596 functions applied successfully with zero skipped.
---
## Use Cases for Reverse Engineering
### 1. Triage and Identification
Instantly determine if a binary is Go, what version it was built with, and
what packages it uses. The `--files` output reveals the Go toolchain version
(from file paths like `/usr/local/go/1.26.1/...`) and every source file path,
including third-party libraries. For malware, this instantly reveals whether
the sample uses `crypto/tls`, `net/http`, `os/exec`, or other packages of
interest.
### 2. Symbol Recovery on Stripped Binaries
The core use case. Go binaries stripped with `-ldflags="-s -w"` lose their
symbol table, but gopclntab survives. This tool restores every function name,
turning anonymous `fcn.1000a0c20` back into `main.main`. This applies to
malware samples, CTF challenges, production binaries, and any stripped Go
executable.
### 3. Navigating the Go Runtime
Go binaries embed the entire runtime (typically 1500 to 2000+ functions).
Without names, the runtime is an impenetrable wall of anonymous functions.
With names, you can instantly locate `runtime.mallocgc`, `runtime.gopanic`,
`runtime.newproc`, `runtime.gcStart`, and understand what the binary is doing
at each call site.
### 4. Separating User Code from Runtime
By searching for `main.` or the application's package path, you can isolate
just the user's code from the runtime. In the examples above, filtering for
`main.` immediately reveals `main.main` and `main.fibonacci` among 2030
total functions. You can also search by third-party package names
(e.g., `-n "github.com/user/repo"`) to identify external dependencies.
### 5. Source-Level Context
Each function comes with its source file path and start line number. This
means you can cross-reference the disassembly against the Go standard library
source code (which is open), even when analyzing a stripped binary. Knowing
that a function starts at line 733 of `runtime/mgc.go` lets you read the
original source alongside the disassembly.
### 6. Pipeline and Automation
The `--json` mode enables scripting. Feed the output into IDA/Ghidra
importers, diffing tools, YARA rule generators, or any analysis pipeline.
For example, to extract all crypto-related functions:
```bash
python3 r2_gopclntab.py -f sample.exe --json \
| jq '.functions[] | select(.name | contains("crypto"))'
```
### 7. Interactive radare2 Workflow
After `--apply`, the entire r2 session becomes navigable by Go name. You can
seek to functions (`s go.main.main`), search the function list
(`afl~runtime.gc`), inspect cross-references (`axf go.main.fibonacci`), and
see source location comments inline in disassembly output (`pd`). This
transforms r2 from a generic disassembler into a Go-aware analysis
environment.
---
## Supported Platforms and Go Versions
### Binary Formats
| Format | Section Discovery Method | Tested |
|---|---|---|
| ELF (Linux) | Section name `.gopclntab` or `.data.rel.ro.gopclntab` | Yes |
| Mach-O (macOS) | Section name `__gopclntab` (inside `__TEXT` segment) | Yes |
| PE (Windows) | Magic byte scan (no dedicated section) | Yes (tested on Greenblood, a Go ransomware PE64) |
For PE binaries and aggressively stripped ELF/Mach-O binaries that lack the
section header, the tool falls back to scanning all sections for the 4-byte
magic followed by validation bytes (pad=0, ptrSize in {4,8}, minLC in
{1,2,4}).
### Go Versions
| Magic | Go Version | `textStart` in header | `functab.entry` type | `startLine` field | Status |
|---|---|---|---|---|---|
| `0xFFFFFFFB` | 1.2 | No | `uintptr` (absolute) | No | Supported |
| `0xFFFFFFFA` | 1.16 | No | `uintptr` (absolute) | No | Supported |
| `0xFFFFFFF0` | 1.18 - 1.19 | Yes | `uint32` (relative) | No | Supported |
| `0xFFFFFFF1` | 1.20+ | Yes (may be 0) | `uint32` (relative) | Yes | Supported |
The `0xFFFFFFF1` magic is used by Go 1.20 through at least Go 1.26.
---
## Methodology
### Section Location Strategy
The parser uses a two-phase approach to find the gopclntab data:
**Phase 1 (ELF/Mach-O):** Query radare2's section list (`iSj`) and look for
sections named `.gopclntab`, `.data.rel.ro.gopclntab`, or `__gopclntab`.
**Phase 2 (PE/fallback):** If no named section is found, scan all sections
for the 4-byte magic bytes. Each candidate is validated by checking that
bytes 4-7 match the expected pattern: two zero pad bytes, a valid pointer
size (4 or 8), and a valid instruction quantum (1, 2, or 4). This
eliminates false positives from coincidental byte patterns.
### textStart vs .text Section
In Go >= 1.18, function entry points in the functab are stored as relative
offsets. Computing the absolute virtual address requires a base:
```
absolute_addr = base + entryoff
```
The base is resolved with the following logic:
```
Is magic 0xFFFFFFFB (Go 1.2)?
YES -> base = 0 (entries are absolute addresses)
NO -> Is magic 0xFFFFFFFA (Go 1.16)?
YES -> base = .text section vaddr (entries are absolute)
NO -> (Go 1.18 / 1.20+)
Is header.textStart != 0?
YES -> base = header.textStart
NO -> base = .text section vaddr
```
The `textStart == 0` case occurs in Go >= 1.22 for Mach-O and PIE binaries.
When this happens, the `entryoff` values are relative to the `.text` section
start, so the parser queries r2 for the `.text` virtual address and uses that
as the base.
This was verified against a Go 1.26 Mach-O arm64 binary:
- `textStart` in the header: `0x0`
- `.text` section vaddr: `0x100001000`
- `functab[1].entryoff`: `0x70`
- Computed address: `0x100001000 + 0x70 = 0x100001070`
- r2 native analysis confirmed `internal/abi.BoundsDecode` at `0x100001070`
### Version-Aware Struct Parsing
The `_func` struct layout differs between Go 1.18 and Go 1.20+. The only
change is the insertion of a 4-byte `startLine` field at offset 36 in Go
1.20+, which shifts `funcID`, `flag`, and `nfuncdata` by 4 bytes:
| Field | Go 1.18 offset | Go 1.20+ offset |
|---|---|---|
| `entryOff` | 0 | 0 |
| `nameOff` | 4 | 4 |
| `args` | 8 | 8 |
| `cuOffset` | 32 | 32 |
| `startLine` | (absent) | 36 |
| `funcID` | 36 | 40 |
| `flag` | 37 | 41 |
| `nfuncdata` | 39 | 43 |
The parser checks the magic number to determine which layout to use.
### PC Data Decoding
Source file indices and line numbers are stored as compact "PC data programs"
in the pctab region. Each program encodes a sequence of
`(value_delta, pc_delta)` pairs using variable-length integers with zig-zag
encoding for signed values. The parser decodes these to resolve:
- Source file: `_func.pcfile` -> pctab program -> file index -> cutab ->
filetab -> file path string
- Line number: `_func.pcln` -> pctab program -> line number (plus
`startLine` offset for Go 1.20+)
---
## Limitations
1. **Inlined functions** do not appear in the top-level function table. They
are encoded in `FUNCDATA_InlTree` / `PCDATA_InlTreeIndex` structures,
which this tool does not currently decode. In the test binary,
`main.helloWorld` and `main.addNumbers` were inlined by the compiler and
therefore do not appear in the output.
2. **Go 1.2 support** is best-effort. The Go 1.2 format is substantially
different (no separate funcnametab, no cutab, absolute pointers in functab)
and is rarely encountered in practice.
3. **Big-endian** architectures are handled in principle (the endian is
detected from radare2's binary info and used for all struct reads) but
have not been tested.
4. The `--apply` mode creates function stubs with `af+` which may conflict
with r2's own auto-analysis. Running it on a clean session (before or
instead of `aaa`) may give better results in some cases.
---
## Additional Documentation
Detailed technical documentation is available in the `documentation/` directory:
- [DOCUMENTATION.md](documentation/DOCUMENTATION.md) - Full user documentation
with all usage modes, output formats, and flag combinations.
- [METHODOLOGY.md](documentation/METHODOLOGY.md) - Design decisions and
algorithms: textStart vs .text resolution, version-aware parsing, PE
scanning strategy, PC data decoding, r2 integration details.
- [GOPCLNTAB_FORMAT.md](documentation/GOPCLNTAB_FORMAT.md) - The gopclntab
binary format across Go versions: byte-level struct layouts, memory model,
offset chains, varint encoding, version differences summary.
---
## References
- Go runtime source (pcHeader): [go1.20.6/src/runtime/symtab.go#L414](https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L414)
- Go linker (writes the format): [go1.20.6/src/cmd/link/internal/ld/pcln.go](https://github.com/golang/go/blob/go1.20.6/src/cmd/link/internal/ld/pcln.go)
- Mandiant - Golang Internals Symbol Recovery: [mandiant.com/resources/blog/golang-internals-symbol-recovery](https://www.mandiant.com/resources/blog/golang-internals-symbol-recovery)
- Go 1.2 symbol table design document: [docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o](https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub)