Skip to content

Lesson 9 of 9 · 25m

Writing IOCs an IR Team Can Actually Use

The pyramid of pain, indicators with role, confidence, and expiry, what to leave out, behavior detections as Sigma, and the handoff that gets acted on at 2 a.m.

Your deliverable isn't the analysis — it's what IR does with it at 2 a.m. while three other alerts fire. I've watched a raw sandbox indicator list pushed straight to the firewall: it included time.windows.com, www.msftconnecttest.com, and the lab's INetSim address 10.0.0.1, which overlapped a production subnet. The malware kept running; the finance VLAN didn't. Indicators without context are a liability.

The pyramid of pain

Pyramid of Pain

David Bianco's pyramid ranks indicators by the cost a detection imposes on the attacker: hashes (trivial to change) → IPs (easy) → domains (simple) → network/host artifacts (annoying) → tools (challenging) → TTPs (tough). Ship the whole stack: hashes answer "is it on any box right now"; artifacts and behaviors are what still fire after tomorrow's rebuild.

Every indicator carries context

type,value,role,status,confidence,first_seen,expires,how_to_look
sha256,7f3a9c41e2b8...7c9b,packed dropper invoice_0412.exe,observed,high,2024-03-18,never,EDR file hash search
domain,cdn-telemetry-update[.]top,primary C2,observed,high,2024-03-18,2024-06-18,DNS + proxy logs
ip,203.0.113[.]47,backup C2 :8443,configured,medium,2024-03-18,2024-04-18,check for shared hosting first
mutex,Kx7_SvcUpd_7731,host artifact,observed,high,2024-03-18,never,EDR mutex/handle query

Role tells IR what a hit means. Status separates what you saw from what you extracted (lesson 8). Expiry keeps the block list sane: IPs get reassigned in weeks, domains live longer, hashes never expire but stop mattering after a rebuild.

What to leave out

Sandbox noise (the INetSim IP, connectivity checks, CDN domains, your VM's hostname); legitimate services used as C2 — hand over the URL path or token, not the domain; and hashes of abused LOLBins — blocking rundll32.exe is a career-limiting move. A small normalizer helps:

const raw = `hxxps://cdn-telemetry-update[.]top/api/v2/gate.php\nCDN-TELEMETRY-UPDATE[.]TOP\n203.0.113[.]47\n10.0.0.1\nwww.msftconnecttest.com`;
const noise = [/^10\./, /^192\.168\./, /(^|\.)msftconnecttest\.com$/];
const refang = s => s.trim().replace(/\[\.\]/g, ".").replace(/^hxxp/i, "http");
const host = s => (/^https?:\/\//i.test(s) ? new URL(s).hostname : s).toLowerCase();
const kept = [...new Set(raw.split("\n").map(refang).map(host))].filter(h => !noise.some(rx => rx.test(h)));
console.log(kept.map(h => h.replace(/\./g, "[.]")));   // deduped, noise dropped, re-defanged for the report

Behavior beats hashes: hand over a detection

title: Rundll32 Running DllRegisterServer From Users Public
status: experimental
description: Kestrel loader stage 2 (CASE-2291) - rundll32 loading a DLL from C:\Users\Public
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\rundll32.exe'
    CommandLine|contains|all: ['\Users\Public\', 'DllRegisterServer']
  condition: selection
falsepositives:
  - None seen in 30 days of fleet process telemetry
level: high

Field notes

  • Put "how to look" next to "what to look for" — the EDR query is worth more than the string.
  • Lead the report with a three-line TL;DR: what it is, what it does, what to do now. The detail is for whoever asks.
  • Export STIX 2.1 / MISP for the TIP ([file:hashes.'SHA-256' = '7f3a...']), but the human-readable table is what actually gets read.
  • Say what you didn't check — "did not test with admin privileges" saves IR from false confidence.