reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
---
|
||||
name: analyzing-golang-malware-with-ghidra
|
||||
description: Reverse engineer Go-compiled malware in Ghidra by parsing Go buildinfo
|
||||
and pclntab structures, recovering stripped/obfuscated function names (e.g. via
|
||||
GoResolver), and extracting embedded module/dependency strings and types from Go
|
||||
binaries. Use when analyzing a Go-language malware sample, deobfuscating a garble-packed
|
||||
Go binary, or recovering function names and third-party dependencies from a stripped
|
||||
Go executable.
|
||||
domain: cybersecurity
|
||||
subdomain: malware-analysis
|
||||
tags:
|
||||
- golang
|
||||
- ghidra
|
||||
- reverse-engineering
|
||||
- malware-analysis
|
||||
- binary-analysis
|
||||
- go-malware
|
||||
- disassembly
|
||||
version: '1.0'
|
||||
author: mahipal
|
||||
license: Apache-2.0
|
||||
nist_csf:
|
||||
- DE.AE-02
|
||||
- RS.AN-03
|
||||
- ID.RA-01
|
||||
- DE.CM-01
|
||||
mitre_attack:
|
||||
- T1027
|
||||
- T1620
|
||||
- T1140
|
||||
- T1059
|
||||
---
|
||||
# Analyzing Golang Malware with Ghidra
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## When to Use
|
||||
|
||||
- When investigating security incidents that require analyzing golang malware with ghidra
|
||||
- When building detection rules or threat hunting queries for this domain
|
||||
- When SOC analysts need structured procedures for this analysis type
|
||||
- When validating security monitoring coverage for related attack techniques
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Ghidra 11.0+ with JDK 17+
|
||||
- GoResolver plugin (for function name recovery)
|
||||
- Go Reverse Engineering Tool Kit (go-re.tk)
|
||||
- Python 3.9+ for helper scripts
|
||||
- Understanding of Go runtime internals (goroutines, channels, interfaces)
|
||||
- Familiarity with Go binary structure (pclntab, moduledata, itab)
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Go Binary Structure
|
||||
|
||||
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.
|
||||
|
||||
### Function Recovery in Stripped Binaries
|
||||
|
||||
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.
|
||||
|
||||
### Crate/Dependency Extraction
|
||||
|
||||
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.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Initial Binary Analysis
|
||||
|
||||
```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])
|
||||
```
|
||||
|
||||
### Step 2: Ghidra Analysis Script
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
- Go version and build information extracted from binary
|
||||
- pclntab located and parsed for function name recovery
|
||||
- Third-party dependencies identified revealing malware capabilities
|
||||
- Main package functions enumerated for targeted analysis
|
||||
- Network, crypto, and OS exec functions categorized
|
||||
- Ghidra analysis correctly labels Go runtime structures
|
||||
|
||||
## References
|
||||
|
||||
- [CUJO AI - Reverse Engineering Go Binaries with Ghidra](https://cujo.com/blog/reverse-engineering-go-binaries-with-ghidra/)
|
||||
- [Volexity GoResolver](https://www.volexity.com/blog/2025/04/01/goresolver-using-control-flow-graph-similarity-to-deobfuscate-golang-binaries-automatically/)
|
||||
- [Go Reverse Engineering Tool Kit](https://go-re.tk/about/)
|
||||
- [SentinelOne AlphaGolang](https://www.sentinelone.com/labs/alphagolang-a-step-by-step-go-malware-reversing-methodology-for-ida-pro/)
|
||||
- [Go Binary Reversing Notes](https://gist.github.com/0xdevalias/4e430914124c3fd2c51cb7ac2801acba)
|
||||
Reference in New Issue
Block a user