Was macht Analyse von Golang-Schadsoftware mit Ghidra?
Überblick
Go (Golang) ist aufgrund seiner Cross-Compilation-Fähigkeiten, des statischen Linkens (das eigenständige Binärdateien erzeugt) und der Komplexität, die es für Reverse Engineering mit sich bringt, zu einer beliebten Sprache für Malware-Autoren geworden. Go-Binärdateien enthalten die gesamte Laufzeitumgebung, die Standardbibliothek und alle Abhängigkeiten statisch gelinkt, was zu großen Binärdateien (oft 5-15 MB) mit Tausenden von Funktionen führt. Ghidra hat Schwierigkeiten mit Go-spezifischen Zeichenfolgenformaten (nicht null-terminiert), gestrippten Funktionsnamen und Goroutine-Nebenläufigkeitsmustern. Spezialisierte Tools wie GoResolver (Volexity, 2025) nutzen Ähnlichkeiten von Kontrollflussgraphen, um Funktionsnamen in gestrippten oder obfuskierten Go-Binärdateien automatisch zu deobfuskieren und wiederherzustellen.
Verwendung
- Bei der Untersuchung von Sicherheitsvorfällen, die die Analyse von Golang-Malware mit Ghidra erfordern.
- Beim Erstellen von Erkennungsregeln oder Threat-Hunting-Abfragen für diesen Bereich.
- Wenn SOC-Analysten strukturierte Verfahren für diese Art von Analyse benötigen.
- Bei der Validierung der Sicherheitsüberwachungsabdeckung für verwandte Angriffstechniken.
Voraussetzungen
- Ghidra 11.0+ mit JDK 17+
- GoResolver-Plugin (zur Wiederherstellung von Funktionsnamen)
- Go Reverse Engineering Tool Kit (go-re.tk)
- Python 3.9+ für Hilfsskripte
- Verständnis der Go-Laufzeitinterna (Goroutinen, Channels, Interfaces)
- Vertrautheit mit der Go-Binärstruktur (pclntab, moduledata, itab)
Schlüsselkonzepte
Go-Binärstruktur
Go-Binärdateien betten umfangreiche Metadaten in die pclntab-Struktur (PC Line Table) ein, die Programmzähler auf Funktionsnamen, Quelldateien und Zeilennummern abbildet. Selbst gestrippte Binärdateien behalten diese Metadaten. Die moduledata-Struktur enthält Zeiger auf Typinformationen, itabs (Interfacetabellen) und die pclntab selbst. Go-Zeichenfolgen werden als Paar aus Zeiger und Länge gespeichert, nicht als null-terminierte C-Zeichenfolgen.
Funktionswiederherstellung in gestrippten Binärdateien
Trotz des Entfernens von Symboltabellen behalten Go-Binärdateien Funktionsnamen innerhalb der pclntab. Obfuskierungstools wie garble benennen Funktionen jedoch in zufällige Zeichenfolgen um. GoResolver adressiert dies, indem es Signaturen von Kontrollflussgraphen obfuskatierter Funktionen berechnet und mit einer Datenbank bekannter Funktionen aus der Go-Standardbibliothek und Drittanbieterpaketen abgleicht.
Abhängigkeitsextraktion
Das Abhängigkeitsmanagement von Go bettet Modulpfade und Versionszeichenfolgen in die Binärdatei ein. Die Extraktion dieser Metadaten offenbart die Drittanbieter-Abhängigkeiten der Malware (HTTP-Bibliotheken, Verschlüsselungspakete, C2-Frameworks), was Einblicke in die Fähigkeiten bietet, ohne eine vollständige Rückentwicklung durchführen zu müssen.
Arbeitsablauf
Schritt 1: Initiale Binäranalyse
#!/usr/bin/env python3
"""Analyse von Go-Binärmetadaten für Malware-Analyse."""
import struct
import sys
import re
def find_go_build_info(data):
"""Extrahiere Go-Build-Informationen aus der Binärdatei."""
# Go buildinfo-Magic: \xff Go buildinf:
magic = b'\xff Go buildinf:'
offset = data.find(magic)
if offset == -1:
return None
print(f"[+] Go-Buildinfo bei Offset 0x{offset:x}")
# Extrahiere Go-Versionszeichenfolge in der Nähe
go_version = re.search(rb'go\d+\.\d+(?:\.\d+)?', data[offset:offset+256])
if go_version:
print(f" Go-Version: {go_version.group().decode()}")
return offset
def find_pclntab(data):
"""Lokalisiere die pclntab-Struktur."""
# pclntab-Magic-Bytes variieren je nach Go-Version
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 gefunden bei 0x{offset:x} ({version})")
return offset, version
return None, None
def extract_function_names(data, pclntab_offset):
"""Extrahiere Funktionsnamen aus pclntab."""
if pclntab_offset is None:
return []
functions = []
# Funktionsnamen-Zeichenfolgen folgen bestimmten Mustern
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):
"""Extrahiere Go-artige Zeichenfolgen (Pointer+Länge-Paare)."""
# Go-Zeichenfolgen sind nicht null-terminiert; extrahiere lesbare Sequenzen
strings = []
ascii_pattern = re.compile(rb'[\x20-\x7e]{10,}')
for match in ascii_pattern.finditer(data):
s = match.group().decode('ascii')
# Filtere nach interessanten Malware-Zeichenfolgen
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):
"""Extrahiere Go-Modul-Abhängigkeiten aus der Binärdatei."""
deps = []
# Modulpfade folgen dem Muster: 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):
"""Vollständige Analyse einer Go-Malware-Binärdatei."""
with open(filepath, 'rb') as f:
data = f.read()
print(f"[+] Analysiere Go-Binärdatei: {filepath}")
print(f" Dateigröße: {len(data):,} Bytes")
print("=" * 60)
# Buildinfo
find_go_build_info(data)
# pclntab
pclntab_offset, go_version = find_pclntab(data)
# Funktionen
functions = extract_function_names(data, pclntab_offset)
print(f"\n[+] {len(functions)} Funktionsnamen wiederhergestellt")
# Funktionen kategorisieren
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)} Funktionen):")
for fn in funcs[:10]:
print(f" {fn}")
# Abhängigkeiten
deps = extract_dependencies(data)
print(f"\n[+] Abhängigkeiten ({len(deps)}):")
for dep in deps[:20]:
print(f" {dep}")
# Verdächtige Zeichenfolgen
sus_strings = extract_go_strings(data)
print(f"\n[+] Verdächtige Zeichenfolgen ({len(sus_strings)}):")
for s in sus_strings[:20]:
print(f" {s}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Verwendung: {sys.argv[0]} <go_binärdatei>")
sys.exit(1)
analyze_go_binary(sys.argv[1])
Schritt 2: Ghidra-Analyse-Skript
# Ghidra-Skript (im Ghidra-Skriptmanager ausführen)
# Speichern als AnalyzeGoBinary.py im Ghidra-Skriptverzeichnis
# @category MalwareAnalysis
# @description Analysiere Go-Binärstruktur und stelle Metadaten wieder her
def analyze_go_binary_ghidra():
"""Ghidra-Skript zur Analyse von Go-Binärdateien."""
from ghidra.program.model.mem import MemoryAccessException
program = getCurrentProgram()
memory = program.getMemory()
listing = program.getListing()
print("[+] Go-Binäranalyse-Skript")
print(f" Programm: {program.getName()}")
# Finde 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 gefunden bei {addr}")
# Label erstellen
program.getSymbolTable().createLabel(
addr, "go_pclntab", None,
ghidra.program.model.symbol.SourceType.ANALYSIS
)
break
# Go-Zeichenfolgen-Definitionen korrigieren
# Go-Zeichenfolgen sind ptr+len, nicht null-terminiert
print("[+] Korrigiere Go-Zeichenfolgenreferenzen...")
# Suche nach Funktionsnamen, die Paketpfade enthalten
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-Funktionssymbole gefunden")
# Ausführen
analyze_go_binary_ghidra()
Validierungskriterien
- Go-Version und Buildinformationen aus der Binärdatei extrahiert
- pclntab lokalisiert und für die Wiederherstellung von Funktionsnamen geparst
- Drittanbieter-Abhängigkeiten identifiziert, die Malware-Fähigkeiten offenlegen
- Hauptpaketfunktionen für gezielte Analyse aufgelistet
- Netzwerk-, Krypto- und OS-Exec-Funktionen kategorisiert
- Ghidra-Analyse kennzeichnet Go-Laufzeitstrukturen korrekt