malware analysis

Reverse Engineering Malware with Ghidra

Reverse engineers malware binaries using NSA's Ghidra disassembler and decompiler to understand internal logic, cryptographic routines, C2 protocols, and evasion techniques at the assembly and pseudo-C level. Activates for requests involving malware reverse engineering, disassembly analysis, decompilation, binary analysis, or understanding malware internals.

decompilationdisassemblyghidramalwarereverse-engineering
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • Static and dynamic analysis have identified suspicious functionality that requires deeper code-level understanding
  • You need to reverse engineer C2 communication protocols, encryption algorithms, or custom obfuscation
  • Understanding the exact exploit mechanism or vulnerability targeted by a malware sample
  • Extracting hardcoded configuration data (C2 addresses, encryption keys, campaign IDs) embedded in compiled code
  • Developing precise YARA rules or detection signatures based on unique code patterns

Do not use for initial triage of unknown samples; perform static analysis with PEStudio and behavioral analysis with Cuckoo first.

Prerequisites

  • Ghidra 11.x installed (download from https://ghidra-sre.org/) with JDK 17+
  • Analysis VM isolated from production network (Windows or Linux host)
  • Familiarity with x86/x64 assembly language and Windows API conventions
  • PDB symbol files for Windows system DLLs to improve decompilation accuracy
  • Ghidra scripts repository (ghidra_scripts) for automated analysis tasks
  • Secondary reference: IDA Free or Binary Ninja for cross-validation of analysis results

Workflow

Step 1: Create Project and Import Binary

Set up a Ghidra project and import the malware sample:

1. Launch Ghidra: ghidraRun (Linux) or ghidraRun.bat (Windows)
2. File -> New Project -> Non-Shared Project -> Select directory
3. File -> Import File -> Select malware binary
4. Ghidra auto-detects format (PE, ELF, Mach-O) and architecture
5. Accept default import options (or specify base address if known)
6. Double-click imported file to open in CodeBrowser
7. When prompted, run Auto Analysis with default analyzers enabled

Headless analysis for automation:

# Run Ghidra headless analysis with decompiler
/opt/ghidra/support/analyzeHeadless /tmp/ghidra_project MalwareProject \
  -import suspect.exe \
  -postScript ExportDecompilation.py \
  -scriptPath /opt/ghidra/scripts/ \
  -deleteProject

Step 2: Identify Key Functions and Entry Points

Navigate the binary to locate critical code sections:

Navigation Strategy:
━━━━━━━━━━━━━━━━━━━
1. Start at entry point (OEP) - follow execution from _start/WinMain
2. Check Symbol Tree for imported functions (Window -> Symbol Tree)
3. Search for cross-references to suspicious APIs:
   - VirtualAlloc/VirtualAllocEx (memory allocation for injection)
   - CreateRemoteThread (remote thread injection)
   - CryptEncrypt/CryptDecrypt (encryption operations)
   - InternetOpen/HttpSendRequest (C2 communication)
   - RegSetValueEx (persistence via registry)
4. Use Search -> For Strings to find embedded URLs, IPs, and paths
5. Check the Functions window sorted by size (large functions often contain core logic)

Ghidra keyboard shortcuts for efficient navigation:

G         - Go to address
Ctrl+E    - Search for strings
X         - Show cross-references to current location
Ctrl+Shift+F - Search memory for byte patterns
L         - Rename label/function
;         - Add comment
T         - Retype variable
Ctrl+L    - Retype return value

Step 3: Analyze Decompiled Code

Use Ghidra's decompiler to understand function logic:

// Example: Ghidra decompiler output for a decryption routine
// Analyst renames variables and adds types for clarity
 
void decrypt_config(BYTE *encrypted_data, int data_len, BYTE *key, int key_len) {
    // XOR decryption with rolling key
    for (int i = 0; i < data_len; i++) {
        encrypted_data[i] = encrypted_data[i] ^ key[i % key_len];
    }
    return;
}
 
// Analyst actions in Ghidra:
// 1. Right-click parameters -> Retype to correct types (BYTE*, int)
// 2. Right-click variables -> Rename to meaningful names
// 3. Add comments explaining the algorithm
// 4. Set function signature to propagate types to callers

Step 4: Trace C2 Communication Logic

Follow the network communication code path:

Analysis Steps for C2 Protocol Reverse Engineering:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Find InternetOpenA/WinHttpOpen call -> trace to wrapper function
2. Follow data flow from encrypted config -> URL construction
3. Identify HTTP method (GET/POST), headers, and body format
4. Locate response parsing logic (JSON parsing, custom binary protocol)
5. Map the C2 command dispatcher (switch/case or jump table)
6. Document the command set (download, execute, exfiltrate, update, uninstall)

Ghidra Script for extracting C2 configuration:

# Ghidra Python script: extract_c2_config.py
# Run via Script Manager in Ghidra
 
from ghidra.program.model.data import StringDataType
from ghidra.program.model.symbol import SourceType
 
# Search for XOR decryption patterns
listing = currentProgram.getListing()
memory = currentProgram.getMemory()
 
# Find references to InternetOpenA
symbol_table = currentProgram.getSymbolTable()
for symbol in symbol_table.getExternalSymbols():
    if "InternetOpen" in symbol.getName():
        refs = getReferencesTo(symbol.getAddress())
        for ref in refs:
            print("C2 init at: {}".format(ref.getFromAddress()))

Step 5: Analyze Encryption and Obfuscation

Identify and document cryptographic routines:

Common Malware Encryption Patterns:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
XOR Cipher:     Loop with XOR operation, often single-byte or rolling key
RC4:            Two loops (KSA + PRGA), 256-byte S-box initialization
AES:            Look for S-box constants (0x63, 0x7C, 0x77...) or calls to CryptEncrypt
Base64:         Lookup table with A-Za-z0-9+/= characters
Custom:         Combination of arithmetic operations (ADD, SUB, ROL, ROR with XOR)
 
Identification Tips:
- Search for constants: AES S-box, CRC32 table, MD5 init values
- Look for loop structures operating on byte arrays
- Check for Windows Crypto API usage (CryptAcquireContext -> CryptCreateHash -> CryptEncrypt)
- FindCrypt Ghidra plugin automatically identifies crypto constants

Step 6: Document Findings and Create Detection Signatures

Produce actionable intelligence from reverse engineering:

# Generate YARA rule from unique code patterns found in Ghidra
cat << 'EOF' > malware_family_x.yar
rule MalwareFamilyX_Decryptor {
    meta:
        description = "Detects MalwareX decryption routine"
        author = "analyst"
        date = "2025-09-15"
    strings:
        // XOR decryption loop with hardcoded key
        $decrypt = { 8A 04 0E 32 04 0F 88 04 0E 41 3B CA 7C F3 }
        // C2 URL pattern after decryption
        $c2_pattern = "/gate.php?id=" ascii
    condition:
        uint16(0) == 0x5A4D and $decrypt and $c2_pattern
}
EOF

Key Concepts

Term Definition
Disassembly Converting machine code bytes into human-readable assembly language instructions; Ghidra's Listing view shows disassembled code
Decompilation Lifting assembly code to pseudo-C representation for easier analysis; Ghidra's Decompile window provides this view
Cross-Reference (XREF) Reference showing where a function or data address is called from or used; essential for tracing code execution flow
Control Flow Graph (CFG) Visual representation of all possible execution paths through a function; reveals branching logic and loops
Original Entry Point (OEP) The actual start address of the malware code after unpacking; packers redirect execution through an unpacking stub first
Function Signature The return type, name, and parameter types of a function; applying correct signatures improves decompiler output quality
Ghidra Script Python or Java automation script executed within Ghidra to perform batch analysis, pattern searching, or data extraction

Tools & Systems

  • Ghidra: NSA's open-source software reverse engineering suite with disassembler, decompiler, and scripting support for multiple architectures
  • IDA Pro/Free: Industry-standard interactive disassembler; IDA Free provides x86/x64 cloud-based decompilation
  • Binary Ninja: Commercial reverse engineering platform with modern UI and extensive API for plugin development
  • x64dbg: Open-source x64/x32 debugger for Windows used alongside Ghidra for dynamic debugging of malware
  • FindCrypt (Ghidra Plugin): Plugin that identifies cryptographic constants and algorithms in binary code

Common Scenarios

Scenario: Reversing Custom C2 Protocol

Context: Behavioral analysis shows encrypted traffic to an external IP on a non-standard port. Network signatures cannot detect variants because the protocol is proprietary. Deep reverse engineering is needed to understand the protocol structure.

Approach:

  1. Import the unpacked sample into Ghidra and run full auto-analysis
  2. Locate socket/WinHTTP API calls and trace backwards to the calling function
  3. Identify the encryption routine called before data is sent (follow data flow from send/HttpSendRequest)
  4. Reverse the encryption (XOR key extraction, RC4 key derivation, AES key location)
  5. Map the command structure by analyzing the response parsing function (switch/case on command IDs)
  6. Document the protocol format (header structure, command bytes, encryption method)
  7. Create a protocol decoder script for network monitoring tools

Pitfalls:

  • Not running the full auto-analysis before starting manual analysis (missing function boundaries and type propagation)
  • Ignoring indirect calls through function pointers or vtables (use cross-references to data holding function addresses)
  • Spending time on library code that Ghidra's Function ID (FID) or FLIRT signatures should have identified
  • Not saving Ghidra project progress frequently (analysis state can be lost on crashes)

Output Format

REVERSE ENGINEERING ANALYSIS REPORT
=====================================
Sample:           unpacked_payload.exe
SHA-256:          abc123def456...
Architecture:     x86 (32-bit PE)
Ghidra Project:   MalwareX_Analysis
 
FUNCTION MAP
0x00401000  main()              - Entry point, initializes config
0x00401200  decrypt_config()    - XOR decryption with 16-byte key
0x00401400  init_c2()           - WinHTTP initialization, URL construction
0x00401800  c2_beacon()         - HTTP POST beacon with system info
0x00401C00  cmd_dispatcher()    - Switch on 12 command codes
0x00402000  inject_process()    - Process hollowing into svchost.exe
0x00402400  persist_registry()  - HKCU Run key persistence
0x00402800  exfil_data()        - File collection and encrypted upload
 
C2 PROTOCOL
Method:           HTTPS POST to /gate.php
Encryption:       RC4 with derived key (MD5 of bot_id + campaign_key)
Bot ID Format:    MD5(hostname + username + volume_serial)
Beacon Interval:  60 seconds with 10% jitter
Command Set:
  0x01 - Download and execute file
  0x02 - Execute shell command
  0x03 - Upload file to C2
  0x04 - Update configuration
  0x05 - Uninstall and remove traces
 
ENCRYPTION DETAILS
Algorithm:        RC4
Key Derivation:   MD5(bot_id + "campaign_2025_q3")
Hardcoded Seed:   "campaign_2025_q3" at offset 0x00405A00
 
EXTRACTED IOCs
C2 URLs:          hxxps://update.malicious[.]com/gate.php
                  hxxps://backup.evil[.]net/gate.php (failover)
Campaign ID:      campaign_2025_q3
RC4 Key Material: [see encryption details above]
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.6 KB

API Reference: Malware Reverse Engineering with Ghidra Agent

Overview

Combines Ghidra headless analysis with r2pipe (radare2) for automated malware binary analysis: function enumeration, import classification, section entropy, cryptographic constant detection, and network indicator extraction.

Dependencies

Package Version Purpose
r2pipe >= 1.8 Radare2 scripting interface for binary analysis
hashlib stdlib File hash computation

External Tools

Tool Purpose
Ghidra (analyzeHeadless) Automated disassembly and decompilation
radare2 Binary analysis, function detection, string extraction

Core Functions

run_ghidra_headless(ghidra_path, project_dir, project_name, binary_path, script)

Executes Ghidra in headless mode with optional post-analysis script.

  • Timeout: 600 seconds
  • Returns: dict with command, returncode, stdout/stderr

export_functions_ghidra(...)

Generates and runs a Ghidra script to export function list as JSON.

  • Exports: name, address, size, calling convention, is_thunk

analyze_with_radare2(filepath)

Full r2pipe analysis: binary info, functions, imports, strings, sections, entry points.

  • Classifies imports: injection, network, evasion, crypto, persistence
  • Extracts: network indicators (URLs, IPs) from strings
  • Returns: dict with info, function_count, suspicious_imports, sections, etc.

extract_crypto_constants(filepath)

Searches binary for known cryptographic constants: AES S-box, RC4 init table, SHA-256 init vector, RSA magic bytes.

  • Returns: list[dict] with constant name and file offset

analyze_malware(filepath, ghidra_path, output_dir)

Full pipeline: hashes -> crypto constants -> radare2 analysis -> Ghidra headless.

Suspicious Import Categories

Category Example Functions
injection VirtualAllocEx, WriteProcessMemory, CreateRemoteThread
network InternetOpenA, WSAStartup, URLDownloadToFileA
evasion IsDebuggerPresent, NtQueryInformationProcess
crypto CryptEncrypt, CryptDecrypt
persistence RegSetValueExA, CreateServiceA

Radare2 Commands Used

Command Purpose
aaa Full auto-analysis
ij Binary info as JSON
aflj Function list as JSON
iij Import list as JSON
izj String list as JSON
iSj Section list as JSON
iej Entry points as JSON

Usage

# With radare2 only
python agent.py malware.exe
 
# With Ghidra headless analysis
python agent.py malware.exe /opt/ghidra

Scripts 1

agent.py8.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Malware reverse engineering agent using Ghidra headless analyzer and r2pipe."""

import subprocess
import os
import sys
import json
import re
import hashlib

try:
    import r2pipe
except ImportError:
    r2pipe = None


def compute_hashes(filepath):
    """Compute file hashes for identification."""
    with open(filepath, "rb") as f:
        data = f.read()
    return {
        "md5": hashlib.md5(data).hexdigest(),
        "sha1": hashlib.sha1(data).hexdigest(),
        "sha256": hashlib.sha256(data).hexdigest(),
        "size": len(data),
    }


def run_ghidra_headless(ghidra_path, project_dir, project_name, binary_path,
                        script=None, script_args=None):
    """Run Ghidra in headless mode for automated analysis."""
    os.makedirs(project_dir, exist_ok=True)
    cmd = [
        os.path.join(ghidra_path, "support", "analyzeHeadless"),
        project_dir, project_name,
        "-import", binary_path,
        "-overwrite",
    ]
    if script:
        cmd.extend(["-postScript", script])
        if script_args:
            cmd.extend(script_args)
    result = subprocess.run(
        cmd, capture_output=True, text=True, timeout=600
    )
    return {
        "command": " ".join(cmd),
        "returncode": result.returncode,
        "stdout": result.stdout[-2000:] if result.stdout else "",
        "stderr": result.stderr[-1000:] if result.stderr else "",
    }


def export_functions_ghidra(ghidra_path, project_dir, project_name, binary_path,
                            output_file):
    """Export function list using Ghidra headless with a script."""
    script_content = """
import ghidra.program.model.listing.FunctionIterator
import json

output = []
fm = currentProgram.getFunctionManager()
funcs = fm.getFunctions(True)
for func in funcs:
    entry = {
        "name": func.getName(),
        "address": str(func.getEntryPoint()),
        "size": func.getBody().getNumAddresses(),
        "calling_convention": func.getCallingConventionName(),
        "is_thunk": func.isThunk(),
    }
    output.append(entry)

with open("{output}", "w") as f:
    json.dump(output, f, indent=2)
""".replace("{output}", output_file.replace("\\", "\\\\"))
    script_path = os.path.join(project_dir, "export_functions.py")
    with open(script_path, "w") as f:
        f.write(script_content)
    return run_ghidra_headless(
        ghidra_path, project_dir, project_name, binary_path,
        script="export_functions.py"
    )


def analyze_with_radare2(filepath):
    """Analyze binary with radare2 via r2pipe for quick triage."""
    if r2pipe is None:
        return {"error": "r2pipe not installed (pip install r2pipe)"}
    r2 = r2pipe.open(filepath, flags=["-2"])
    r2.cmd("aaa")
    info = r2.cmdj("ij")
    functions = r2.cmdj("aflj") or []
    imports = r2.cmdj("iij") or []
    strings = r2.cmdj("izj") or []
    sections = r2.cmdj("iSj") or []
    entry_points = r2.cmdj("iej") or []
    suspicious_imports = {
        "injection": ["VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread",
                      "NtCreateThreadEx"],
        "network": ["InternetOpenA", "HttpSendRequestA", "WSAStartup",
                     "URLDownloadToFileA"],
        "evasion": ["IsDebuggerPresent", "CheckRemoteDebuggerPresent",
                     "NtQueryInformationProcess"],
        "crypto": ["CryptEncrypt", "CryptDecrypt", "CryptAcquireContextA"],
        "persistence": ["RegSetValueExA", "CreateServiceA"],
    }
    import_findings = []
    for imp in imports:
        name = imp.get("name", "")
        for category, funcs in suspicious_imports.items():
            if name in funcs:
                import_findings.append({
                    "category": category,
                    "function": name,
                    "library": imp.get("lib", ""),
                })
    network_strings = []
    for s in strings:
        val = s.get("string", "")
        if re.search(r"https?://", val) or re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", val):
            network_strings.append(val[:200])
    section_analysis = []
    for sec in sections:
        entropy = sec.get("entropy", 0)
        flags = []
        if entropy and entropy > 7.0:
            flags.append("HIGH_ENTROPY")
        section_analysis.append({
            "name": sec.get("name", ""),
            "size": sec.get("size", 0),
            "vsize": sec.get("vsize", 0),
            "entropy": entropy,
            "flags": flags,
        })
    r2.quit()
    return {
        "info": {
            "arch": info.get("bin", {}).get("arch", ""),
            "bits": info.get("bin", {}).get("bits", 0),
            "os": info.get("bin", {}).get("os", ""),
            "type": info.get("bin", {}).get("bintype", ""),
            "compiler": info.get("bin", {}).get("compiler", ""),
        },
        "function_count": len(functions),
        "import_count": len(imports),
        "string_count": len(strings),
        "suspicious_imports": import_findings,
        "network_indicators": network_strings[:20],
        "sections": section_analysis,
        "entry_points": [{"vaddr": e.get("vaddr"), "type": e.get("type")} for e in entry_points],
    }


def extract_crypto_constants(filepath):
    """Search binary for known cryptographic constants."""
    with open(filepath, "rb") as f:
        data = f.read()
    constants = {
        "AES_SBOX": bytes([0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5]),
        "RC4_INIT": bytes(range(256)),
        "SHA256_INIT": bytes.fromhex("6a09e667bb67ae853c6ef372a54ff53a"),
        "RSA_MAGIC": b"RSA1",
    }
    found = []
    for name, pattern in constants.items():
        offset = data.find(pattern)
        if offset >= 0:
            found.append({"constant": name, "offset": hex(offset)})
    return found


def analyze_malware(filepath, ghidra_path=None, output_dir="/tmp/ghidra_analysis"):
    """Full malware analysis pipeline."""
    os.makedirs(output_dir, exist_ok=True)
    report = {"file": os.path.basename(filepath)}
    report["hashes"] = compute_hashes(filepath)
    report["crypto_constants"] = extract_crypto_constants(filepath)
    if r2pipe:
        report["radare2"] = analyze_with_radare2(filepath)
    if ghidra_path and os.path.exists(ghidra_path):
        ghidra_result = run_ghidra_headless(
            ghidra_path, output_dir, "malware_project", filepath
        )
        report["ghidra"] = {
            "analysis_complete": ghidra_result["returncode"] == 0,
            "output": ghidra_result["stdout"][-500:],
        }
    return report


def print_report(report):
    print("Malware Reverse Engineering Report")
    print("=" * 50)
    print(f"File: {report['file']}")
    print(f"SHA-256: {report['hashes']['sha256']}")
    print(f"Size: {report['hashes']['size']} bytes")
    if report.get("crypto_constants"):
        print(f"\nCrypto Constants Found:")
        for c in report["crypto_constants"]:
            print(f"  {c['constant']} at {c['offset']}")
    r2 = report.get("radare2", {})
    if r2 and "error" not in r2:
        info = r2.get("info", {})
        print(f"\nBinary Info: {info.get('arch', '?')}/{info.get('bits', '?')}bit "
              f"({info.get('os', '?')}) [{info.get('type', '?')}]")
        print(f"Functions: {r2.get('function_count', 0)}")
        print(f"Imports: {r2.get('import_count', 0)}")
        if r2.get("suspicious_imports"):
            print(f"\nSuspicious Imports:")
            for imp in r2["suspicious_imports"]:
                print(f"  [{imp['category']}] {imp['library']} -> {imp['function']}")
        if r2.get("network_indicators"):
            print(f"\nNetwork Indicators:")
            for ni in r2["network_indicators"][:10]:
                print(f"  {ni}")
        print(f"\nSections:")
        for sec in r2.get("sections", []):
            flags = f" [{', '.join(sec['flags'])}]" if sec.get("flags") else ""
            print(f"  {sec['name']:10s} size={sec['size']:>8} entropy={sec.get('entropy', 0):.2f}{flags}")
    if report.get("ghidra", {}).get("analysis_complete"):
        print(f"\nGhidra: Analysis complete")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python agent.py <binary> [ghidra_install_path]")
        sys.exit(1)
    binary = sys.argv[1]
    ghidra = sys.argv[2] if len(sys.argv) > 2 else None
    result = analyze_malware(binary, ghidra_path=ghidra)
    print_report(result)
Keep exploring