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, anddumpbinto 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
- 1. Read a PE header with
dumpbin/pefile - 2. Read an ELF header with
objdump/readelf - 3. Translate the output into the lab dict format
- 4. Run the format analyzer (Lab 01)
- 5. Run the OPSEC linter (Lab 02)
- 6. Write the Sigma rule for the highest-noise finding
- 7. The evidence packet
- 8. Teardown
- Common false claims
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,pefilePython 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 listopsec_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/readelfoutput 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
pefilefrom 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.