re-go

Helps reverse-engineer Go programs to understand how they work and find hidden code.

Installation
Run `npx skills add "https://github.com/dslsdzc/rev-skills" --skill "re-go"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

What this skill does
What it does: - Analyzes programs written in the Go programming language by reading the built-in symbol tables and function lists that Go keeps in its binaries - Recovers function names and structure even from Go programs that have been stripped of symbols - Identifies the Go version used to build the program and finds configuration data - Traces how Go programs with multiple concurrent tasks work When to use it: - You have a Go program and want to understand what it does by reading the code - You found malicious Go software and want to find command and control addresses or configuration - Do not use it for programs written in C, C++, or Rust - Do not use it if the program is packed with a shell before analyzing
SKILL.mdShow the author's original SKILL.md (not in English)
---
name: re-go
description: Go 二进制逆向:符号保留、字符串表、goroutine。触发词:Go、golang、Go二进制、goroutine、go build
---

# Go 二进制逆向(符号 / 字符串表 / goroutine)

## 何时使用 / 何时不用

- 用:确认是 Go 编译的二进制(`file` 输出 Go BuildID / 节表含 `.gopclntab`)后,利用 Go 默认保留符号的特性还原逻辑;识别 Go 版本与构建信息;分析多 goroutine 程序的控制流;恢复 strip 后的 Go 符号
- 用:恶意 Go 样本([[re-malware]] 静态还原——Go 恶意程序多带明文 C2 配置与 buildinfo)
- 不用:非 Go 原生程序(C/C++ 直接 [[re-binary-core]];Rust 走 [[re-rust]])
- 不用:确认带壳先走 [[re-anti-analysis]](Go 二进制被加壳后 pclntab 偏移失效,脱壳后再回本技能)
- 注意:动态步骤默认沙箱([[platform-tips]] 最高原则);Go 二进制体积大,静态优先(大型样本原则)

## 工具准备

参考 [[platform-tips]]——Go 二进制通常 1MB 起(runtime 占大头),遵循「静态优先(大型样本)」;动态(dlv/gdb 运行样本)默认进沙箱。

### Go 工具链(go version / go tool buildid / go version -m)

- Debian/Ubuntu: `apt install golang-go`(官方包;Debian 版可能滞后——`go version -m` 的 buildinfo 指纹需 Go 1.18+,工具太旧可用官方 tarball)
- Fedora: `dnf install golang`;Arch: `pacman -S go`
- macOS: `brew install go`;Windows: `choco install golang` 或官方安装器
- 验证: `go version`;`go tool buildid sample` 能输出 build ID(非 Go 程序会报错)

### Ghidra(内置 Go 支持)与 Go 插件

- Ghidra 11.2+ 内置 Go 分析器(识别 Go 二进制、从 pclntab 恢复符号、runtime 类型标注),装法见 [[re-ghidra]]
- IDA 侧插件(可选): GitHub `0xjiayu/go_parser`——IDA Pro 的 Go 解析脚本,恢复类型与结构体
- 验证: Ghidra 导入后函数树出现 `main.*`/`runtime.*` 命名

### GoReSym(strip 后符号恢复,可选但关键)

- 无发行版官方包 → 官方安装(需 Go 工具链): `go install github.com/mandiant/GoReSym@latest`
- 用法: `GoReSym -t -d -p sample > gosyms.json`(-t 类型、-d 标准库包、-p 文件路径)
- 导入反编译器: `goresym_rename.py`(GoReSym 官方仓库脚本,Ghidra/IDA 导入,PR #11 已合入)
- 验证: `GoReSym -about` 输出 about 与 license 信息;输出 JSON 含函数名/地址

### redress(Go 二进制元数据,可选)

- 无发行版官方包 → 官方安装: `go install github.com/goretk/redress@latest`
- 用法: `redress info sample`(编译器版本/GoRoot/main 包路径)、`redress packages sample --std --vendor`(包列表)、`redress source sample`(源码树投影)、`redress types struct sample --methods`(类型)
- 验证: `redress version`

### garble —— Go 混淆识别(恶意样本常见,无独立工具)

- garble 是社区维护的第三方 Go 混淆器(构建期变换,非运行时壳;作者 mvdan,非 Go 官方项目)——Go 恶意样本越来越常见,需先识别再分析
- **识别特征**:①字符串/节表偶见 `garble` 标识(默认剥离构建信息,非必然残留)②garble 构建默认无 DWARF(`readelf -S` 无 `.debug_*`)③函数名 hash 化(`main.main` → 短 hash 名,pclntab 保留但名字不可读)④`-literals` 构建字符串乱码
- **常见构建标志**:`garble -literals build`(字符串混淆;去调试为默认行为无需标志;另有 `-tiny` 移除位置信息/剥离 panic 打印与符号名,体积约 -15%)
- 分析要点见步骤 3(hash 函数名 / 字符串惰性解密 / interface 方法名保留)

### bloaty(体积分析,可选)

- Debian/Ubuntu: 无官方包 → GitHub `google/bloaty` release 或源码构建(cmake+ninja,需 protobuf)
- Fedora: `dnf install bloaty`;Arch: `pacman -S bloaty`;macOS: `brew install bloaty`
- Windows: 官方 release(或 WSL 内 Linux 版)
- 验证: `bloaty --version`;用法: `bloaty -d symbols sample`

## 操作步骤

按顺序执行,每步记录证据(路径 + sha256,见 [[re-triage]])。

1. **识别 Go 二进制**:
   ```sh
   file sample                     # 含 "Go BuildID=..." 即 Go(通常静态链接)
   readelf -S sample | grep -i go  # .go.buildinfo / .gopclntab / .gosymtab
   nm sample | grep -E 'main\.main|runtime\.main'   # 符号存在性
   ```
   - `.gopclntab` 是函数表(含函数名/地址/行号),节名即判定依据
   - 确认符号保留状态:`nm` 为空 → 坑 1(strip)

2. **Go 版本指纹**:
   ```sh
   go tool buildid sample                # build ID(go build 唯一标识,一直可用)
   go version -m sample                  # Go 1.18+:Go 版本 + module 路径 + 依赖列表
   strings sample | grep -E '^go1\.[0-9]+'   # 旧版兜底:runtime.buildVersion
   ```
   - 版本决定 pclntab 布局与 GoReSym/redress 的解析方式;无 buildinfo → 坑 5
   - `go version -m` 的 module 路径直接给出项目名/依赖(恶意样本常泄露 C2 框架),先记下来
   - pclntab magic 直接判版本段:`.gopclntab` 开头 4 字节 `0xfffffffb` = Go 1.2–1.15、`0xfffffffa` = 1.16–1.17、`0xfffffff0` = 1.18–1.19、`0xfffffff1` = 1.20+(头部布局见步骤 6;GoReSym 等工具按这四个值扫描)

3. **符号表利用**:
   - **工具选择(目标 → 工具)**:
     - 恢复函数名(strip 后符号)→ **GoReSym**:`GoReSym -p sample > gosyms.json` → `goresym_rename.py` 导入反编译器恢复命名
     - 类型恢复(结构体字段/接口/方法签名)→ **redress**:`redress types struct sample --methods`(或 GoReSym `-t` 输出类型信息)
     - 版本识别 → buildinfo(`go version -m` / `go tool buildid`)+ pclntab magic(步骤 2)——不需要符号工具
   - Go 默认保留符号(除非 `-ldflags "-s -w"`):`nm sample | grep ' main\.'` 列出用户代码函数
   - Ghidra 导入后函数树按包分组;注意 ELF 入口点 `_rt0_amd64_linux` 只是平台桩,真正用户入口是 `main.main`(由 `runtime.main` 调用),`main.init` 是初始化逻辑
   - 反编译 `main.main` 读主逻辑,从 `main.` 命名函数沿调用链展开;符号全保留时无需猜名
   - **garble 混淆样本**(恶意 Go 样本常见,见工具准备):pclntab 仍可解析但函数名 hash 化——GoReSym 列出 hash 名,结构/调用链仍可用;`-literals` 字符串在使用点经顶层解码函数**惰性解密**(仅被触达的字符串才解码)——触发使用后内存取明文,或静态还原解码函数(GoStringUngarbler 可自动化);**interface 方法名(导出)garble 不混淆**(为保 itab 分派),接口调用可直接按方法名定位实现,被 hash 的是未导出方法名

4. **字符串表**:
   ```sh
   strings -n 6 sample | grep -E '^main\.|/.*\.go'   # 函数名与源文件路径
   strings sample | grep -E 'go1\.|module |\.go$'
   ```
   - pclntab 内含函数名与源文件路径,`strings` 直接可见——先收集 `main.` 命名即调用关系草稿
   - 源路径揭示项目布局(如 `/home/user/c2/agent/main.go`)
   - Ghidra 中字符串交叉引用(Ctrl+Shift+F)回溯使用点(对应 [[re-binary-core]] R2 方法)

5. **goroutine/调度器理解**:
   - `go` 关键字在反编译中展开为 `runtime.newproc` 调用(内部再调 `newproc1`),其 funcval 参数指向的地址即该 goroutine 的入口函数
   - goroutine 入口函数返回后走 `runtime.goexit`;`runtime.gopark`/`runtime.gosched`/`runtime.schedule` 是调度与挂起点——看到它们不要当作主流程
   - 每个函数序言的 `runtime.morestack` 调用只是栈增长检查,忽略(噪声)
   - 动态(沙箱内): dlv 对 Go 最友好——`go install github.com/go-delve/delve/cmd/dlv@latest`,`dlv attach <pid>` 后 `goroutines` 列出所有 goroutine、`goroutine <n> stack` 看栈;[[re-tracing]] 的 strace 观察并发系统调用

6. **运行时结构要点(pclntab / goroutine 栈 / buildinfo)**:
   - pclntab 头(1.20+ 布局):magic(4) + pad1 + pad2 + minLC + ptrSize + nfunc(8) + nfiles(8) + 保留(8) + funcnameOffset(8) + cuOffset(8) + filetabOffset(8) + pctabOffset(8) + pclnOffset(8);funcnameOffset 指向函数名字符串池,pctab 是 PC→行号表——1.18/1.19 与 1.20+ 的 pcHeader 同为 72 字节(debug/gosym ver118/ver120 同一解析分支;textStart 经 relocation 写入真实地址、Go 1.26 起恒为 0(不再存储);1.20 起符号名加冒号 `go.`→`go:`);真正更短的头在 ≤1.17(ver116 无 textStart 字段);1.18 的 Go118PCLnTabMagic 变更了 functab 偏移(func 数据入口从地址改偏移),工具按 magic 分派
   - goroutine 栈:g 结构(runtime 私有)首字段 `stack{lo, hi}` 即栈区间,`stackguard0` 是栈增长检查阈值——每个函数序言的 `runtime.morestack` 检查的是它(噪声来源),`sched.gobuf` 保存挂起时的 sp/pc;dlv 的 `goroutines`/`goroutine <n> stack` 输出即来自这些字段,挂起点在 `runtime.gopark` 恢复
   - buildinfo 手工解析:`.go.buildinfo` 内嵌 `\xff Go buildinf:`(14 字节)+ ptrSize(1) + 字节序标记(1),16 字节对齐;`go version -m` 失效时按此魔数提取版本与 module 信息(与 go tool 同源,解码失败才是真没有)
   - 平台差异:Windows Go 产物是 PE 但节名与符号体系相同(`.gopclntab`/`.go.buildinfo` 都在),nm/GoReSym/redress 在 Linux 上可直接分析 PE Go 样本;入口桩符号随平台(Linux `_rt0_amd64_linux`、Windows `_rt0_amd64_windows`、macOS `_rt0_amd64_darwin`),都只是跳到 runtime.main

## 跨域联合

- [[re-binary-core]]:本技能由 core 网关引用——初勘 [[re-triage]]、格式 [[re-format-elf]]/[[re-format-pe]]、反编译底座 [[re-ghidra]]/[[re-ida]]、库指纹 [[re-imports]]
- [[re-ghidra]] / [[re-ida]]:Go 符号/类型恢复产物(GoReSym JSON、redress types)导入反编译器
- 动态侧:沙箱内 [[re-gdb]]/[[re-tracing]];Go 程序调试首选 dlv(goroutine 感知)
- 恶意场景:Go 恶意样本(C2、loader)静态按本技能还原 → 行为分析转 [[re-malware]]
- [[platform-tips]] 相关分支:静态优先(大型样本)、动态默认沙箱、跨平台 Go 样本(Windows PE Go 在 Linux 侧走 Wine 分支)

## 常见坑与陷阱

- **strip 后符号全丢,恢复难**:现象——`nm` 无输出、Ghidra 函数全 `sub_*`、找不到 `main.main`;原因——`-ldflags "-s -w"` 或发布流程 strip 掉 symtab/DWARF(pclntab 数据仍在,只是没有符号引用它);对策——GoReSym/redress 从 `.gopclntab`(节定位失败则按 pclntab magic 字节扫描)恢复函数名/地址 → `goresym_rename.py` 导入 Ghidra/IDA;garble 等混淆会混淆函数名(pclntab 结构保留、函数边界仍可恢复,但名字无意义)→ 需 GoResolver/GoStringUngarbler 配合,必要时退化为 strings + 动态行为分析
- **二进制巨大,runtime 占大头**:现象——Ghidra 导入几万函数、自动分析卡顿、`runtime.*` 淹没目标;原因——Go 静态链接 runtime(调度/GC/内存管理)+ 每函数 morestack 栈检查,通常占 1-2MB+;对策——符号恢复后先过滤:`nm | grep ' main\.'`、GoReSym JSON `grep '^main\.'` 缩小到用户代码;Ghidra 只对目标函数按需反编译;遵循 platform-tips「静态优先(大型样本)」——先静态定位、动态按需补充
- **goroutine 入口识别**:现象——反编译看不到线性主流程,只见 `runtime.gopark`/`schedule` 调度调用;原因——用户代码以多个 goroutine 并发(`go` 关键字/`runtime.newproc` 派生),无单一主路径,执行顺序由调度器决定;对策——在 `runtime.newproc` 调用点读 funcval 参数得入口地址;入口返回后走 `runtime.goexit`;动态用 dlv(沙箱内)`goroutines` 列出并看各 goroutine 栈
- **逃逸分析影响栈布局**:现象——源码里的局部变量不在栈上,变成 `runtime.newobject` + 堆指针;原因——Go 编译器逃逸分析把逃逸对象移到堆(GC 管理),栈帧不含预期局部变量;对策——别按 C 风格从栈帧找局部变量;跟踪 newobject 返回的堆指针与类型(GoReSym `-t`/redress `types` 恢复类型结构),对象字段按恢复的类型布局读取
- **buildinfo 版本指纹有门槛**:现象——`go version -m` 报错或无输出,无法确定 Go 版本;原因——buildinfo(`.go.buildinfo`)自 Go 1.18 才默认嵌入,旧版或被裁剪的二进制没有;对策——`go tool buildid` 一直可用(拿 build ID);旧版看 `strings | grep '^go1\.'` 的 `runtime.buildVersion`;`redress info` 也能按 pclntab 布局推断编译器版本
- **自定义结构序列化数据(gob)**:现象——程序数据段是"字节流"但反编译显示用 `encoding/gob` 解码;原因——作者用 gob 编码自定义结构(配置/VM 程序/内嵌对象),`LoadProgram` 类函数里 `gob.NewDecoder` + `Decode` 就是入口;对策——从反编译重建结构体(字段名/字段数从 gob 流里的类型定义提取),**用 Go 生成同结构样本对比字节**(逐字节比对类型定义段,字段名/类型/数量一目了然),字段名损坏(材料/编码导致缺字节)时 gob 按字段号越界报错,可改本地 gob 源码做宽松解码(跳过字段号检查)逼近数据
- **LD_PRELOAD 对 Go 程序无效**:现象——hook libc 函数(execve/openat)拦截不到 Go 程序的调用;原因——Go 直接 syscall(不经过 libc 符号),LD_PRELOAD 只劫持 libc 动态符号;对策——hook **子进程**(bash 等 C 程序调用的 libc 符号)或 hook 更底层(seccomp/strace 观察真实 syscall);Go 程序执行的 `exec.Command` 会 fork 子进程,子进程(bash/ls)仍走 libc,可被 LD_PRELOAD 拦截
- **运行时行为重放(伪服务端)**:现象——Go 恶意程序逻辑复杂但需要网络交互(IRC/HTTP 服务端)才能触发;原因——程序是客户端(信标/C2 bot),离线静态分析看不到完整行为;对策——**伪造服务端重放**:DNS hook(getaddrinfo 劫持域名→127.0.0.1)+ 假 IRC/HTTP 服务器(按 pcap 提取的真实交互序列重放),让程序自己跑完逻辑链;注意握手时序(等 bot JOIN 完成再发消息)与消息格式(bot 按名字/频道校验消息)
- **Garble 混淆的 Go 二进制**:现象——stripped + 随机函数名 + 字符串全空;原因——Garble 同时混淆函数名与字符串;对策——GoReSym 恢复 pclntab(函数边界不受名字混淆影响)→ GoResolver CFG 签名恢复标准库名 → GoStringUngarbler 批量解密字符串 → 从解密串找 C2/密钥
- **Go 加密密钥定位**:现象——找不到 AES 密钥;原因——密钥运行时从多个常量拼接;对策——跟踪 `crypto/aes.NewCipher`(或 `crypto/cipher.NewGCM`)第一个参数来源,回溯拼接点
- **Go 接口调用看不懂**:现象——反编译的 interface 调用是间接跳转;原因——Go interface 经 itab 分派;对策——定位 itab 表手动标注接口类型
- **符号可用但签名缺失**:现象——函数名有、参数/类型没有;原因——新版本 Go 产物不带完整符号信息;对策——接受函数名可用、签名靠其他证据(配置反序列化类型/调用点)重建
- **字符串噪声大**:现象——恢复的字符串混入大量标准库常量;原因——Go 标准库字符串常量;对策——按包级别过滤后再筛业务字符串
- **源码重建原则**:现象——逐行还原不现实;原因——产物无源码对应;对策——按包重建可读代码、保留逻辑而非逐行一致(逻辑优先)
- **cgo 混合产物,C 部分无 Go 命名**:现象——`nm` 里混着一批裸名符号(`printf`、`pthread_*`),与 `main.`/`runtime.` 风格不一致;原因——cgo 把 C 代码/静态库直接链接进 Go 二进制,C 符号不参与 Go 命名体系;对策——按命名风格分片:Go 侧(`main.`/包名.)走本技能,C 侧裸名符号走 [[re-binary-core]]([[re-imports]] 库指纹);cgo 块通常只做薄封装,主逻辑仍在 Go 侧
- **Windows PE Go 样本在 Linux 上直接静态分析**:现象——`file` 报 PE、Ghidra 导入后入口不是 `_rt0_amd64_linux`,误以为需要 Windows 环境;原因——Go 产物跨平台结构一致,只是容器格式(PE/Mach-O/ELF)不同;对策——nm/GoReSym/redress 照常用(符号表与 pclntab 与平台无关),入口按平台认(Windows 侧 PE 入口指向 `_rt0_amd64_windows` 桩);动态侧才需要 Windows/Wine([[platform-tips]] 分支)
(来源:reverse-skill field-journal,MIT)

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us