Hitchhiker's Guide — Triage a Binary, Score Its OPSEC, Write the Detection

The analyst/range walkthrough for Phase 02. On an owned range (or on allowed system binaries), you use objdump, readelf, and dumpbin to observe real PE/ELF structure; translate that into the lab's synthetic dict format; feed it through the format analyzer and OPSEC linter; then write a Sysmon Sigma rule for the highest-noise finding.

Safety (non-negotiable). You analyze allowed binaries — legitimate system binaries (notepad.exe, /bin/ls, /usr/bin/python3) or tools you built yourself on an isolated range. There is no malware, no live C2 agent, no weaponized payload involved. The PE/ELF structure of a legitimate system binary demonstrates all the same format concepts that a tool binary would; the lab works on dicts regardless. Never analyze unknown or untrusted binaries on a host connected to production networks.

Table of Contents


0. Authorize and scope

Before touching any binary:

  • Owner: you own the range or are analyzing a system binary (notepad.exe, /bin/ls) in a read-only, non-production context.
  • Scope: static analysis only — no execution, no dynamic analysis, no network connection.
  • Tools allowed: dumpbin, objdump, readelf, file, strings, pefile Python library.
  • Data: the binary metadata you extract stays on your range machine. No uploads to public sandboxes without explicit engagement authorization.
  • Stop condition: if the binary is unknown origin or triggers AV detection → do not analyze on a production machine; use an isolated sandbox VM.

1. Read a PE header with dumpbin / pefile

On a Windows range VM, analyze a system binary as a format exercise:

# On Windows (Visual Studio Build Tools or MSVC installed):
dumpbin /headers C:\Windows\System32\notepad.exe

Key fields to locate in the output:

FILE HEADER VALUES
    8664 machine (x64)
    ...  time date stamp       <- Unix timestamp
    ...  characteristics

OPTIONAL HEADER VALUES
    20B  magic # (PE32+)
    ...  linker version
    ...  entry point
    ...  subsystem (2 = GUI)
    ...
DATA DIRECTORIES
    ...  [0] export directory     RVA    size
    ...  [1] import directory     RVA    size
    ...  [6] debug directory      RVA    size

IMPORTS
  KERNEL32.dll
    ...  WriteFile
    ...  ReadFile
  ...

With Python pefile (install in isolated range venv):

import pefile, hashlib

pe = pefile.PE("C:/Windows/System32/notepad.exe")

# Machine, timestamp, subsystem:
print("Machine:", hex(pe.FILE_HEADER.Machine))
print("TimeDateStamp:", pe.FILE_HEADER.TimeDateStamp)
print("Subsystem:", pe.OPTIONAL_HEADER.Subsystem)

# Imports:
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    dll = entry.dll.decode()
    for imp in entry.imports:
        name = imp.name.decode() if imp.name else f"ord_{imp.ordinal}"
        print(f"  {dll} -> {name}")

# PDB path:
if hasattr(pe, 'DIRECTORY_ENTRY_DEBUG'):
    for dbg in pe.DIRECTORY_ENTRY_DEBUG:
        if dbg.struct.Type == 2:   # IMAGE_DEBUG_TYPE_CODEVIEW
            pdb = dbg.entry.PdbFileName.decode(errors='replace').rstrip('\x00')
            print("PDB:", pdb)

Capture the output and use it to fill in the pe_metadata dict in §3.


2. Read an ELF header with objdump / readelf

On Linux (Ubuntu 22.04 range VM), analyze a system binary:

# ELF header:
readelf -h /usr/bin/python3

# Program headers (segments):
readelf -l /usr/bin/python3

# Dynamic section (equivalent of PE import table):
readelf -d /usr/bin/python3 | grep 'NEEDED\|RPATH\|RUNPATH'

# Dynamic symbol table (function imports):
readelf -W --syms /usr/bin/python3 | grep 'UND'  # undefined = imported from shared libs

# Sections:
readelf -S /usr/bin/python3

Key output to capture:

ELF Header:
  Class:     ELF64
  Type:      DYN (Position-Independent Executable file)
  Machine:   Advanced Micro Devices X86-64
  Entry point address:  0x...

Dynamic section:
  (NEEDED) Shared library: [libc.so.6]
  (NEEDED) Shared library: [libpthread.so.0]
  (NEEDED) Shared library: [libdl.so.2]

Symbol table '.dynsym':
  UND  openat@@GLIBC_2.10
  UND  mmap@@GLIBC_2.2.5
  ...

3. Translate the output into the lab dict format

Convert the dumpbin or pefile output for the PE to the Lab 01 dict format:

# Example from notepad.exe (values illustrative, not exact)
pe_metadata = {
    "magic": "MZ",
    "machine": "x86-64",
    "timestamp": 1670000000,       # from TimeDateStamp
    "subsystem": 2,                # 2 = Windows GUI (expected for notepad)
    "imports": [
        ("KERNEL32.dll", "WriteFile"),
        ("KERNEL32.dll", "ReadFile"),
        ("KERNEL32.dll", "CreateFileW"),
        ("USER32.dll", "MessageBoxW"),
        ("USER32.dll", "CreateWindowExW"),
        # ... (add all imports from dumpbin output)
    ],
    "exports": [],                 # notepad has no exports
    "pdb_path": "",                # production binaries have PDB stripped
    "sections": [".text", ".data", ".rdata", ".rsrc", ".reloc"],
}

For the ELF dict (Lab 01 ELF variant):

elf_metadata = {
    "magic": "ELF",
    "e_type": "ET_DYN",
    "machine": "x86-64",
    "needed": ["libc.so.6", "libpthread.so.0", "libdl.so.2"],
    "dynsym_undefined": ["openat", "mmap", "mprotect", "read", "write"],
    "sections": [".text", ".data", ".rodata", ".dynamic", ".dynsym", ".rela.plt"],
    "interp": "/lib64/ld-linux-x86-64.so.2",
}

For the OPSEC linter dict (Lab 02), build a synthetic tool entry that demonstrates a high-noise configuration. Use values that represent "what a default-configured C2 agent would show" — not a real agent, but a realistic fictional one for the exercise:

synthetic_tool = {
    "name": "beacon-default",
    "type": "c2_agent",
    "named_pipe": "MSSE-4abc-server",          # default Cobalt Strike pipe pattern
    "user_agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)",
    "beacon_uri": "/api/",
    "sleep_seconds": 60,
    "jitter_percent": 0,
    "imphash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",   # fictional; not known-bad
    "pdb_path_stripped": False,
    "pdb_path": "C:\\build\\implant.pdb",
}

4. Run the format analyzer (Lab 01)

cd red-team-engineer/phase-02-offensive-tooling-development/lab-01-pe-format-analyzer

# Verify the reference solution passes all 14 tests:
LAB_MODULE=solution python3 -m pytest -q

# Run the analyzer against your notepad-derived dict:
python3 -c "
import solution, json

pe_metadata = {
    'magic': 'MZ',
    'machine': 'x86-64',
    'timestamp': 1670000000,
    'subsystem': 2,
    'imports': [
        ('KERNEL32.dll', 'WriteFile'),
        ('KERNEL32.dll', 'ReadFile'),
        ('USER32.dll', 'MessageBoxW'),
    ],
    'exports': [],
    'pdb_path': '',
    'sections': ['.text', '.data', '.rdata', '.rsrc'],
}
result = solution.analyze(pe_metadata)
print(json.dumps(result, indent=2))
"

Expected output includes:

  • file_type: "PE32+"
  • imphash: the computed MD5 of the normalized import list
  • opsec_flags: a list of flags (for notepad with no PDB and no suspicious imports, the list should be empty or minimal — which is the expected result for a hardened system binary)

Now try with a synthetic tool dict that has OPSEC noise:

# Add to the metadata:
pe_metadata_noisy = {
    "magic": "MZ",
    "machine": "x86-64",
    "timestamp": 1710000000,  # recent timestamp, not zeroed
    "subsystem": 3,
    "imports": [
        ("kernel32.dll", "VirtualAllocEx"),
        ("kernel32.dll", "WriteProcessMemory"),
        ("ntdll.dll", "NtCreateThreadEx"),
        ("ws2_32.dll", "WSAStartup"),
    ],
    "exports": [{"name": "ReflectiveLoader", "rva": 0x1000}],
    "pdb_path": "C:\\Users\\operator\\repos\\implant\\x64\\Release\\implant.pdb",
    "sections": [".text", ".data", ".rdata"],
}

The analyzer should flag pdb_path_present and reflective_export_name as OPSEC indicators.


5. Run the OPSEC linter (Lab 02)

cd ../lab-02-tooling-ioc-opsec-linter

# Verify the reference solution passes all 15 tests:
LAB_MODULE=solution python3 -m pytest -q

# Run the linter against the synthetic tool inventory:
python3 -c "
import solution, json

inventory = [
    {
        'name': 'beacon-default',
        'type': 'c2_agent',
        'named_pipe': 'MSSE-4abc-server',
        'user_agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)',
        'beacon_uri': '/api/',
        'sleep_seconds': 60,
        'jitter_percent': 0,
        'imphash': 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6',
        'pdb_path_stripped': False,
        'pdb_path': 'C:\\\\build\\\\implant.pdb',
    }
]
known_bad_db = {}   # no known-bad imphash entries for this fictional tool
results = solution.lint(inventory, known_bad_db)
for r in results:
    print(json.dumps(r, indent=2))
"

Expected output: findings ranked HIGH→MEDIUM→LOW with detection descriptions:

  • HIGH: default_named_pipe — "MSSE-* pattern detected by Sysmon EID 17 + Sigma rule"
  • HIGH: default_user_agent — "matches known Cobalt Strike default; proxy log detection"
  • MEDIUM: pdb_path_present — "PDB path not stripped; extract with pefile/strings"
  • MEDIUM: default_sleep_nojitter — "periodic 60s beacon; netflow periodic-connection detection"
  • LOW: default_beacon_uri — "/api/ URI detected by proxy rule"

The noise score for this tool should be at least: 3 (named_pipe HIGH) + 3 (user_agent HIGH) + 2 (pdb MEDIUM) + 2 (sleep MEDIUM) + 1 (uri LOW) = 11.


6. Write the Sigma rule for the highest-noise finding

The highest-noise finding for the synthetic tool is default_named_pipe. Write the detection:

title: Default C2 Named Pipe — Known Pattern (MSSE)
id: f1e2d3c4-b5a6-7890-fedc-ba9876543210
status: experimental
description: >
  A named pipe matching the default Cobalt Strike pipe-name pattern (MSSE-*-server) was created.
  This indicates use of a C2 agent with the default profile. Should be customized in production;
  this default is detected by multiple detection platforms.
references:
  - https://attack.mitre.org/techniques/T1071/
logsource:
  product: windows
  category: pipe_created
detection:
  selection:
    PipeName|startswith: '\MSSE-'
  condition: selection
falsepositives:
  - None expected; this specific pattern is not used by legitimate software.
level: high
tags:
  - attack.command_and_control
  - attack.t1071

Load this rule into your Wazuh or Elastic detection engine. To verify it fires:

  • On your range Windows VM, you can create a named pipe with a matching name using a PowerShell one-liner (for range-only testing): [System.IO.Pipes.NamedPipeServerStream]::new("MSSE-test-server")
  • Check Sysmon EID 17 in the event log for the pipe creation event.
  • Confirm the Sigma rule generates an alert in your SIEM.

This demonstrates the detection fires on the pipe name, not on a binary hash — it survives any recompilation of the C2 agent as long as the profile default is unchanged.


7. The evidence packet

Collect and store in the range (not published):

  • The dumpbin / pefile / readelf output for the binary you analyzed.
  • The translated dict used as lab input.
  • The Lab 01 analyzer output (file type, imphash, OPSEC flags).
  • The Lab 02 linter output (ranked findings, noise score).
  • The Sigma rule file.
  • A screenshot or log of the Sysmon EID 17 event (the pipe-creation detection firing on range).
  • A one-paragraph narrative: which binary was analyzed, which OPSEC findings were highest-impact, which detection was written, and what the detection keys on (the pipe name, not the hash).

This is the Phase 02 portfolio artifact: the FIN-LATTICE tooling triage for Operation Cedar Lattice.


8. Teardown

  • Remove pefile from any system Python install (uninstall from the range venv, not a shared env).
  • Delete the translated dicts and any binary copies that are not part of the evidence packet.
  • If you ran the synthetic pipe-creation test on the Windows VM, close and remove the pipe server.
  • Revert the range VM to its snapshot for the next exercise.

Common false claims

"I analyzed a real implant." In this exercise, you analyzed a legitimate system binary (notepad.exe, python3) and a synthetic dict. The honest claim: "I applied the PE/ELF triage methodology to a known-good binary, observing the same structural fields that appear in any binary, and demonstrated the OPSEC linter against a fictional tool configuration."

"A clean OPSEC linter score means the tool is undetectable." The linter checks default-indicator hygiene. It does not assess behavioral (TTP-level) detectability. A perfectly hygiene-cleaned tool is still detectable via ETW-TI, kernel callbacks, and parent-child analysis. State this limitation in any OPSEC assessment you write.

"imphash detection is a meaningful control." It is a triage accelerator in DFIR; it is not a production detection control. The detection that matters is the behavioral IOC at the TTP layer. If a client asks whether to implement imphash-based blocking, the honest answer is: "it adds one recompile of friction; invest in behavioral detections at the same time."

"The Sysmon EID 17 rule is all we need." A named-pipe rule detects the default profile; a profile change evades it. The complete detection stack for a C2 agent includes the pipe rule (fast, brittle) plus the behavioral connection-from-unusual-process rule (slower, more durable) plus JA3 fingerprinting at the network layer (durable for a given TLS configuration). Document all three and their durability ratings.