AsherDLL/r2gopclntabParser

GitHub: AsherDLL/r2gopclntabParser

基于 radare2 的 Go 二进制 gopclntab 解析器,可从完全剥离符号的二进制文件中恢复函数名称、源文件路径和行号信息

Stars: 11 | Forks: 0

# r2gopclntabParser 一个基于 radare2 的 Go `gopclntab` 解析器(`r2_gopclntab.py`),用于从 Go 二进制文件中恢复函数符号,包括完全被剥离(stripped)的二进制文件。支持 ELF、Mach-O 和 PE 格式,覆盖 Go 版本 1.2、1.16、1.18 和 1.20+。 每个 Go 二进制文件都包含一个名为 `gopclntab`(程序计数器行表,Program Counter Line Table)的数据区域,Go 运行时将其用于堆栈跟踪、panic 消息、垃圾回收和调试器支持。这些数据在剥离操作(`-ldflags="-s -w"`)后依然存在,使其成为剥离版 Go 二进制文件中最有价值的符号信息来源。 `r2_gopclntab.py` 通过 radare2 读取该区域,解析特定版本的结构,并打印恢复的函数列表(包含地址、源文件和行号),或者将恢复的名称作为函数定义、标志和注释应用回当前打开的 radare2 会话中。 ## 目录 - [前置条件](#prerequisites) - [快速开始](#quick-start) - [CLI 参考](#cli-reference) - [输出模式与示例](#output-modes-and-examples) - [默认模式(Header + Function List)](#default-mode-header--function-list) - [详细模式 (-v)](#verbose-mode--v) - [搜索模式 (-n)](#search-mode--n) - [JSON 输出 (--json)](#json-output---json) - [源文件列表 (--files)](#source-file-listing---files) - [应用模式 (--apply)](#apply-mode---apply) - [结果摘要:Mach-O(剥离后的测试二进制文件)](#results-summary-mach-o-stripped-test-binary) - [PE 测试:Greenblood,一个 Go 勒索软件二进制文件](#pe-test-go-ransomware-binary) - [逆向工程用例](#use-cases-for-reverse-engineering) - [支持的平台与 Go 版本](#supported-platforms-and-go-versions) - [方法论](#methodology) - [节区定位策略](#section-location-strategy) - [textStart 与 .text 节区](#textstart-vs-text-section) - [版本感知的结构解析](#version-aware-struct-parsing) - [PC 数据解码](#pc-data-decoding) - [局限性](#limitations) - [更多文档](#additional-documentation) - [参考](#references) ## 前置条件 | 依赖项 | 最低版本 | |---|---|---| | Python 3 | 3.8+ | | radare2 | 5.0+ (在 6.0.9 上测试) | | r2pipe | 任意 | 不需要其他 Python 包。该脚本仅使用标准库(`struct`、`json`、`argparse`、`os`、`sys`)以及 `r2pipe`。 ## 快速开始 ``` # 列出从 Go binary 恢复的所有函数 python3 r2_gopclntab.py -f ./mybinary -l # 搜索特定函数(子串匹配) python3 r2_gopclntab.py -f ./mybinary -n main.main # 详细 header + 函数列表 python3 r2_gopclntab.py -f ./mybinary -v -l # 将恢复的名称应用到 r2 session python3 r2_gopclntab.py -f ./mybinary --apply # JSON 输出 python3 r2_gopclntab.py -f ./mybinary --json # 在 r2 内部(附加到正在运行的 session) #!pipe python3 r2_gopclntab.py --r2pipe --apply -v ``` ## CLI 参考 ``` usage: r2_gopclntab.py [-h] [-f FILE] [-n FUNCNAME] [-v] [-l] [--apply] [--json] [--files] [--r2pipe] ``` ### 必选项(之一) | 标志 | 描述 | |---|---| | `-f FILE`, `--file FILE` | 要分析的 Go 二进制文件路径。脚本会生成自己的 r2 实例。 | | `--r2pipe` | 附加到已经运行的 r2 会话(在 r2 控制台内部使用)。 | ### 可选项 | 标志 | 描述 | |---|---| | `-l`, `--list` | 打印每个恢复的函数及其地址。 | | `-n NAME`, `--funcname NAME` | 仅打印名称包含 `NAME` 的函数(子字符串匹配)。如果存在精确匹配,其地址将单独打印。 | | `-v`, `--verbose` | 打印进度消息、解析的头字段和内部偏移量。 | | `--apply` | 将恢复的函数名称写入 radare2 会话,作为函数定义(`af+`)、`go.` flagspace 中的标志,以及包含原始 Go 名称和源码位置的注释。 | | `--json` | 将头和完整函数列表以 JSON 格式输出到 stdout。 | | `--files` | 打印从文件表中提取的源文件路径列表。 | | `-h`, `--help` | 显示帮助信息。 | 标志可以自由组合。当没有给出输出标志时,默认行为是打印头信息和完整函数列表。 ## 输出模式与示例 以下所有示例均针对一个剥离后的 Go 1.26 Mach-O arm64 二进制文件运行(使用 `-ldflags="-s -w"` 构建)。测试程序定义了 `main.main`、`main.fibonacci`、`main.helloWorld` 和 `main.addNumbers`。编译器内联了 `helloWorld` 和 `addNumbers`,因此它们不会出现在 gopclntab 中。 ### 默认模式(Header + Function List) 不带标志运行(或仅带 `-f`)会打印解析后的头信息,随后是完整的函数表: ``` $ 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 ``` ### 详细模式 增加节区扫描进度、textStart 解析细节和内部偏移信息: ``` $ 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+ ... ``` ### 搜索模式 通过子字符串匹配过滤函数列表。如果存在精确匹配,其地址将单独打印。 搜索所有包含 `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) ``` 精确匹配搜索: ``` $ 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 ``` 搜索 GC 相关的运行时内部函数: ``` $ 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) ``` 搜索 `fmt.`(标准库打印): ``` $ 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) ... ``` 搜索 `sync.`(并发原语): ``` $ 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 输出 (--json) 用于脚本和管道集成的机器可读输出: ``` $ python3 r2_gopclntab.py -f ./gotest_stripped --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 } ``` ### 源文件列表 提取二进制文件中嵌入的所有源文件路径: ``` $ 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 ``` ### 应用模式 将所有恢复的函数名称写入 radare2 会话。以下展示了在剥离后的二进制文件上的前后对比。 **之前**(r2 对剥离后二进制文件的原生分析,未解析 gopclntab): ``` 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 发现了 1913 个函数但没有命名任何一个(只有匿名的 `fcn.XXXXXXXX` 标签)。搜索 `main.main` 或 `main.fibonacci` 没有任何结果。 **应用 gopclntab 符号:** ``` [*] 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) ``` **之后**(应用了 gopclntab 符号的 r2 会话): ``` 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 ``` `main.main` 处的反汇编现在显示了恢复的名称和源码位置: ``` ;-- 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 ``` `main.fibonacci` 处的反汇编现在显示了恢复的名称和源码位置: ``` ;-- 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 ``` 在 r2 会话中现在可以通过名称进行跳转: ``` go.main.main resolves to: 0x1000a0c20 go.main.fibonacci resolves to: 0x1000a0bb0 ``` ## 结果摘要:Mach-O(剥离后的测试二进制文件) | 指标 | r2 原生(剥离后) | 使用 r2_gopclntab.py 后 | |---|---|---| | 发现的函数 | 1913 (匿名 `fcn.XXXX` 标签) | 2030 (完整的 Go 包限定名) | | 识别的用户函数 | 0 | `main.main`, `main.fibonacci` 及源码 + 行号 | | 恢复的源文件 | 0 | 261 (完整绝对路径) | | 命名符号 | 仅 C 导入桩(`sym.imp.mmap` 等) | 每个 Go 函数都有标签(`go.main.main`, `go.runtime.gcStart` 等) | | 源码注释 | 无 | 反汇编中内联显示 `src: /tmp/gotest/main.go:20` | | 可按名称导航 | 否 | 是 (`s go.main.main`, `afl~runtime.gc`) | ## PE 测试:Greenblood,一个 Go 勒索软件二进制文件 该解析器针对一个真实的 PE 二进制文件 Greenblood(`greenblood_1`)进行了测试,这是一个编译为 PE32+ x86-64 Windows 可执行文件的 Go 勒索软件样本。PE 二进制文件没有专门的 `.gopclntab` 节区,因此这测试了魔术字节扫描回退路径。 ### 检测与头信息 ``` $ 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 ``` 扫描器在 `.rdata` 节区的 `0x568C00` 处找到了 gopclntab。因为这是一个标准 PE(非 PIE),`textStart` 为 `0x401000`(非零),所以直接使用头值进行地址计算。 | 字段 | 值 | |---|---| | 格式 | PE32+ x86-64 | | gopclntab 位置 | `.rdata` at `0x568C00` (通过魔术扫描发现) | | Magic | `0xFFFFFFF1` (Go 1.20+) | | 指针大小 | 8 | | Quantum (minLC) | 1 (x86) | | textStart | `0x401000` (来自头) | | 恢复的函数 | 2596 | | 源文件 | 345 | ### 恢复的恶意软件函数 搜索恶意软件自身的代码(`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) ``` 所有 43 个用户函数均从位于 `/root/victims/ransom/daf/enc.go` 的单一源文件中恢复。函数名立即揭示了勒索软件的能力:密钥管理、机器指纹识别、带路径遍历的文件加密、勒索信放置、禁用恢复、权限检查、基于互斥锁的单实例强制、驱动器枚举和自删除。 ### 非标准库依赖 提取不属于 Go 标准库的源文件: ``` /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/dll_windows.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/registry/key.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/registry/value.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/security_windows.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/str.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/syscall.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/syscall_windows.go /root/go/pkg/mod/golang.org/x/sys@v0.40.0/windows/zsyscall_windows.go /root/victims/ransom/daf/enc.go ``` 唯一的外部依赖是 `golang.org/x/sys@v0.40.0`,用于 Windows 特定的系统调用(注册表访问、安全令牌、DLL 加载)。 ### 加密函数 搜索 `crypto` 发现了 137 个加密相关函数,包括: ``` $ 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) ``` 加密使用概况:AES(CBC 和 CTR 模式)、SHA-256、SHA-512、HMAC、CSHAKE128 和 DRBG(确定性随机比特生成器)。这与以下勒索软件行为一致:生成源自机器指纹的每机器加密密钥,使用 AES-CTR 加密文件,并使用 HMAC 进行完整性校验。 ### 应用模式 ``` $ 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. ``` 所有 2596 个函数成功应用,零跳过。 ## 逆向工程用例 ### 1. 分类与识别 即时判断二进制文件是否为 Go 编写、构建版本以及使用的包。`--files` 输出可揭示 Go 工具链版本(通过文件路径如 `/usr/local/go/1.26.1/...`)以及每个源文件路径,包括第三方库。对于恶意软件,这可以立即揭示样本是否使用了 `crypto/tls`、`net/http`、`os/exec` 或其他感兴趣的包。 ### 2. 剥离二进制文件的符号恢复 这是核心用例。使用 `-ldflags="-s -w"` 剥离的 Go 二进制文件会丢失其符号表,但 gopclntab 幸存下来。此工具恢复每个函数名称,将匿名的 `fcn.1000a0c20` 还原为 `main.main`。这适用于恶意软件样本、CTF 挑战、生产二进制文件和任何剥离的 Go 可执行文件。 ### 3. 导航 Go 运行时 Go 二进制文件嵌入了整个运行时(通常为 1500 到 2000+ 个函数)。没有名称,运行时就是一堵由匿名函数组成的难以穿透的墙。有了名称,您可以立即定位 `runtime.mallocgc`、`runtime.gopanic`、`runtime.newproc`、`runtime.gcStart`,并理解二进制文件在每个调用点的行为。 ### 4. 分离用户代码与运行时 通过搜索 `main.` 或应用程序的包路径,您可以仅从运行时中隔离出用户代码。在上述示例中,过滤 `main.` 立即从 2030 个总函数中揭示了 `main.main` 和 `main.fibonacci`。您也可以按第三方包名搜索(例如 `-n "github.com/user/repo"`)以识别外部依赖。 ### 5. 源码级上下文 每个函数都带有其源文件路径和起始行号。这意味着即使分析剥离的二进制文件,您也可以将反汇编与 Go 标准库源代码(开源)进行交叉引用。知道函数起始于 `runtime/mgc.go` 的第 733 行,让您可以一边阅读原始源码一边查看反汇编。 ### 6. 管道与自动化 `--json` 模式支持脚本化。将输出馈送给 IDA/Ghidra 导入器、差异工具、YARA 规则生成器或任何分析管道。例如,提取所有加密相关函数: ``` python3 r2_gopclntab.py -f sample.exe --json \ | jq '.functions[] | select(.name | contains("crypto"))' ``` ### 7. 交互式 radare2 工作流 在 `apply` 之后,整个 r2 会话即可通过 Go 名称导航。您可以跳转到函数(`s go.main.main`)、搜索函数列表(`afl~runtime.gc`)、检查交叉引用(`axf go.main.fibonacci`),并在反汇编输出(`pd`)中查看内联的源码位置注释。这将 r2 从通用反汇编器转变为 Go 感知的分析环境。 ## 支持的平台与 Go 版本 ### 二进制格式 | 格式 | 节区发现方法 | 已测试 | |---|---|---| | ELF (Linux) | 节区名 `.gopclntab` 或 `.data.rel.ro.gopclntab` | 是 | | Mach-O (macOS) | 节区名 `__gopclntab`(位于 `__TEXT` 段内) | 是 | | PE (Windows) | 魔术字节扫描(无专用节区) | 是 (在 Greenblood 上测试,一个 Go 勒索软件 PE64) | 对于缺少节区头的 PE 二进制文件和激进剥离的 ELF/Mach-O 二进制文件,工具会回退到扫描所有节区,查找 4 字节魔术字后跟验证字节(pad=0, ptrSize in {4,8}, minLC in {1,2,4})。 ### Go 版本 | Magic | Go 版本 | 头中的 `textStart` | `functab.entry` 类型 | `startLine` 字段 | 状态 | |---|---|---|---|---|---| | `0xFFFFFFFB` | 1.2 | 无 | `uintptr` (绝对) | 无 | 支持 | | `0xFFFFFFFA` | 1.16 | 无 | `uintptr` (绝对) | 无 | 支持 | | `0xFFFFFFF0` | 1.18 - 1.19 | 有 | `uint32` (相对) | 无 | 支持 | | `0xFFFFFFF1` | 1.20+ | 有 (可能为 0) | `uint32` (相对) | 有 | 支持 | `0xFFFFFFF1` 魔术字至少被 Go 1.20 到 Go 1.26 使用。 ## 方法论 ### 节区定位策略 解析器使用两阶段方法查找 gopclntab 数据: **阶段 1 (ELF/Mach-O):** 查询 radare2 的节区列表(`iSj`)并查找名为 `.gopclntab`、`.data.rel.ro.gopclntab` 或 `__gopclntab` 的节区。 **阶段 2 (PE/回退):** 如果未找到命名节区,则扫描所有节区查找 4 字节魔术字。通过检查字节 4-7 是否匹配预期模式来验证每个候选:两个零填充字节、有效的指针大小(4 或 8)以及有效的指令 quantum(1、2 或 4)。这消除了来自巧合字节模式的误报。 ### textStart 与 .text 节区 在 Go >= 1.18 中,functab 中的函数入口点存储为相对偏移量。计算绝对虚拟地址需要一个基址: ``` absolute_addr = base + entryoff ``` 基址通过以下逻辑解析: ``` 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 ``` `textStart == 0` 的情况出现在 Go >= 1.22 的 Mach-O 和 PIE 二进制文件中。发生这种情况时,`entryoff` 值相对于 `.text` 节区起始位置,因此解析器查询 r2 获取 `.text` 虚拟地址并将其用作基址。 这已针对 Go 1.26 Mach-O arm64 二进制文件进行了验证: - 头中的 `textStart`:`0x0` - `.text` 节区 vaddr:`0x100001000` - `functab[1].entryoff`:`0x70` - 计算出的地址:`0x100001000 + 0x70 = 0x100001070` - r2 原生分析确认 `internal/abi.BoundsDecode` 位于 `0x100001070` ### 版本感知的结构解析 `_func` 结构布局在 Go 1.18 和 Go 1.20+ 之间有所不同。唯一的变化是在 Go 1.20+ 的偏移量 36 处插入了 4 字节的 `startLine` 字段,这将 `funcID`、`flag` 和 `nfuncdata` 移动了 4 字节: | 字段 | Go 1.18 偏移 | Go 1.20+ 偏移 | |---|---|---| | `entryOff` | 0 | 0 | | `nameOff` | 4 | 4 | | `args` | 8 | 8 | | `cuOffset` | 32 | 32 | | `startLine` | (不存在) | 36 | | `funcID` | 36 | 40 | | `flag` | 37 | 41 | | `nfuncdata` | 39 | 43 | 解析器检查魔术字以确定使用哪种布局。 ### PC 数据解码 源文件索引和行号作为紧凑的“PC 数据程序”存储在 pctab 区域中。每个程序使用可变长度整数和针对有符号值的 zig-zag 编码来编码一系列 `(value_delta, pc_delta)` 对。解析器解码这些数据以解析: - 源文件:`_func.pcfile` -> pctab 程序 -> 文件索引 -> cutab -> filetab -> 文件路径字符串 - 行号:`_func.pcln` -> pctab 程序 -> 行号(Go 1.20+ 需加上 `startLine` 偏移) ## 局限性 1. **内联函数**不会出现在顶级函数表中。它们编码在 `FUNCDATA_InlTree` / `PCDATA_InlTreeIndex` 结构中,此工具目前不解码。在测试二进制文件中,`main.helloWorld` 和 `main.addNumbers` 被编译器内联,因此未出现在输出中。 2. **Go 1.2 支持**是尽力而为的。Go 1.2 格式差异很大(没有单独的 funcnametab,没有 cutab,functab 中是绝对指针),且在实践中很少遇到。 3. **大端**架构原则上受支持(从 radare2 的二进制信息中检测 endian 并用于所有结构读取),但尚未经过测试。 4. `--apply` 模式使用 `af+` 创建函数桩,这可能与 r2 自己的自动分析冲突。在干净的会话上运行(在 `aaa` 之前或代替 `aaa`)在某些情况下可能会产生更好的结果。 ## 更多文档 详细的文档可在 `documentation/` 目录中找到: - [DOCUMENTATION.md](documentation/DOCUMENTATION.md) - 完整的用户文档,包含所有使用模式、输出格式和标志组合。 - [METHODOLOGY.md](documentation/METHODOLOGY.md) - 设计决策和算法:textStart 与 .text 解析、版本感知解析、PE 扫描策略、PC 数据解码、r2 集成细节。 - [GOPCLNTAB_FORMAT.md](documentation/GOPCLNTAB_FORMAT.md) - 跨 Go 版本的 gopclntab 二进制格式:字节级结构布局、内存模型、偏移链、varint 编码、版本差异摘要。 ## 参考 - Go 运行时源码: [go1.20.6/src/runtime/symtab.go#L414](https://github.com/golang/go/blob/go1.20.6/src/runtime/symtab.go#L414) - Go 链接器 (写入格式): [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 符号表设计文档: [docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o](https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub)
标签:DAST, ELF, Golang, gopclntab, Go语言, Mach-O, PE, radare2, 二进制分析, 二进制安全, 云安全监控, 云安全运维, 云资产清单, 去混淆, 可配置连接, 安全编程, 恶意软件分析, 程序分析, 程序破解, 符号恢复, 符号表解析, 脚本, 调试, 逆向工程, 静态分析