计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
re-go【效率与协作】Go 二进制逆向:符号保留、字符串表、goroutine。触发词:Go、golang、Go二进制、goroutine、go build
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
file 输出 Go BuildID / 节表含 .gopclntab)后,利用 Go 默认保留符号的特性还原逻辑;识别 Go 版本与构建信息;分析多 goroutine 程序的控制流;恢复 strip 后的 Go 符号参考 [[re-analyze/platform-tips]]——Go 二进制通常 1MB 起(runtime 占大头),遵循「静态优先(大型样本)」;动态(dlv/gdb 运行样本)默认进沙箱。
apt install golang-go(官方包;Debian 版可能滞后——go version -m 的 buildinfo 指纹需 Go 1.18+,工具太旧可用官方 tarball)dnf install golang;Arch: pacman -S gobrew install go;Windows: choco install golang 或官方安装器go version;go tool buildid sample 能输出 build ID(非 Go 程序会报错)0xjiayu/go_parser——IDA Pro 的 Go 解析脚本,恢复类型与结构体main.*/runtime.* 命名go install github.com/mandiant/GoReSym@latestGoReSym -t -d -p sample > gosyms.json(-t 类型、-d 标准库包、-p 文件路径)goresym_rename.py(GoReSym 官方仓库脚本,Ghidra/IDA 导入,PR #11 已合入)GoReSym -about 输出 about 与 license 信息;输出 JSON 含函数名/地址go install github.com/goretk/redress@latestredress info sample(编译器版本/GoRoot/main 包路径)、redress packages sample --std --vendor(包列表)、redress source sample(源码树投影)、redress types struct sample --methods(类型)redress versiongarble 标识(默认剥离构建信息,非必然残留)②garble 构建默认无 DWARF(readelf -S 无 .debug_*)③函数名 hash 化(main.main → 短 hash 名,pclntab 保留但名字不可读)④-literals 构建字符串乱码garble -literals build(字符串混淆;去调试为默认行为无需标志;另有 -tiny 移除位置信息/剥离 panic 打印与符号名,体积约 -15%)google/bloaty release 或源码构建(cmake+ninja,需 protobuf)dnf install bloaty;Arch: pacman -S bloaty;macOS: brew install bloatybloaty --version;用法: bloaty -d symbols sample按顺序执行,每步记录证据(路径 + sha256,见 [[re-triage]])。
识别 Go 二进制:
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)Go 版本指纹:
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
go version -m 的 module 路径直接给出项目名/依赖(恶意样本常泄露 C2 框架),先记下来.gopclntab 开头 4 字节 0xfffffffb = Go 1.2–1.15、0xfffffffa = 1.16–1.17、0xfffffff0 = 1.18–1.19、0xfffffff1 = 1.20+(头部布局见步骤 6;GoReSym 等工具按这四个值扫描)符号表利用:
GoReSym -p sample > gosyms.json → goresym_rename.py 导入反编译器恢复命名redress types struct sample --methods(或 GoReSym -t 输出类型信息)go version -m / go tool buildid)+ pclntab magic(步骤 2)——不需要符号工具-ldflags "-s -w"):nm sample | grep ' main\.' 列出用户代码函数_rt0_amd64_linux 只是平台桩,真正用户入口是 main.main(由 runtime.main 调用),main.init 是初始化逻辑main.main 读主逻辑,从 main. 命名函数沿调用链展开;符号全保留时无需猜名-literals 字符串在使用点经顶层解码函数惰性解密(仅被触达的字符串才解码)——触发使用后内存取明文,或静态还原解码函数(GoStringUngarbler 可自动化);interface 方法名(导出)garble 不混淆(为保 itab 分派),接口调用可直接按方法名定位实现,被 hash 的是未导出方法名字符串表:
strings -n 6 sample | grep -E '^main\.|/.*\.go' # 函数名与源文件路径
strings sample | grep -E 'go1\.|module |\.go#x27;
strings 直接可见——先收集 main. 命名即调用关系草稿/home/user/c2/agent/main.go)goroutine/调度器理解:
go 关键字在反编译中展开为 runtime.newproc 调用(内部再调 newproc1),其 funcval 参数指向的地址即该 goroutine 的入口函数runtime.goexit;runtime.gopark/runtime.gosched/runtime.schedule 是调度与挂起点——看到它们不要当作主流程runtime.morestack 调用只是栈增长检查,忽略(噪声)go install github.com/go-delve/delve/cmd/dlv@latest,dlv attach <pid> 后 goroutines 列出所有 goroutine、goroutine <n> stack 看栈;[[re-tracing]] 的 strace 观察并发系统调用运行时结构要点(pclntab / goroutine 栈 / buildinfo):
go.→go:);真正更短的头在 ≤1.17(ver116 无 textStart 字段);1.18 的 Go118PCLnTabMagic 变更了 functab 偏移(func 数据入口从地址改偏移),工具按 magic 分派stack{lo, hi} 即栈区间,stackguard0 是栈增长检查阈值——每个函数序言的 runtime.morestack 检查的是它(噪声来源),sched.gobuf 保存挂起时的 sp/pc;dlv 的 goroutines/goroutine <n> stack 输出即来自这些字段,挂起点在 runtime.gopark 恢复.go.buildinfo 内嵌 \xff Go buildinf:(14 字节)+ ptrSize(1) + 字节序标记(1),16 字节对齐;go version -m 失效时按此魔数提取版本与 module 信息(与 go tool 同源,解码失败才是真没有).gopclntab/.go.buildinfo 都在),nm/GoReSym/redress 在 Linux 上可直接分析 PE Go 样本;入口桩符号随平台(Linux _rt0_amd64_linux、Windows _rt0_amd64_windows、macOS _rt0_amd64_darwin),都只是跳到 runtime.mainnm 无输出、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.* 淹没目标;原因——Go 静态链接 runtime(调度/GC/内存管理)+ 每函数 morestack 栈检查,通常占 1-2MB+;对策——符号恢复后先过滤:nm | grep ' main\.'、GoReSym JSON grep '^main\.' 缩小到用户代码;Ghidra 只对目标函数按需反编译;遵循 platform-tips「静态优先(大型样本)」——先静态定位、动态按需补充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 恢复类型结构),对象字段按恢复的类型布局读取go version -m 报错或无输出,无法确定 Go 版本;原因——buildinfo(.go.buildinfo)自 Go 1.18 才默认嵌入,旧版或被裁剪的二进制没有;对策——go tool buildid 一直可用(拿 build ID);旧版看 strings | grep '^go1\.' 的 runtime.buildVersion;redress info 也能按 pclntab 布局推断编译器版本encoding/gob 解码;原因——作者用 gob 编码自定义结构(配置/VM 程序/内嵌对象),LoadProgram 类函数里 gob.NewDecoder + Decode 就是入口;对策——从反编译重建结构体(字段名/字段数从 gob 流里的类型定义提取),用 Go 生成同结构样本对比字节(逐字节比对类型定义段,字段名/类型/数量一目了然),字段名损坏(材料/编码导致缺字节)时 gob 按字段号越界报错,可改本地 gob 源码做宽松解码(跳过字段号检查)逼近数据CGO_ENABLED=0)产物直接 syscall、不经过 libc 符号,无 PT_INTERP 也无 dynamic section,LD_PRELOAD 无从 interpose;对策——先判定产物形态:readelf -l/-d/-Ws、ldd 看 PT_INTERP、DT_NEEDED 与 cgo 痕迹——纯 Go 走 strace/eBPF/ptrace(观察真实 syscall 或 hook 更底层),存在 cgo 或动态 ELF 符号时 LD_PRELOAD 仍可正常 interpose(如 cgo 产物 DT_NEEDED libc.so.6,hook 可命中,见下条「cgo 混合产物」);另可 hook 子进程——Go 的 exec.Command fork 出的 bash/ls 等 C 程序仍走 libc,可被 LD_PRELOAD 拦截aes.NewCipher;对策——跟踪 crypto/aes.NewCipher 的 key 参数来源、回溯拼接点;注意 crypto/cipher.NewGCM(及 NewGCMWithNonceSize/NewGCMWithTagSize/NewGCMWithRandomNonce)的参数是 cipher.Block 不是密钥,若先定位到这些封装,需继续回溯其 Block 到 aes.NewCipher 再取 keynm 里混着一批裸名符号(printf、pthread_*),与 main./runtime. 风格不一致;原因——cgo 把 C 代码/静态库直接链接进 Go 二进制,C 符号不参与 Go 命名体系;对策——按命名风格分片:Go 侧(main./包名.)走本技能,C 侧裸名符号走 [[re-binary-core]]([[re-imports]] 库指纹);cgo 块通常只做薄封装,主逻辑仍在 Go 侧file 报 PE、Ghidra 导入后入口不是 _rt0_amd64_linux,误以为需要 Windows 环境;原因——Go 产物跨平台结构一致,只是容器格式(PE/Mach-O/ELF)不同;对策——nm/GoReSym/redress 照常用(符号表与 pclntab 与平台无关),入口按平台认(Windows 侧 PE 入口指向 _rt0_amd64_windows 桩);动态侧才需要 Windows/Wine([[re-analyze/platform-tips]] 分支)
(来源:reverse-skill field-journal,MIT)