r2gopclntabParser: A radare2-based Go gopclntab parser for recovering function symbols from Go binaries, including fully stripped ones.

github.com · asherdl02 · 8 days ago · tool
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)