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, 二进制分析, 二进制安全, 云安全监控, 云安全运维, 云资产清单, 去混淆, 可配置连接, 安全编程, 恶意软件分析, 程序分析, 程序破解, 符号恢复, 符号表解析, 脚本, 调试, 逆向工程, 静态分析