使用 Ghidra 分析 Golang 惡意軟體 是做什麼的?
概述
Go (Golang) 因其跨平台編譯能力、可產生自包含二進位檔案的靜態連結特性,以及為逆向工程帶來的複雜性,已成為惡意軟體作者喜愛的語言。Go 二進位檔案包含了完整的執行時期環境、標準程式庫以及所有靜態連結的依賴項,導致產生的二進位檔案通常體積龐大 (5-15MB),且包含數千個函式。Ghidra 在處理 Go 特有的字串格式 (非空終止)、剝離的符號表以及 Goroutine 並行模式時較為困難。像 GoResolver (Volexity, 2025) 這類專用工具,利用控制流圖相似性來自動去混淆並恢復被剝離或混淆的 Go 二進位檔案中的函式名稱。
使用時機
- 在調查需要分析 Golang 惡意軟體的安全事件時
- 在為此領域建立偵測規則或威脅獵捕查詢時
- 當 SOC 分析師需要此分析類型的結構化程序時
- 在驗證相關攻擊技術的安全監控覆蓋範圍時
先決條件
- Ghidra 11.0+ 搭配 JDK 17+
- GoResolver 插件 (用於函式名稱恢復)
- Go 逆向工程工具包 (go-re.tk)
- Python 3.9+ 用於輔助腳本
- 了解 Go 執行時期內部機制 (goroutine、channel、interface)
- 熟悉 Go 二進位結構 (pclntab、moduledata、itab)
關鍵概念
Go 二進位結構
Go 二進位檔案在 pclntab (PC 行表) 結構中嵌入了豐富的中繼資料,該結構將程式計數器對應到函式名稱、原始碼檔案和行號。即使是剝離了符號表的二進位檔案也會保留此中繼資料。moduledata 結構包含指向型別資訊、itab (介面表) 和 pclntab 本身的指標。Go 字串儲存為 (指標, 長度) 對,而非以空字元結尾的 C 字串。
剝離二進位檔案中的函式恢復
儘管剝離了符號表,Go 二進位檔案仍會在 pclntab 中保留函式名稱。然而,像 garble 這樣的混淆工具會將函式重新命名為隨機字串。GoResolver 透過計算混淆函式的控制流圖簽名,並與已知 Go 標準程式庫和第三方套件函式的資料庫進行比對來解決此問題。
套件/依賴項提取
Go 的依賴項管理將模組路徑和版本字串嵌入到二進位檔案中。提取這些資訊可以揭示惡意軟體的第三方依賴項 (HTTP 程式庫、加密套件、C2 框架),無需完整逆向工程即可了解其功能。
工作流程
第一步:初始二進位分析
#!/usr/bin/env python3
"""分析 Go 二進位檔案的中繼資料以進行惡意軟體分析。"""
import struct
import sys
import re
def find_go_build_info(data):
"""從二進位檔案中提取 Go build 資訊。"""
# Go buildinfo 魔術字串: \xff Go buildinf:
magic = b'\xff Go buildinf:'
offset = data.find(magic)
if offset == -1:
return None
print(f"[+] Go build 資訊位於偏移量 0x{offset:x}")
# 在附近提取 Go 版本字串
go_version = re.search(rb'go\d+\.\d+(?:\.\d+)?', data[offset:offset+256])
if go_version:
print(f" Go 版本: {go_version.group().decode()}")
return offset
def find_pclntab(data):
"""定位 pclntab (PC 行表) 結構。"""
# pclntab 魔術字節因 Go 版本而異
magics = {
b'\xfb\xff\xff\xff\x00\x00': "Go 1.2-1.15",
b'\xfa\xff\xff\xff\x00\x00': "Go 1.16-1.17",
b'\xf1\xff\xff\xff\x00\x00': "Go 1.18-1.19",
b'\xf0\xff\xff\xff\x00\x00': "Go 1.20+",
}
for magic, version in magics.items():
offset = data.find(magic)
if offset != -1:
print(f"[+] pclntab 位於偏移量 0x{offset:x} ({version})")
return offset, version
return None, None
def extract_function_names(data, pclntab_offset):
"""從 pclntab 中提取函式名稱。"""
if pclntab_offset is None:
return []
functions = []
# 函式名稱字串遵循特定模式
func_pattern = re.compile(
rb'(?:main|runtime|fmt|net|os|crypto|encoding|io|sync|'
rb'syscall|reflect|strings|bytes|path|time|math|sort|'
rb'github\.com|golang\.org)[/\.][\w/.]*',
)
for match in func_pattern.finditer(data):
name = match.group().decode('utf-8', errors='replace')
if len(name) > 4 and len(name) < 200:
functions.append(name)
return sorted(set(functions))
def extract_go_strings(data):
"""提取 Go 風格的字串 (指標+長度對)。"""
# Go 字串不是以空字元結尾的;提取可讀的序列
strings = []
ascii_pattern = re.compile(rb'[\x20-\x7e]{10,}')
for match in ascii_pattern.finditer(data):
s = match.group().decode('ascii')
# 過濾感興趣的惡意軟體字串
interesting = [
'http', 'https', 'tcp', 'udp', 'dns',
'cmd', 'shell', 'exec', 'upload', 'download',
'encrypt', 'decrypt', 'key', 'token', 'password',
'c2', 'beacon', 'agent', 'implant', 'bot',
'mutex', 'persist', 'registry', 'scheduled',
]
if any(kw in s.lower() for kw in interesting):
strings.append(s)
return strings
def extract_dependencies(data):
"""從二進位檔案中提取 Go 模組依賴項。"""
deps = []
# 模組路徑遵循模式:github.com/user/repo
dep_pattern = re.compile(
rb'((?:github\.com|gitlab\.com|golang\.org|gopkg\.in|'
rb'go\.etcd\.io|google\.golang\.org)/[^\x00\s]{5,80})'
)
for match in dep_pattern.finditer(data):
dep = match.group().decode('utf-8', errors='replace')
deps.append(dep)
unique_deps = sorted(set(deps))
return unique_deps
def analyze_go_binary(filepath):
"""對 Go 惡意軟體二進位檔案進行完整分析。"""
with open(filepath, 'rb') as f:
data = f.read()
print(f"[+] 分析 Go 二進位檔案: {filepath}")
print(f" 檔案大小: {len(data):,} 字節")
print("=" * 60)
# Build 資訊
find_go_build_info(data)
# pclntab
pclntab_offset, go_version = find_pclntab(data)
# 函式
functions = extract_function_names(data, pclntab_offset)
print(f"\n[+] 恢復了 {len(functions)} 個函式名稱")
# 分類函式
categories = {
"network": [], "crypto": [], "os_exec": [],
"file_io": [], "main": [], "third_party": [],
}
for f in functions:
if 'net/' in f or 'http' in f.lower():
categories["network"].append(f)
elif 'crypto' in f:
categories["crypto"].append(f)
elif 'os/exec' in f or 'syscall' in f:
categories["os_exec"].append(f)
elif 'os.' in f or 'io/' in f:
categories["file_io"].append(f)
elif f.startswith('main.'):
categories["main"].append(f)
elif 'github.com' in f or 'golang.org' in f:
categories["third_party"].append(f)
for cat, funcs in categories.items():
if funcs:
print(f"\n [{cat}] ({len(funcs)} 個函式):")
for fn in funcs[:10]:
print(f" {fn}")
# 依賴項
deps = extract_dependencies(data)
print(f"\n[+] 依賴項 ({len(deps)}):")
for dep in deps[:20]:
print(f" {dep}")
# 可疑字串
sus_strings = extract_go_strings(data)
print(f"\n[+] 可疑字串 ({len(sus_strings)}):")
for s in sus_strings[:20]:
print(f" {s}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"用法: {sys.argv[0]} <go_binary>")
sys.exit(1)
analyze_go_binary(sys.argv[1])
第二步:Ghidra 分析腳本
# Ghidra 腳本 (在 Ghidra 的腳本管理器中執行)
# 另存為 AnalyzeGoBinary.py 至 Ghidra 腳本目錄
# @category MalwareAnalysis
# @description 分析 Go 二進位結構並恢復中繼資料
def analyze_go_binary_ghidra():
"""用於 Go 二進位分析的 Ghidra 腳本。"""
from ghidra.program.model.mem import MemoryAccessException
program = getCurrentProgram()
memory = program.getMemory()
listing = program.getListing()
print("[+] Go 二進位分析腳本")
print(f" 程式: {program.getName()}")
# 尋找 pclntab
pclntab_magics = [
bytes([0xf0, 0xff, 0xff, 0xff]), # Go 1.20+
bytes([0xf1, 0xff, 0xff, 0xff]), # Go 1.18-1.19
bytes([0xfa, 0xff, 0xff, 0xff]), # Go 1.16-1.17
bytes([0xfb, 0xff, 0xff, 0xff]), # Go 1.2-1.15
]
for magic in pclntab_magics:
addr = memory.findBytes(
program.getMinAddress(), magic, None, True, None
)
if addr:
print(f"[+] pclntab 位於 {addr}")
# 建立標籤
program.getSymbolTable().createLabel(
addr, "go_pclntab", None,
ghidra.program.model.symbol.SourceType.ANALYSIS
)
break
# 修正 Go 字串定義
# Go 字串是 ptr+len,而非空終止
print("[+] 修正 Go 字串引用...")
# 搜尋包含套件路徑的函式名稱
symbol_table = program.getSymbolTable()
func_count = 0
for symbol in symbol_table.getAllSymbols(True):
name = symbol.getName()
if ('.' in name and
any(pkg in name for pkg in
['main.', 'runtime.', 'net.', 'crypto.', 'os.'])):
func_count += 1
print(f"[+] 找到 {func_count} 個 Go 函式符號")
# 執行
analyze_go_binary_ghidra()
驗證標準
- 從二進位檔案中提取 Go 版本和 build 資訊
- 定位並解析 pclntab 以便恢復函式名稱
- 識別第三方依賴項,揭示惡意軟體功能
- 列舉主套件函式以進行有針對性的分析
- 分類網路、加密和作業系統執行函式
- Ghidra 分析正確標記 Go 執行時期結構