Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reverse engineer Go-compiled malware using Ghidra with specialized scripts for function recovery, string extraction, and type reconstruction in stripped Go binaries.
.claude/skills/analyzing-golang-malware-with-ghidra/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
Go (Golang) has become a popular language for malware authors due to its cross-compilation capabilities, static linking that produces self-contained binaries, and the complexity it introduces for reverse engineering. Go binaries contain the entire runtime, standard library, and all dependencies statically linked, resulting in large binaries (often 5-15MB) with thousands of functions. Ghidra struggles with Go-specific string formats (non-null-terminated), stripped function names, and goroutine concurrency patterns. Specialized tools like GoResolver (Volexity, 2025) use control-flow graph similarity to automatically deobfuscate and recover function names in stripped or obfuscated Go binaries.
Go binaries embed rich metadata in the pclntab (PC Line Table) structure, which maps program counters to function names, source files, and line numbers. Even stripped binaries retain this metadata. The moduledata structure contains pointers to type information, itabs (interface tables), and the pclntab itself. Go strings are stored as a pointer-length pair rather than null-terminated C strings.
Despite stripping symbol tables, Go binaries retain function names within the pclntab. However, obfuscation tools like garble rename functions to random strings. GoResolver addresses this by computing control-flow graph signatures of obfuscated functions and matching them against a database of known Go standard library and third-party package functions.
Go's dependency management embeds module paths and version strings in the binary. Extracting these reveals the malware's third-party dependencies (HTTP libraries, encryption packages, C2 frameworks), which provides insight into capabilities without full reverse engineering.
python#!/usr/bin/env python3 """Analyze Go binary metadata for malware analysis.""" import struct import sys import re def find_go_build_info(data): """Extract Go build information from binary.""" # Go buildinfo magic: \xff Go buildinf: magic = b'\xff Go buildinf:' offset = data.find(magic) if offset == -1: return None print(f"[+] Go build info at offset 0x{offset:x}") # Extract Go version string nearby 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): """Locate the pclntab (PC Line Table) structure.""" # pclntab magic bytes vary by 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 found at 0x{offset:x} ({version})") return offset, version return None, None def extract_function_names(data, pclntab_offset): """Extract function names from pclntab.""" if pclntab_offset is None: return [] functions = [] # Function name strings follow specific patterns 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): """Extract Go-style strings (pointer+length pairs).""" # Go strings are not null-terminated; extract readable sequences strings = [] ascii_pattern = re.compile(rb'[\x20-\x7e]{10,}') for match in ascii_pattern.finditer(data): s = match.group().decode('ascii') # Filter for interesting malware strings 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): """Extract Go module dependencies from binary.""" deps = [] # Module paths follow pattern: 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): """Full analysis of Go malware binary.""" with open(filepath, 'rb') as f: data = f.read() print(f"[+] Analyzing Go binary: {filepath}") print(f" File size: {len(data):,} bytes") print("=" * 60) # Build info find_go_build_info(data) # pclntab pclntab_offset, go_version = find_pclntab(data) # Functions functions = extract_function_names(data, pclntab_offset) print(f"\n[+] Recovered {len(functions)} function names") # Categorize 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)} functions):") for fn in funcs[:10]: print(f" {fn}") # Dependencies deps = extract_dependencies(data) print(f"\n[+] Dependencies ({len(deps)}):") for dep in deps[:20]: print(f" {dep}") # Suspicious strings sus_strings = extract_go_strings(data) print(f"\n[+] Suspicious strings ({len(sus_strings)}):") for s in sus_strings[:20]: print(f" {s}") if __name__ == "__main__": if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} <go_binary>") sys.exit(1) analyze_go_binary(sys.argv[1])
python# Ghidra script (run within Ghidra's script manager) # Save as AnalyzeGoBinary.py in Ghidra scripts directory # @category MalwareAnalysis # @description Analyze Go binary structure and recover metadata def analyze_go_binary_ghidra(): """Ghidra script for Go binary analysis.""" from ghidra.program.model.mem import MemoryAccessException program = getCurrentProgram() memory = program.getMemory() listing = program.getListing() print("[+] Go Binary Analysis Script") print(f" Program: {program.getName()}") # Find 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 found at {addr}") # Create label program.getSymbolTable().createLabel( addr, "go_pclntab", None, ghidra.program.model.symbol.SourceType.ANALYSIS ) break # Fix Go string definitions # Go strings are ptr+len, not null terminated print("[+] Fixing Go string references...") # Search for function names containing package paths 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"[+] Found {func_count} Go function symbols") # Execute analyze_go_binary_ghidra()
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 22 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +39 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.