Análisis de Malware Golang con Ghidra
Resumen
Go (Golang) se ha convertido en un lenguaje popular para los autores de malware debido a sus capacidades de compilación cruzada, enlace estático que produce binarios autónomos y la complejidad que introduce para la ingeniería inversa. Los binarios Go contienen todo el runtime, la biblioteca estándar y todas las dependencias enlazadas estáticamente, lo que da como resultado binarios grandes (a menudo de 5 a 15 MB) con miles de funciones. Ghidra tiene dificultades con los formatos de cadena específicos de Go (no terminados en nulo), nombres de funciones eliminados y patrones de concurrencia de goroutines. Herramientas especializadas como GoResolver (Volexity, 2025) utilizan similitud de grafos de flujo de control para desofuscar y recuperar automáticamente nombres de funciones en binarios Go ofuscados o sin símbolos.
Cuándo Usar
- Al investigar incidentes de seguridad que requieran analizar malware Golang con Ghidra
- Al construir reglas de detección o consultas de búsqueda de amenazas para este dominio
- Cuando los analistas de SOC necesiten procedimientos estructurados para este tipo de análisis
- Al validar la cobertura de monitoreo de seguridad para técnicas de ataque relacionadas
Requisitos Previos
- Ghidra 11.0+ con JDK 17+
- Plugin GoResolver (para recuperación de nombres de funciones)
- Go Reverse Engineering Tool Kit (go-re.tk)
- Python 3.9+ para scripts auxiliares
- Comprensión de los componentes internos del runtime de Go (goroutines, canales, interfaces)
- Familiaridad con la estructura de los binarios Go (pclntab, moduledata, itab)
Conceptos Clave
Estructura del Binario Go
Los binarios Go incrustan metadatos enriquecidos en la estructura pclntab (Tabla de Líneas de PC), que asigna contadores de programa a nombres de funciones, archivos fuente y números de línea. Incluso los binarios sin símbolos conservan estos metadatos. La estructura moduledata contiene punteros a información de tipos, itabs (tablas de interfaz) y el propio pclntab. Las cadenas Go se almacenan como un par puntero-longitud en lugar de cadenas C terminadas en nulo.
Recuperación de Funciones en Binarios Sin Símbolos
A pesar de eliminar las tablas de símbolos, los binarios Go retienen los nombres de las funciones dentro del pclntab. Sin embargo, herramientas de ofuscación como garble renombran las funciones con cadenas aleatorias. GoResolver aborda esto calculando firmas de grafos de flujo de control de funciones ofuscadas y comparándolas con una base de datos de funciones conocidas de la biblioteca estándar de Go y paquetes de terceros.
Extracción de Dependencias (Crate)
La gestión de dependencias de Go incrusta rutas de módulos y cadenas de versión en el binario. Extraer estas revela las dependencias de terceros del malware (bibliotecas HTTP, paquetes de cifrado, frameworks C2), lo que proporciona información sobre las capacidades sin necesidad de una ingeniería inversa completa.
Flujo de Trabajo
Paso 1: Análisis Inicial del Binario
#!/usr/bin/env python3
"""Analizar metadatos de un binario Go para análisis de malware."""
import struct
import sys
import re
def find_go_build_info(data):
"""Extraer información de compilación de Go del binario."""
# Mágica de buildinfo de Go: \xff Go buildinf:
magic = b'\xff Go buildinf:'
offset = data.find(magic)
if offset == -1:
return None
print(f"[+] Información de build de Go en offset 0x{offset:x}")
# Extraer cadena de versión de Go cercana
go_version = re.search(rb'go\d+\.\d+(?:\.\d+)?', data[offset:offset+256])
if go_version:
print(f" Versión de Go: {go_version.group().decode()}")
return offset
def find_pclntab(data):
"""Localizar la estructura pclntab (Tabla de Líneas de PC)."""
# Los bytes mágicos de pclntab varían según la versión de 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 encontrado en offset 0x{offset:x} ({version})")
return offset, version
return None, None
def extract_function_names(data, pclntab_offset):
"""Extraer nombres de funciones del pclntab."""
if pclntab_offset is None:
return []
functions = []
# Las cadenas de nombres de funciones siguen patrones específicos
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):
"""Extraer cadenas al estilo Go (pares puntero+longitud)."""
# Las cadenas Go no terminan en nulo; extraer secuencias legibles
strings = []
ascii_pattern = re.compile(rb'[\x20-\x7e]{10,}')
for match in ascii_pattern.finditer(data):
s = match.group().decode('ascii')
# Filtrar cadenas interesantes para malware
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):
"""Extraer dependencias de módulos Go del binario."""
deps = []
# Las rutas de módulos siguen el patrón: github.com/usuario/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):
"""Análisis completo de un binario de malware Go."""
with open(filepath, 'rb') as f:
data = f.read()
print(f"[+] Analizando binario Go: {filepath}")
print(f" Tamaño del archivo: {len(data):,} bytes")
print("=" * 60)
# Información de compilación
find_go_build_info(data)
# pclntab
pclntab_offset, go_version = find_pclntab(data)
# Funciones
functions = extract_function_names(data, pclntab_offset)
print(f"\n[+] Recuperados {len(functions)} nombres de funciones")
# Categorizar funciones
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)} funciones):")
for fn in funcs[:10]:
print(f" {fn}")
# Dependencias
deps = extract_dependencies(data)
print(f"\n[+] Dependencias ({len(deps)}):")
for dep in deps[:20]:
print(f" {dep}")
# Cadenas sospechosas
sus_strings = extract_go_strings(data)
print(f"\n[+] Cadenas sospechosas ({len(sus_strings)}):")
for s in sus_strings[:20]:
print(f" {s}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Uso: {sys.argv[0]} <binario_go>")
sys.exit(1)
analyze_go_binary(sys.argv[1])
Paso 2: Script de Análisis en Ghidra
# Script de Ghidra (ejecutar dentro del administrador de scripts de Ghidra)
# Guardar como AnalyzeGoBinary.py en el directorio de scripts de Ghidra
# @category MalwareAnalysis
# @description Analizar la estructura de un binario Go y recuperar metadatos
def analyze_go_binary_ghidra():
"""Script de Ghidra para análisis de binarios Go."""
from ghidra.program.model.mem import MemoryAccessException
program = getCurrentProgram()
memory = program.getMemory()
listing = program.getListing()
print("[+] Script de Análisis de Binario Go")
print(f" Programa: {program.getName()}")
# Encontrar 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 encontrado en {addr}")
# Crear etiqueta
program.getSymbolTable().createLabel(
addr, "go_pclntab", None,
ghidra.program.model.symbol.SourceType.ANALYSIS
)
break
# Corregir definiciones de cadenas Go
# Las cadenas Go son ptr+len, no terminadas en nulo
print("[+] Corrigiendo referencias a cadenas Go...")
# Buscar nombres de funciones que contengan rutas de paquetes
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"[+] Encontrados {func_count} símbolos de funciones Go")
# Ejecutar
analyze_go_binary_ghidra()
Criterios de Validación
- Versión de Go e información de compilación extraída del binario
- pclntab localizado y analizado para la recuperación de nombres de funciones
- Dependencias de terceros identificadas, revelando capacidades del malware
- Funciones del paquete principal enumeradas para análisis dirigido
- Funciones de red, criptografía y ejecución de SO categorizadas
- El análisis de Ghidra etiqueta correctamente las estructuras del runtime de Go