$npx -y skills add killvxk/cybersecurity-skills-zh --skill analyzing-golang-malware-with-ghidra使用 Ghidra 及专用脚本对 Go 编译的恶意软件进行逆向工程,包括函数恢复、字符串提取和去符号表 Go 二进制文件的类型重建。
| 1 | # 使用 Ghidra 分析 Golang 恶意软件 |
| 2 | |
| 3 | ## 概述 |
| 4 | |
| 5 | Go(Golang)因其跨平台编译能力、生成自包含二进制文件的静态链接以及逆向工程的复杂性,成为恶意软件作者的热门语言。Go 二进制文件包含整个运行时、标准库和所有依赖项的静态链接,产生大型二进制文件(通常 5-15MB),包含数千个函数。Ghidra 在处理 Go 特有的字符串格式(非空终止符)、去符号的函数名和 goroutine 并发模式时存在困难。Volexity 开发的 GoResolver 等专用工具使用控制流图相似性在去符号或混淆的 Go 二进制文件中自动去混淆并恢复函数名。 |
| 6 | |
| 7 | ## 前置条件 |
| 8 | |
| 9 | - Ghidra 11.0+,配合 JDK 17+ |
| 10 | - GoResolver 插件(用于函数名恢复) |
| 11 | - Go 逆向工程工具包(go-re.tk) |
| 12 | - Python 3.9+(用于辅助脚本) |
| 13 | - 了解 Go 运行时内部机制(goroutine、channel、interface) |
| 14 | - 熟悉 Go 二进制文件结构(pclntab、moduledata、itab) |
| 15 | |
| 16 | ## 核心概念 |
| 17 | |
| 18 | ### Go 二进制文件结构 |
| 19 | |
| 20 | Go 二进制文件在 `pclntab`(PC 行表)结构中嵌入了丰富的元数据,将程序计数器映射到函数名、源文件和行号。即使去符号表的二进制文件也保留此元数据。`moduledata` 结构包含指向类型信息、itab(接口表)和 pclntab 本身的指针。Go 字符串以指针-长度对的形式存储,而非以空字符结尾的 C 字符串。 |
| 21 | |
| 22 | ### 去符号表二进制文件中的函数恢复 |
| 23 | |
| 24 | 尽管去除了符号表,Go 二进制文件仍在 pclntab 中保留函数名。然而,garble 等混淆工具会将函数重命名为随机字符串。GoResolver 通过计算混淆函数的控制流图签名,并与已知 Go 标准库和第三方包函数的数据库进行匹配来解决此问题。 |
| 25 | |
| 26 | ### 包/依赖项提取 |
| 27 | |
| 28 | Go 的依赖管理将模块路径和版本字符串嵌入二进制文件。提取这些信息可揭示恶意软件的第三方依赖(HTTP 库、加密包、C2 框架),在不进行完整逆向工程的情况下了解其能力。 |
| 29 | |
| 30 | ## 实操步骤 |
| 31 | |
| 32 | ### 步骤 1:初始二进制分析 |
| 33 | |
| 34 | ```python |
| 35 | #!/usr/bin/env python3 |
| 36 | """分析 Go 二进制文件元数据用于恶意软件分析。""" |
| 37 | import struct |
| 38 | import sys |
| 39 | import re |
| 40 | |
| 41 | |
| 42 | def find_go_build_info(data): |
| 43 | """从二进制文件提取 Go 构建信息。""" |
| 44 | # Go buildinfo 魔数:\xff Go buildinf: |
| 45 | magic = b'\xff Go buildinf:' |
| 46 | offset = data.find(magic) |
| 47 | if offset == -1: |
| 48 | return None |
| 49 | |
| 50 | print(f"[+] Go 构建信息位于偏移 0x{offset:x}") |
| 51 | |
| 52 | # 提取附近的 Go 版本字符串 |
| 53 | go_version = re.search(rb'go\d+\.\d+(?:\.\d+)?', data[offset:offset+256]) |
| 54 | if go_version: |
| 55 | print(f" Go 版本:{go_version.group().decode()}") |
| 56 | |
| 57 | return offset |
| 58 | |
| 59 | |
| 60 | def find_pclntab(data): |
| 61 | """定位 pclntab(PC 行表)结构。""" |
| 62 | # pclntab 魔数字节随 Go 版本而变化 |
| 63 | magics = { |
| 64 | b'\xfb\xff\xff\xff\x00\x00': "Go 1.2-1.15", |
| 65 | b'\xfa\xff\xff\xff\x00\x00': "Go 1.16-1.17", |
| 66 | b'\xf1\xff\xff\xff\x00\x00': "Go 1.18-1.19", |
| 67 | b'\xf0\xff\xff\xff\x00\x00': "Go 1.20+", |
| 68 | } |
| 69 | |
| 70 | for magic, version in magics.items(): |
| 71 | offset = data.find(magic) |
| 72 | if offset != -1: |
| 73 | print(f"[+] pclntab 位于 0x{offset:x}({version})") |
| 74 | return offset, version |
| 75 | |
| 76 | return None, None |
| 77 | |
| 78 | |
| 79 | def extract_function_names(data, pclntab_offset): |
| 80 | """从 pclntab 提取函数名。""" |
| 81 | if pclntab_offset is None: |
| 82 | return [] |
| 83 | |
| 84 | functions = [] |
| 85 | # 函数名字符串遵循特定模式 |
| 86 | func_pattern = re.compile( |
| 87 | rb'(?:main|runtime|fmt|net|os|crypto|encoding|io|sync|' |
| 88 | rb'syscall|reflect|strings|bytes|path|time|math|sort|' |
| 89 | rb'github\.com|golang\.org)[/\.][\w/.]+', |
| 90 | ) |
| 91 | |
| 92 | for match in func_pattern.finditer(data): |
| 93 | name = match.group().decode('utf-8', errors='replace') |
| 94 | if len(name) > 4 and len(name) < 200: |
| 95 | functions.append(name) |
| 96 | |
| 97 | return sorted(set(functions)) |
| 98 | |
| 99 | |
| 100 | def extract_go_strings(data): |
| 101 | """提取 Go 风格字符串(指针+长度对)。""" |
| 102 | # Go 字符串不以空字符结尾;提取可读序列 |
| 103 | strings = [] |
| 104 | ascii_pattern = re.compile(rb'[\x20-\x7e]{10,}') |
| 105 | |
| 106 | for match in ascii_pattern.finditer(data): |
| 107 | s = match.group().decode('ascii') |
| 108 | # 过滤出有趣的恶意软件字符串 |
| 109 | interesting = [ |
| 110 | 'http', 'https', 'tcp', 'udp', 'dns', |
| 111 | 'cmd', 'shell', 'exec', 'upload', 'download', |
| 112 | 'encrypt', 'decrypt', 'key', 'token', 'password', |
| 113 | 'c2', 'beacon', 'agent', 'implant', 'bot', |
| 114 | 'mutex', 'persist', 'registry', 'scheduled', |
| 115 | ] |
| 116 | if any(kw in s.lower() for kw in interesting): |
| 117 | strings.append(s) |
| 118 | |
| 119 | return strings |
| 120 | |
| 121 | |
| 122 | def extract_dependencies(data): |
| 123 | """从二进制文件提取 Go 模块依赖项。""" |
| 124 | deps = [] |
| 125 | # 模块路径遵循模式:github.com/user/repo |
| 126 | dep_pattern = re.compile( |
| 127 | rb'((?:github\.com|gitlab\.com|golang\.org|gopkg\.in|' |
| 128 | rb'go\.etcd\.io|google\.golang\.org)/[^\x00\s]{5,80})' |
| 129 | ) |
| 130 | |
| 131 | for match in dep_pattern.finditer(data): |
| 132 | dep = match.group().decode('utf-8', errors='replace') |
| 133 | deps.append(dep) |
| 134 | |
| 135 | unique_deps = sorted(set(deps)) |
| 136 | return unique_deps |
| 137 | |
| 138 | |
| 139 | def analyze_go_binary(filepath): |
| 140 | """对 Go 恶意软件二进制文件进行完整分析。""" |
| 141 | with open(filepath, 'rb') as f: |
| 142 | data = f.read() |
| 143 | |
| 144 | print(f"[+] 正在分析 Go 二进制文件:{filepath}") |
| 145 | print(f" 文件大小:{len(data):,} 字节") |
| 146 | print("=" * 60) |
| 147 | |
| 148 | # 构建信息 |
| 149 | find_go_build_info(data) |
| 150 | |
| 151 | # pclntab |
| 152 | pclntab_offset, go_version = find_pclntab(data) |
| 153 | |
| 154 | # 函数 |
| 155 | functions = extract_function_names(data, pclntab_offset) |
| 156 | print(f"\n[+] 恢复了 {len(functions)} 个函数名") |
| 157 | |
| 158 | # 分类函数 |
| 159 | categories = { |
| 160 | "network": [], "crypto": [], "os_exec": [], |
| 161 | "file_io": [], "main": [], "third_party": [], |
| 162 | } |
| 163 | for f in functions: |
| 164 | if 'net/' in f or 'http' in f.lower(): |
| 165 | categories["network"].append(f) |
| 166 | elif 'crypto' in f: |
| 167 | categories["crypto"].append(f) |
| 168 | elif 'os/exec' in f or 'syscall' in f: |
| 169 | categories["os_exec"].append(f) |