mcpField
A
analyzing-golang-malware-with-ghidra

analyzing-golang-malware-with-ghidra

github.com/mukul975セキュリティ35.2k442 回インストール8 週間前に更新
リポジトリ

analyzing-golang-malware-with-ghidra について

GhidraでGoマルウェアを調べるためのセキュリティ分析スキルです。関数名やメタデータの復元、依存関係の特定、対象を絞ったバイナリ分析の進め方を扱います。

主な機能

  1. 01Goバイナリのメタデータ分析
  2. 02pclntab解析と関数名の復元
  3. 03依存関係の抽出
  4. 04Ghidra分析ワークフロー

活用シーン

  1. 01Goマルウェアのインシデント調査
  2. 02バイナリから依存関係と機能を特定
  3. 03分析結果に基づく検知ルールの作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill analyzing-golang-malware-with-ghidra

インストール後、Claude Code で/analyzing-golang-malware-with-ghidraを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE
assets/
template.md
references/
api-reference.md
standards.md
workflows.md
scripts/
agent.py
process.py

詳細

Ghidraを使用したGoマルウェアの解析

概要

Go(Golang)は、クロスコンパイル機能、自己完結型バイナリを生成する静的リンク、リバースエンジニアリングを複雑にする特性により、マルウェア作成者の間で人気の言語になっています。Goバイナリにはランタイム、標準ライブラリ、すべての依存関係が静的にリンクされた状態で含まれるため、サイズが大きく(多くの場合5~15MB)、数千もの関数が含まれます。Ghidraでは、Go固有の文字列形式(ヌル終端ではない)、削除された関数名、ゴルーチンの並行処理パターンの扱いに苦労します。GoResolver(Volexity、2025年)のような専用ツールは、制御フローグラフの類似性を利用して、削除または難読化されたGoバイナリの難読化を自動的に解除し、関数名を復元します。

使用する場面

  • GoマルウェアをGhidraで解析する必要があるセキュリティインシデントの調査
  • この領域の検出ルールや脅威ハンティング用クエリの作成
  • SOCアナリストがこの種の解析に構造化された手順を必要とする場合
  • 関連する攻撃手法に対するセキュリティ監視の網羅性の検証

前提条件

  • JDK 17以降を備えたGhidra 11.0以降
  • GoResolverプラグイン(関数名の復元用)
  • Go Reverse Engineering Tool Kit(go-re.tk)
  • ヘルパースクリプト用のPython 3.9以降
  • Goランタイム内部(ゴルーチン、チャネル、インターフェース)の理解
  • Goバイナリ構造(pclntab、moduledata、itab)の知識

主な概念

Goバイナリの構造

Goバイナリには、プログラムカウンターを関数名、ソースファイル、行番号に対応付けるpclntab(PC Line Table)構造に、豊富なメタデータが埋め込まれています。シンボルを削除したバイナリでも、このメタデータは保持されます。moduledata構造には、型情報、itab(インターフェーステーブル)、pclntab自体へのポインターが含まれます。Goの文字列は、ヌル終端C文字列ではなく、ポインターと長さのペアとして格納されます。

シンボルを削除したバイナリでの関数の復元

シンボルテーブルが削除されていても、Goバイナリはpclntab内に関数名を保持しています。ただし、garbleのような難読化ツールは関数名をランダムな文字列に変更します。GoResolverは、難読化された関数の制御フローグラフのシグネチャを計算し、既知のGo標準ライブラリ関数およびサードパーティパッケージ関数のデータベースと照合することで、この問題に対処します。

モジュール/依存関係の抽出

Goの依存関係管理では、モジュールパスとバージョン文字列がバイナリに埋め込まれます。これらを抽出すると、マルウェアのサードパーティ依存関係(HTTPライブラリ、暗号化パッケージ、C2フレームワーク)が判明し、完全なリバースエンジニアリングを行わなくても、その機能を把握できます。

ワークフロー

ステップ1:バイナリの初期解析
#!/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])
ステップ2:Ghidra解析スクリプト
# 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()

検証基準

  • バイナリからGoのバージョンとビルド情報が抽出されている
  • pclntabが特定され、解析されて関数名が復元されている
  • サードパーティ依存関係が特定され、マルウェアの機能が明らかになっている
  • 対象を絞った解析のために、メインパッケージの関数が列挙されている
  • ネットワーク、暗号化、OS実行関数が分類されている
  • GhidraでGoランタイムの構造に正しくラベルが付けられている

参考資料

コメント