注意:该功能已直接集成到 radare2 核心中,作为 gopc analysis plugin
一个基于 radare2 的 Go gopclntab 解析器,用于从 Go 二进制文件中恢复函数符号,包括完全剥离符号的二进制文件。支持 ELF、Mach-O 和 PE 二进制文件,涵盖 Go 1.2、1.16、1.18 及 1.20+ 版本。
每个 Go 1.2+ 二进制文件都嵌入了一个名为 gopclntab(程序计数器行表)的数据区域,Go 运行时利用它进行堆栈跟踪、恐慌消息、垃圾回收和调试器支持。它是 Go 二进制文件中最有价值的符号信息源之一。
r2gopclntabParser 通过 radare2 读取该区域,解析版本特定的结构,然后打印恢复的函数列表(包含地址、源文件和行号),或将恢复的函数名称作为函数定义、标志和注释应用回打开的 radare2 会话中。
| 依赖项 | 最低版本 |
|---|---|
| Python 3 | 3.8+ |
| radare2 | 5.0+(已在 6.0.9 上测试) |
| r2pipe | 任意 |
不需要其他 Python 包。脚本仅使用标准库(struct、json、argparse、os、sys)加上 r2pipe。
python3 r2_gopclntab.py -f ./mybinary -l
python3 r2_gopclntab.py -f ./mybinary -n main.main
python3 r2_gopclntab.py -f ./mybinary -v -l
python3 r2_gopclntab.py -f ./mybinary --apply
python3 r2_gopclntab.py -f ./mybinary --json
#!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控制台内部)。 |
标志可以自由组合。当没有给出输出标志时,默认行为是打印头部和完整函数列表。
以下所有示例均针对一个剥离后的Go 1.26 Mach-O arm64二进制文件运行(使用-ldflags="-s -w"构建)。测试程序定义了main.main、main.fibonacci、main.helloWorld和main.addNumbers。编译器内联了helloWorld和addNumbers,因此它们不会出现在gopclntab中。
运行时不带任何标志(或仅使用-f)会打印解析后的头部,后跟完整的函数表:```
$ python3 r2_gopclntab.py -f ./gotest_stripped
在开始之前,您只需要设置一个配置文件。最快和最简单的方法是运行并按照设置向导的提示进行操作,使用以下命令:
```console
$ tools-for-github setup
您将被询问一系列问题,配置文件将生成在 ~/.tools-for-github.toml。
如果您想手动创建并配置文件,请创建 ~/.tools-for-github.toml 并填入以下内容:
# [github] 部分是可选的,如果省略,将使用默认值。
[github]
base_url = "https://api.github.com" # 如果您使用的是 GitHub Enterprise Server,请更改此设置。
api_token = "" # 默认值是 GITHUB_TOKEN 环境变量的值。
此配置文件位于 $HOME/.tools-for-github.toml。
还有其他可用的选项,但这些是必需的。有关所有配置选项的完整参考,请参阅文档。
接下来,您还需要一个 GitHub 令牌,可以设置为 GITHUB_TOKEN 环境变量,或在 ~/.tools-for-github.toml 中配置。创建令牌的文档可在此处找到。还要记得在需要时为您的令牌启用 SSO。
现在,您可以像这样使用任何脚本:
$ tools-for-github <script> <arguments>
例如,要使用 sync-github-all 脚本,可以运行以下命令:
$ tools-for-github sync-github-all --help
要列出所有可用脚本,请运行以下命令:
$ tools-for-github
初始设置后,使用 update 脚本将 tools-for-github 更新到最新版本:
$ tools-for-github update
github-tools 或早期版本的 tools-for-github 升级如果您之前使用过 github-tools 或 v3.0.0 之前的 tools-for-github 版本,配置文件已发生变更。配置文件的位置已从 ~/.github-tools.toml 移至 ~/.tools-for-github.toml,并且格式也发生了变化。幸运的是,设置向导可以帮助您迁移现有的配置。只需运行:
$ tools-for-github setup
向导将检测您现有的配置,并询问是否要迁移到新格式。
旧的配置格式如下所示:
github_api_token = ""
github_base_url = "https://api.github.com"
而新的配置格式如下所示:
[github]
base_url = "https://api.github.com"
api_token = ""
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
### 详细模式 (-v)
添加了部分扫描进度、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
Magic: 0xFFFFFFF1 Go version: 1.20+ ...
### 搜索模式 (-n)
按子串匹配过滤函数列表。如果存在精确匹配,则会单独打印其地址。
搜索所有包含 `main.` 的子串函数:```
$ python3 r2_gopclntab.py -f ./gotest_stripped -n "main."
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"
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"
块特定规则:
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."
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."
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
关键观察
初始访问技术
"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 }
### 源文件列表 (--files)
提取嵌入在二进制文件中的所有源文件路径:```
$ python3 r2_gopclntab.py -f ./gotest_stripped --files
internal/kit/block.go:将阻断规则应用于 BlockedItem 记录列表。它读取 YAML 文件,为每条规则构建默认值映射,然后对每个 BlockedItem 评估所有规则,合并规则特定的默认值,并可选地将详细说明写入文件。
internal/kit/block.go:读取 YAML 配置(可以是 URL 或文件路径),对其进行分词处理,并提供对规则、规则文件和类别的结构化访问。它使用 internal/kit/grammar.y 中定义的语法和解析器。
internal/kit/block.go:这可能是一个更全面的示例,利用了完整的数据管道。它定义了 BlockedItem 结构体来表示域名/注册表对及其应用的规则,RuleDetails 来捕获每条规则的元数据,以及 EvaluateAllRules 和 AnalyzeItem 等函数。```
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)
将所有恢复的函数名称写入 radare2 会话。以下展示了剥离二进制文件前后的对比。
**BEFORE**(对剥离二进制文件进行 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)
**之后** (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
位于 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
--protocol <protocol> - 指定用于端口扫描的协议。可以是 tcp、udp 或 both(同时使用 tcp 和 udp)。默认为 tcp。目前仅通过 --stdin 选项可用。--exclude-timeout <exclude-timeout> - 在指定的超时时间(例如 "20s"、"10m")后,从 -exclude-hosts 标准输入中排除主机。需要 --stdin。--exclude-hosts <exclude-hosts> - 指定一个文件,从中读取要从标准输入中排除的额外主机。需要 --stdin。--max-kb-per-second <max-kb-per-second> - 限制地址生成所使用的最大带宽,单位为 KByte/s。除非使用指定的输出文件,否则可通过 禁用此选项。扫描器在 `.rdata` 节的 `0x568C00` 处发现了 gopclntab。
由于这是一个标准 PE(非 PIE),`textStart` 为 `0x401000`(非零),
因此头部值直接用于地址计算。
| 字段 | 值 |
|---|---|
| 格式 | PE32+ x86-64 |
| gopclntab 位置 | `.rdata` 在 `0x568C00`(通过魔数扫描发现) |
| 魔数 | `0xFFFFFFF1` (Go 1.20+) |
| 指针大小 | 8 |
| 量子(minLC) | 1 (x86) |
| textStart | `0x401000`(来自头部) |
| 恢复的函数 | 2596 |
| 源文件 | 345 |
### 恢复的恶意软件函数
搜索恶意软件自身的代码(`main.`):```
$ python3 r2_gopclntab.py -f ./greenblood_1 -n "main."
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/[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
唯一的外部依赖是用于 Windows 特定系统调用的 golang.org/x/[email protected](注册表访问、安全令牌、DLL 加载)。
搜索 crypto 共发现 137 个加密相关函数,包括:```
$ python3 r2_gopclntab.py -f ./greenblood_1 -n "crypto"
```markdown
### 安装指南
1. **克隆仓库:**
```bash
git clone https://github.com/your-username/your-repo.git
cd your-repo
安装依赖: 确保已安装 Python 3.8+ 和必要的包:
pip install -r requirements.txt
配置:
编辑 config.yaml 文件,填入你的 API 密钥和目标设置。
运行主脚本:
python main.py -t example.com
选项:
-t, --target:指定目标域名或 IP 地址。-o, --output:输出目录(默认:./results)。-v, --verbose:启用详细输出。[+] 目标:example.com
[+] 开始扫描...
[+] 发现开放端口:80, 443, 8080
[+] 服务识别完成。
[+] 漏洞扫描完成。
[+] 报告已保存至:./results/example.com_report.html
欢迎提交拉取请求。对于重大更改,请先开 issue 讨论你想要改变的内容。
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规则生成器或任何分析流水线。例如,提取所有与加密相关的函数:```bash
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) | 魔数字节扫描 (无专用节) | 是 (在 Go 勒索软件 PE64 Greenblood 上测试) |
对于 PE 二进制文件和缺乏节头的严重剥离的 ELF/Mach-O 二进制文件,该工具回退到扫描所有节以查找 4 字节魔数,后跟验证字节 (pad=0, ptrSize in {4,8}, minLC in {1,2,4})。
### Go 版本
| 魔数 | 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) 和有效的指令量子 (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 |
解析器会检查 magic 数字以确定使用哪种布局。
### PC 数据解码
源文件索引和行号以紧凑的“PC 数据程序”形式存储在 pctab 区域中。每个程序使用变长整数编码(有符号值采用锯齿编码)对 `(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 的二进制信息中检测并用于所有结构体读取),但未经测试。
4. `--apply` 模式会使用 `af+` 创建函数存根,这可能与 r2 自身的自动分析冲突。在某些情况下,在干净的会话上(在 `aaa` 之前或代替 `aaa`)运行它可能会得到更好的结果。
---
## 附加文档
详细的技术文档位于 `documentation/` 目录中:
- [DOCUMENTATION.md](https://github.com/asherdll/r2gopclntabparser/blob/main/documentation/DOCUMENTATION.md) - 完整的用户文档,包含所有使用模式、输出格式和标志组合。
- [METHODOLOGY.md](https://github.com/asherdll/r2gopclntabparser/blob/main/documentation/METHODOLOGY.md) - 设计决策和算法:textStart 与 .text 解析、版本感知解析、PE 扫描策略、PC 数据解码、r2 集成细节。
- [GOPCLNTAB_FORMAT.md](https://github.com/asherdll/r2gopclntabparser/blob/main/documentation/GOPCLNTAB_FORMAT.md) - 各 Go 版本的 gopclntab 二进制格式:字节级结构布局、内存模型、偏移链、变长整数编码、版本差异总结。
---
## 参考资料
- Go 运行时源代码 (pcHeader):[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)
| 标志 | 描述 |
|---|
-l, --list | 打印每个恢复的函数及其地址。 |
-n NAME, --funcname NAME | 仅打印名称包含NAME(子字符串匹配)的函数。如果存在完全匹配,则单独打印其地址。 |
-v, --verbose | 打印进度信息、解析的头部字段和内部偏移量。 |
--apply | 将恢复的函数名称写入radare2会话,包括函数定义(af+)、go.标志空间中的标志,以及带有原始Go名称和源代码位置的注释。 |
--json | 将头部和完整函数列表以JSON格式输出到stdout。 |
--files | 打印从文件表中提取的源文件路径列表。 |
-h, --help | 显示帮助信息。 |
| 攻击技术 | 漏洞编号 | 受影响软件 | 观察到的勒索软件 |
|---|
| 外部远程服务 | CVE-2024-21683 | Confluence Data Center and Server | Akira, Fog, Rhysida |
| 外部远程服务 | CVE-2023-22527 | Confluence Data Center and Server | Akira |
| 外部远程服务 | CVE-2023-34362 | MOVEit Transfer | Clop |
| 外部远程服务 | CVE-2024-27198 / CVE-2024-27199 | JetBrains TeamCity | Akira, Fog, Rhysida, Cactus |
| 信任关系 | N/A | 托管服务提供商 | Akira, BlackSuit, LockBit, Play, RansomHub |
| 有效账户 | N/A | VPN, 远程桌面 | 多种 |
| { | |||
| "header": { |
--disable-stdin-exclude-cdn - 跳过对 CDN 的完整端口扫描(仅检查 80、443 端口)。-ec - 跳过对 CDN 的完整端口扫描(仅检查 80、443 端口)。```
[] 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