CoursesDetection engineeringTTPs & the Pyramid of Pain

TTPs & the Pyramid of Pain

Detect behavior, not disposable indicators.

Advanced30 min · lesson 8 of 15

A burglar can swap gloves in a second. Making them leave no fingerprints costs them nothing, so they do it without thinking twice. Now make them give up the one trick they know for popping a window latch. That is expensive. They have to relearn the job. Detection engineering has exactly this shape. An indicator of compromise (IOC), meaning one specific artifact tied to one attack such as a file hash, an IP address or a domain name, is the glove. A tactic, technique or procedure (TTP), meaning the way an attacker actually gets the job done, is the craft. Detect the glove and they change gloves. Detect the craft and they have to change how they work. This lesson is about moving your detections up from the gloves to the craft, and then proving they fire.

The pyramid, one tier at a time

David Bianco published the Pyramid of Pain in 2013. It ranks the *types* of indicator you can build a detection on by how much pain, meaning cost and effort and lost time, you inflict on the attacker when you take that indicator away from them. Start at the bottom. Hash values are *trivial*: flip one byte, recompile, and the hash is brand new. IP addresses are *easy*: rotate through a cloud VPS (virtual private server, a rented machine you can throw away) or through Tor. Domain names are *simple*: register another one, or have the malware generate them algorithmically. Then you cross a line. Host and network artifacts are *annoying*, because they are the user-agent strings, named pipes, service names and registry keys the tooling leaves behind whether the operator wants it to or not. Tools are *challenging*, because now the attacker has to find or build a replacement for Mimikatz. At the top sit TTPs, rated *tough*, because that tier is the behavior itself. The three tiers above the line are where durable detections live. An artifact, a tool or a technique costs real time and real skill to change.

None of that makes hashes and IP addresses worthless. The claim is narrower. A detection keyed to a hash expires the moment the attacker changes anything at all, while a detection keyed to a technique survives them re-tooling completely. So put the bulk of your engineering hours near the top, where every rule you ship forces the adversary into an expensive rethink instead of a five-second swap.

The same threat, written two ways

The difference stops being abstract the moment you write both queries side by side. Below, one threat appears twice: an attacker dumping credentials out of LSASS (Local Security Authority Subsystem Service, the Windows process that holds logon secrets in memory). Both versions are KQL (Kusto Query Language), the language behind Microsoft Defender and Sentinel advanced hunting. The brittle version pins itself to a hash and an IP address. The durable version pins itself to the behavior described by MITRE ATT&CK (Adversarial Tactics, Techniques and Common Knowledge, the public catalog of attacker behavior) technique T1003.001: a non-system process opening a handle to lsass.exe. Read the result rows under the durable query. One attacker used procdump. The other used a comsvcs.dll living-off-the-land trick, meaning they abused a component Windows already ships instead of bringing their own tool. One technique-level rule caught both. That is the pyramid paying off.

defender-hunting.kql
// BRITTLE — bottom of the pyramid (Hash / IP). Returns 0 rows the instant
// the attacker recompiles or moves to a new server.
DeviceFileEvents
| where SHA256 == "a3f0c7e1b28d...9f" // one recompile -> new hash -> no match
DeviceNetworkEvents
| where RemoteIP == "203.0.113.5" // one new VPS -> no match
// DURABLE — top of the pyramid (TTP: T1003.001 OS Credential Dumping: LSASS Memory).
// Detect the behavior: a non-system process opening a handle to lsass.exe.
DeviceEvents
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| where InitiatingProcessFileName !in~ ("MsMpEng.exe","wininit.exe","csrss.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine
// Timestamp DeviceName InitiatingProcessFileName InitiatingProcessCommandLine
// 2026-07-13T09:14:22Z FIN-WKS-07 procdump64.exe procdump64.exe -ma lsass.exe out.dmp
// 2026-07-13T11:02:51Z DC-01 rundll32.exe rundll32 comsvcs.dll, MiniDump 640 c:\t.dmp full
// -> two different TOOLS, one TECHNIQUE, one rule catches both.

The bill for that resilience is visible in the same query. The durable version needs rich endpoint telemetry, specifically process API call events (the operating system's record of one program asking for access to another), which not every agent collects and not every agent forwards. It also needs an allow-list of the software that legitimately reads LSASS, or your own antivirus will bury you in matches. IOC queries are cheap and need almost no context around them. TTP queries buy durability with data volume and tuning time.

Fire the technique at yourself before you trust the rule

A smoke alarm nobody has ever tested is decoration. A detection you have never watched fire is a hypothesis, not a control. Atomic Red Team is an open-source library from Red Canary of small, self-contained tests, each one mapped to an ATT&CK technique, and it exists so you can reproduce a behavior safely on a lab host and check whether your rule wakes up. The runner is a PowerShell module called invoke-atomicredteam. Run the atomic for T1003.001, watch your KQL rule light up, and you have closed the purple-team loop (red-team behavior and blue-team detection checked against each other): emulate, detect, verify.

atomic-emulate.ps1
# Install once (Red Canary's runner + YAML parser)
Install-Module -Name invoke-atomicredteam, powershell-yaml -Scope CurrentUser -Force
Import-Module invoke-atomicredteam
# What does technique T1003.001 actually test?
Invoke-AtomicTest T1003.001 -ShowDetailsBrief
# PathToAtomicsFolder = C:\AtomicRedTeam\atomics
# T1003.001-1 Dump LSASS.exe Memory using ProcDump
# T1003.001-2 Dump LSASS.exe Memory using comsvcs.dll
# T1003.001-3 Dump LSASS.exe Memory using direct system calls and API unhooking
# Confirm the test can run here, then fire test #1 in your LAB only
Invoke-AtomicTest T1003.001 -TestNumbers 1 -CheckPrereqs
# Prerequisites met: T1003.001-1 Dump LSASS.exe Memory using ProcDump
Invoke-AtomicTest T1003.001 -TestNumbers 1
# Executing test: T1003.001-1 Dump LSASS.exe Memory using ProcDump
# Done executing test: T1003.001-1 Dump LSASS.exe Memory using ProcDump
# Always remove the artifacts the test dropped (the .dmp file, etc.)
Invoke-AtomicTest T1003.001 -TestNumbers 1 -Cleanup

Within seconds, your OpenProcessApiCall query should return the emulation host. Be precise about what that proves. A detection firing is a claim that something matched your logic; an incident is a human confirming the match was real and mattered. Here you already know the answer, because you caused it. If nothing comes back at all, you have found a gap, and that gap is the most useful thing you will produce all week. Maybe the telemetry from that host was never onboarded. Maybe your allow-list is so broad it swallowed the test. Either answer beats a rule that reads convincingly in review and has never once fired.

Tag every rule, then count what you actually cover

MITRE ATT&CK files adversary behavior the way a library files a book by subject, then title, then edition. Tactics are the goal (Credential Access, TA0006). Techniques are how the goal gets reached (OS Credential Dumping, T1003). Sub-techniques are a specific variant (T1003.001, LSASS Memory). Procedures are one real crew's real implementation. Tag every detection you write with its technique ID and the fuzzy question "are we covered?" becomes arithmetic: for each technique that matters in your environment, is there a rule, and has it ever fired? Here is a five-second coverage tally across a Sigma rule repository (Sigma being the vendor-neutral rule format you convert into whatever your SIEM, or security information and event management platform, actually speaks), followed by an ID-to-name lookup with the current mitreattack-python library so you can put a name on the gap you found.

attack-coverage.sh
# How many unique ATT&CK techniques do our Sigma rules cover today?
grep -rhoE 'attack\.t[0-9]{4}(\.[0-9]{3})?' rules/ | sort -u | wc -l
# 147
# Which Credential Access sub-techniques under T1003 do we actually detect?
grep -rhoE 'attack\.t1003(\.[0-9]{3})?' rules/ | sort | uniq -c
# 9 attack.t1003.001 <- LSASS Memory (well covered)
# 2 attack.t1003.002 <- Security Account Manager (SAM)
# (nothing for t1003.003 NTDS.dit -> that is your coverage gap)
# Resolve the gap's ID to a human name so you can prioritise it
pip install mitreattack-python
python - <<'PY'
from mitreattack.stix20 import MitreAttackData
mad = MitreAttackData("enterprise-attack.json") # download once from MITRE CTI
print(mad.get_object_by_attack_id("T1003.003", "attack-pattern").name)
# NTDS
PY

Feed those technique IDs into ATT&CK Navigator and you get a heat-map layer leadership can read at a glance. Then overlay DeTT&CT, which scores how much visibility your data sources give you, because an uncovered technique often has nothing to do with a missing rule. Sometimes the log you would need never reaches you in the first place. Telling those two failures apart is what separates a real coverage program from a rule count.

Credential-dumping atomics are live fire, so ring-fence them
Test T1003.001 writes a genuine LSASS memory dump to disk, and your own EDR (endpoint detection and response) or antivirus will treat it exactly like an attack. It may quarantine the dump file mid-run, which corrupts your result, or page the SOC (security operations centre), which turns a Tuesday afternoon test into a 2 a.m. incident. Never run credential-access atomics on a production host. Use an isolated lab or a ring-fenced VM (virtual machine), run -CheckPrereqs before and -Cleanup after every single time, and tell the SOC in advance which host, which technique ID and which time window, so a genuine intrusion during your test is never written off as your test.
The Pyramid of Pain: cost to the adversary when you deny an indicator
Below the line · disposable IOCs: a detection here expires the moment the attacker changes anything
Hash values (trivial)
Flip one byte, recompile → brand-new hash
IP addresses (easy)
Rotate through cloud VPS or Tor
Domain names (simple)
Register a new one, or generate them algorithmically (DGA)
Above the line · durable detections: each one costs the adversary real time and skill to change
Host / network artifacts (annoying)
User-agents, named pipes, service names, registry keys the tooling leaves
Tools (challenging)
Force the attacker to find or build a Mimikatz / ProcDump replacement
TTPs (tough)
The behavior itself, so evading it means changing how they operate
The pyramid ranks durability, not detectability, and the tiers are not strictly ordered (a rare artifact can hurt more than a generic technique). Spend your engineering hours above the line, where each rule forces an expensive rethink, and keep cheap, high-confidence IOC blocking at the perimeter.

What climbing the pyramid costs you

Climbing is not free. TTP detections want richer and pricier telemetry (full process command lines, API calls, authentication events) and far more tuning, because an administrator doing their job and an attacker doing theirs often look identical at the event level. Top-tier rules trend noisy and need behavioral context around them before they earn the right to page anyone. The pyramid ranks durability, not detectability: some techniques are effectively invisible unless you collect one specific data source, which is why a coverage gap so often turns out to be a telemetry gap. The tiers are not a strict ordering either. A rare host artifact used by exactly one crew can hurt more than a generic technique thousands of people use. The grown-up posture is layered. Keep cheap, high-confidence IOC blocking at the perimeter, because a known-bad hash should still be denied instantly, and spend your detection-engineering hours further up, where each rule you ship forces the adversary to change how they operate rather than what they carry. For cert prep, memorise Bianco's six tiers and their pain labels (trivial, easy, simple, annoying, challenging, tough) and be ready to place any indicator you are handed on the right one.

Signatures have a ceiling. Even a flawless T1003.001 rule matches one known shape of the technique. An attacker who dumps LSASS with a custom driver instead of ProcDump, or who spreads malicious logons across a fortnight so no single day crosses any threshold, leaves no signature to match. What they leave is a statistical residue, like an account suddenly touching a server it has never touched in two years of history. Catching residue means learning what normal looks like first, then flagging the deviation. The next lesson, Behavioral analytics (UEBA), short for user and entity behavior analytics, builds those baselines. That is the layer that catches the technique variants nobody ever wrote a rule for.

Treat the whole thing as a portfolio decision, the way you would weight a set of investments. Hashes and IP addresses earn their keep in blocking and in fast response, and they should still not dominate your detection backlog. Score every candidate rule by what it would cost the adversary to evade it, because tool-marking and TTPs beat domain lists every time. When a vendor offers you "ten thousand IOC feed integrations", ask which of those map to a behavior you can still catch after the feed goes stale. Plenty of IOC pipelines produce impressive alert volume and never cost the attacker an hour. Alert volume was never a success metric.

Try this

Take three rules that page a human today and put each one on a tier: hash, IP or domain at the bottom; tool in the middle; TTP at the top. Be honest about where they land. Then pick the most IOC-heavy of the three and rewrite its idea as a behavior-focused Sigma condition, and write the tier and the eviction cost into the pull request so a reviewer can argue with your reasoning instead of guessing at it.

terminal
# Before (IOC-ish): alert on a specific payload hash or C2 IP
# After (TTP-ish): alert on certutil download behavior
$ sigma convert -t splunk -p sysmon rules/win_certutil_downloader.yml
source="WinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
Image="*\\certutil.exe" CommandLine="*urlcache*" CommandLine="*split*"
NOT ParentImage="*\\CcmExec.exe"
# Checklist output you should write in the PR:
# Pyramid tier: TTP/tool | ATT&CK: T1105 | Eviction cost: high (must change tradecraft)

Takeaway

Gloves come off in a second. Craft takes years to relearn. Push your detections up the Pyramid of Pain so an attacker has to change how they operate, not the file they happen to be carrying.

Next step: find one alert that pages on a hash or an IP address, demote it to intel context so it still enriches an investigation without waking anyone at 3 a.m., and hand its paging path to a behavior rule you can prove with an Atomic Red Team test.

Quick check
01You ship the durable T1003.001 rule, which flags any non-system process that opens a handle to lsass.exe, but you skip the allow-list of software that legitimately reads LSASS. What happens in production?
Correct — That is why the lesson's durable query excludes MsMpEng.exe, wininit.exe and csrss.exe. Without that allow-list your own security tooling buries you in matches.
Incorrect — That describes the brittle hash query, which returns no rows the instant the attacker recompiles. A behavior rule fires on the action whatever the hash is, so it over-matches rather than under-matches.
Incorrect — The behavior rule caught both variants in the lesson's own output rows. The allow-list controls noise; it does not decide which tools the technique rule matches.
Incorrect — The allow-list filters benign LSASS readers out of the results. Drop it and the rule returns constant legitimate matches, which is a fidelity problem, not a speed one.
02Your Sigma repository tally shows nine rules tagged attack.t1003.001 and nothing at all for attack.t1003.003. Before you sit down to write a new rule, what does the lesson tell you to check first?
Correct — That is why the lesson pairs ATT&CK Navigator heat-maps with DeTT&CT, which overlays data-source visibility. A rule written against telemetry you never receive covers nothing.
Incorrect — Wrong tier. A hash sits at the bottom of the pyramid and expires on the attacker's next recompile, so it tells you nothing about your coverage of the technique.
Incorrect — 147 counts the unique techniques your rules mention. It is a rule count, which is exactly the metric the lesson warns you not to mistake for coverage.
Incorrect — T1003.003 is a sub-technique of OS Credential Dumping, so it belongs to the technique tier at the top of the pyramid. Nothing about it sits below the line.
03On your lab host you run Invoke-AtomicTest T1003.001 -TestNumbers 1, the console prints "Done executing test: T1003.001-1 Dump LSASS.exe Memory using ProcDump", and your OpenProcessApiCall hunting query returns no rows for that host. What do you do?
Correct — The lesson names both suspects: telemetry that was never onboarded, or an allow-list so broad it swallowed the emulation. Finding that gap is worth more than a rule that only looks plausible on paper.
Incorrect — The runner is reporting that the atomic executed, not that anything detected it. The whole point of emulation is whether your rule saw the behavior, and it did not.
Incorrect — That drops your rule to the bottom of the pyramid, where one recompile defeats it, and it still leaves you not knowing why the behavior event never arrived.
Incorrect — Dangerous. Credential-access atomics write a genuine LSASS dump, trip your own EDR and can page the SOC. The lesson is explicit that these never run on a production host.

Related