Threat Intelligence

What Is an Indicator of Compromise (IOC)?

HuntRule Team · · 10 min read

A single latent fingerprint on a pane of glass, lit from the side in a dark room so only the ridges catch the light
On this page

A SHA-256 hash identifies exactly one file. Flip one byte in that file and the hash is gone, which is why hash-based blocking loses to a recompile. That single property explains most of what an indicator of compromise is good for and most of what it is not.

An IOC is evidence that something already happened. A hash of a binary that ran. An IP a beacon called. A registry value a loader wrote. It is a record of the past pointed at the present, which is both the whole value and the hard ceiling. IOCs answer "have we seen this" cheaply. They do not answer "would we catch it next time".

The types, and how long each one lasts

Every IOC type decays, but they decay at different rates and for different reasons.

Type

Example artifact

Where you see it

Practical shelf life

File hash

SHA-256 of a loader DLL

Sysmon EID 1 Hashes, EDR file events

One build. Minutes if the actor recompiles per target

IP address

C2 listener on a rented VPS

Firewall, proxy, Sysmon EID 3

Days to weeks, then reassigned to someone innocent

Domain

Registered a week before use

DNS logs, Sysmon EID 22, proxy

Weeks. Longer if it is a compromised legitimate site

URL

hxxp://<host>/aa.png staging path

Proxy, web gateway

Shortest of the network types. Paths rotate per campaign

Registry key

HKCU\...\CurrentVersion\Run value

Sysmon EID 12/13, EDR registry events

Months, because it is tied to the tooling not the infra

Mutex

Global\MsWinZonesCacheCounterMutexA0

Memory, process handle listings

Long. Often survives across malware versions

JA3 / JA4

TLS client fingerprint of the implant

Zeek ssl.log, Suricata TLS events

Months, and it survives an IP change

Certificate hash

SHA-256 of a C2 TLS certificate

Zeek x509.log, internet scan data

Until the operator regenerates the cert

The mutex in that table is WannaCry's. Didier Stevens documented that the shipped samples use Global\MsWinZonesCacheCounterMutexA0, with a zero on the end, rather than the MsWinZonesCacheCounterMutexA that early reporting listed. IOCs get copied between reports faster than they get verified.

Why hashes are the weakest useful thing you can share

David Bianco's Pyramid of Pain (2013) put hash values at the bottom for a reason. Changing a hash costs an attacker one build. Changing an IP costs a few dollars. Changing a TTP costs them a redesign.

Hashes still get shared more than anything else because they are unambiguous and legally safe. A hash cannot false positive on a colleague's home router the way an IP can. That makes them a fine confirmation artifact and a fine retro-hunt seed. It makes them a bad foundation for a programme, because a ruleset built on hashes has a coverage number that is really a percentage of files somebody else already saw. A hash tells you whether you were in a specific campaign. It never tells you whether you would survive the next one.

The operational failures nobody puts in the feed documentation

Three things break IOC programmes, and all three are process problems rather than data problems.

An IOC with no expiry poisons a blocklist forever. Nothing in a plain list of IPs says when it was observed or when to stop trusting it. Blocklists grow monotonically and nobody dares prune them, so a VPS that hosted a Cobalt Strike listener in 2023 is still blocked in 2026, three tenants later.

A shared IOC without context produces alerts nobody can triage. "IP 203.0.113.10, malicious" gives an analyst no way to set severity and no way to tell beaconing from a user clicking a link on a shared host. The alert gets closed as noise, and once a feed's alerts are closed in bulk the feed is dead whether or not it is accurate.

Sinkholed and reused infrastructure produces false positives long after the campaign ended. A sinkholed C2 domain resolves to a researcher's collector, so a hit is a real infection signal or a stale artifact depending on context the feed did not carry. Block a domain fronted by a large CDN and you have blocked a slice of the internet.

TLS fingerprints age better, with caveats

JA3 hashes five fields from the TLS ClientHello into an MD5: version, cipher list, extension list, elliptic curves, and curve point formats. JA3S does the same for the ServerHello with version, cipher and extensions. The fingerprint follows the implant's TLS stack rather than its infrastructure, so it survives an IP rotation.

Two things happened to JA3. Chrome began permuting ClientHello extension order, rolling out broadly from 20 January 2023 in Chrome 108 and 109 ahead of the Chrome 110 release it was scheduled for, which makes a browser's JA3 differ per connection. Salesforce archived the JA3 repository on 1 May 2025.

JA4, from FoxIO, sorts the cipher and extension lists before hashing, which removes ordering as a source of instability. The format is a_b_c, for example t13d1516h2_8daaf6152771_e5627efa2ab1. The readable first section decodes as TLS over TCP, TLS 1.3, SNI present as a domain, 15 ciphers, 16 extensions, ALPN h2. Sections b and c are 12-character truncated SHA-256 values over the sorted ciphers and over the extensions plus signature algorithms. Because the sections are delimited you can hunt on a and c alone.

Neither arrives for free. Zeek does not emit JA3 out of the box: it is a package, zkg install ja3, which adds ja3 and ja3s fields to ssl.log. Suricata has ja3.hash and ja4.hash keywords, but the fingerprints must be enabled under app-layer.protocols.tls in suricata.yaml, or loaded implicitly by a rule that uses them.

Certificates sit in the same family. The default Cobalt Strike team server certificate has serial 146473198, CN jquery.com, and SHA-256 87f2085c32b6a2cc709b365f55873e207a9caa10bffecf2fd16d3cf9d94d390c, and Elastic ships a prebuilt rule for it. JA4X fingerprints how a certificate was generated instead, hashing the issuer RDN keys, subject RDN keys and extension keys, which catches self-signed C2 certs whose field values are randomised per deployment.

How an IOC should enter a SIEM

Not as a bare string. Every indicator needs the metadata that lets you expire and triage it. STIX 2.1 already has the shape:

{
  "type": "indicator",
  "spec_version": "2.1",
  "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
  "created": "2026-06-14T09:12:00.000Z",
  "modified": "2026-06-14T09:12:00.000Z",
  "pattern_type": "stix",
  "pattern": "[file:hashes.'SHA-256' = 'a5f8...c31d']",
  "indicator_types": ["malicious-activity"],
  "valid_from": "2026-06-14T09:12:00.000Z",
  "valid_until": "2026-09-14T09:12:00.000Z",
  "confidence": 70
}

valid_until is the field most feeds omit and the one that decides whether your blocklist stays maintainable. If your intel platform will not carry an expiry, set one at ingest per type: short for URLs and IPs, longer for registry and mutex artifacts, longest for TLS and certificate fingerprints. Load indicators into a lookup, never into rule text. A rule that hardcodes an IP has to be edited and redeployed to expire it.

A single index card pulled halfway out of a long wooden card catalogue drawer in a dark archive

The retro-hunt

The one thing IOCs do better than any behavioural rule is answer a question about the past. Splunk against Sysmon network events, seeded from a lookup:

index=sysmon EventCode=3 earliest=-90d@d latest=now
    [| inputlookup c2_indicators.csv
     | search type=ipv4
     | fields value
     | rename value AS DestinationIp]
| stats min(_time) AS first_seen,
        max(_time) AS last_seen,
        dc(Computer) AS hosts,
        values(Image) AS processes
        BY DestinationIp
| convert ctime(first_seen), ctime(last_seen)
| sort - last_seen

Two limits before anyone reports the result upward. The query only sees as far back as your retention, so ninety days of search over thirty days of hot data quietly returns thirty days of answer. And a clean result is not proof of absence: it proves the indicator did not appear in that data source, in that window, on hosts that were reporting. Check dc(Computer) against your asset inventory first. That reporting-coverage check is the same one that gates a new detection, covered in detection engineering.

Deciding what to keep

Four questions per indicator. Anything failing the first two goes to hunt-only rather than into an alerting rule.

  1. Does it carry a first-seen timestamp and an expiry. No expiry, no blocklist entry.

  2. Does it carry enough context to write two sentences of runbook. If an analyst cannot decide what to do on a hit, the rule gets muted within a month.

  3. Would a hit be actionable, or ambiguous by construction. Shared hosting, CDN-fronted domains, Tor exit nodes and known sinkholes belong in enrichment, not in blocking.

  4. Is it tied to infrastructure or to tooling. Mutexes, named pipes, TLS fingerprints and service names outlive IPs and URLs by a wide margin.

From an IOC to a behavioural rule

Say a report gives you two indicators: a URL, hxxp://198.51.100.24/aa.txt, and the SHA-256 of the payload it served. Retro-hunt both. Assume no hits. Both are now worth almost nothing, because the host is burned and the payload will be rebuilt.

What is worth something is the sentence next to them in the report: the payload was fetched with certutil.exe. That is the behaviour, it maps to T1105 (Ingress Tool Transfer), and it is stable across campaigns because it belongs to the operator's tradecraft rather than to their hosting.

title: Certutil URL cache download
id: 3f1c2d54-9b0e-4a7c-a2f1-6b8d5e0c47aa
status: experimental
description: Detects certutil.exe fetching a remote file using the URL cache flags.
references:
    - https://lolbas-project.github.io/lolbas/Binaries/Certutil/
author: HuntRule
date: 2026-07-31
tags:
    - attack.command-and-control
    - attack.t1105
logsource:
    category: process_creation
    product: windows
detection:
    selection_img:
        - Image|endswith: '\certutil.exe'
        - OriginalFileName: 'CertUtil.exe'
    selection_flags:
        CommandLine|contains:
            - 'urlcache'
            - 'verifyctl'
    condition: all of selection_*
falsepositives:
    - Enterprise PKI scripts refreshing CRLs against an internal CA
    - Build and packaging scripts that fetch installers over HTTP
level: medium

What it catches: any use of certutil as a downloader, on any URL, for any payload. The two IOCs from the report no longer decide whether it fires. SigmaHQ ships an equivalent rule, proc_creation_win_certutil_download.yml, tagged the same way.

What it misses matters more. It needs command line logging, so Sysmon Event ID 1 or Windows Security 4688 with the command line auditing policy enabled. Without CommandLine populated the rule returns nothing and never tells you why. It misses a renamed certutil unless your telemetry populates OriginalFileName. It misses every other download method: curl.exe, bitsadmin, msiexec /i http://..., PowerShell. An operator who reads detection blogs will use one of those instead.

That is the honest trade. The IOC was precise and worthless a fortnight later. The rule is imprecise, needs tuning against your own PKI scripts, and keeps working.

Summary

An IOC is evidence of a past event, which makes it good for retro-hunting and poor as a detection strategy. Hashes are the most-shared and least-durable type, and a programme measured in hash counts is measuring somebody else's campaigns. The failures that kill IOC programmes are operational: no expiry, no context, no handling of sinkholed or reused infrastructure. Treat every indicator as a seed for a behavioural rule and keep it only long enough to write that rule. Browse behavioural rules by domain at /rules?domain=network, or search the catalog for certutil.

Techniques referenced: T1105 (Ingress Tool Transfer) for the certutil rule, T1547.001 (Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder) for the Run key artifact, T1573 (Encrypted Channel) for the TLS fingerprint section.

Minimum fields to carry on every indicator record:

value             the indicator itself
type              sha256 | ipv4 | domain | url | registry | mutex | ja3 | ja4 | cert_sha256
source            feed or report name, not "OSINT"
first_seen        ISO 8601
valid_until       ISO 8601, mandatory, set at ingest if the feed omits it
confidence        0-100
context           campaign or malware family, one line of triage guidance
disposition       block | alert | hunt_only | enrich_only

Telemetry to collect so those indicators are searchable at all:

Sysmon EID 1     process creation, with CommandLine and Hashes
Sysmon EID 3     network connection, with Image and DestinationIp
Sysmon EID 12/13 registry key and value set
Sysmon EID 22    DNS query, with QueryName and QueryResults
Zeek ssl.log     with the ja3 package installed (zkg install ja3)
Zeek x509.log    certificate issuer, subject and serial
Suricata         tls and quic events with ja3/ja4 fingerprints enabled

Related articles