IOC Analysis
Indicators of Compromise (IOCs) are forensic artifacts that identify potentially malicious activity. Learn to analyze, correlate, and hunt for IOCs to detect and respond to threats.
IOC Lookup Tool
Search hashes, IPs, domains, and URLs across multiple threat intel sources
Examples (click to try):
The Pyramid of Pain
IOC Types & Pyramid of Pain
Pyramid of Pain
Source: David J. Bianco's Pyramid of Pain - The higher up the pyramid, the more pain for adversaries when you detect their indicators.
Hash-Based IOCs
File hashes uniquely identify malicious files but are easily defeated by minor modifications.
Calculating Hashes
# Windows PowerShell
Get-FileHash -Algorithm MD5 malware.exe
Get-FileHash -Algorithm SHA256 malware.exe
# Linux
md5sum malware.exe
sha256sum malware.exe
# Multiple hashes at once
sha256sum * | tee hashes.txt
# Python
import hashlib
with open('malware.exe', 'rb') as f:
md5 = hashlib.md5(f.read()).hexdigest()
print(f"MD5: {md5}")Hash Lookup Services
# VirusTotal API lookup
curl -s "https://www.virustotal.com/api/v3/files/SHA256_HASH" \
-H "x-apikey: YOUR_API_KEY" | jq '.data.attributes.last_analysis_stats'
# MalwareBazaar lookup
curl -s -X POST "https://mb-api.abuse.ch/api/v1/" \
-d "query=get_info&hash=SHA256_HASH"
# Hybrid Analysis
curl -s "https://www.hybrid-analysis.com/api/v2/search/hash" \
-H "api-key: YOUR_API_KEY" \
-d "hash=SHA256_HASH"Fuzzy Hashing (SSDEEP)
Fuzzy hashes detect similar files even with modifications. Useful for tracking malware families.
# Generate SSDEEP hash
ssdeep malware.exe
# 384:H4lg5xF5zHNzP8DznYZrBPG3qG8qYGLr3:H4WFNEP8pYZrBPG3qG8qYGw
# Compare two files
ssdeep -d sample1.exe sample2.exe
# Compare against a hashset
ssdeep -m known_hashes.txt sample.exe
# Generate hashes for a directory
ssdeep -r /samples/ > ssdeep_hashes.txtNetwork-Based IOCs
IP Address Analysis
# WHOIS lookup
whois 192.168.1.1
# Reverse DNS
dig -x 192.168.1.1
nslookup 192.168.1.1
# Geolocation
curl -s "http://ip-api.com/json/8.8.8.8" | jq
# AbuseIPDB check
curl -s "https://api.abuseipdb.com/api/v2/check" \
-H "Key: YOUR_API_KEY" \
-G -d "ipAddress=192.168.1.1" | jq
# Shodan lookup
shodan host 192.168.1.1
# GreyNoise
curl -s "https://api.greynoise.io/v3/community/192.168.1.1" | jqDomain Analysis
# DNS records
dig ANY example.com
dig +short example.com A AAAA MX NS TXT
# Historical DNS (PassiveTotal/RiskIQ)
curl -s "https://api.passivetotal.org/v2/dns/passive" \
-u "user:apikey" \
-d '{"query": "example.com"}'
# Subdomain enumeration
subfinder -d example.com
amass enum -d example.com
# URLhaus check
curl -s -X POST "https://urlhaus-api.abuse.ch/v1/host/" \
-d "host=example.com"
# Domain age check (WHOIS creation date)
whois example.com | grep -i "creation date"URL Analysis
# URLScan.io submission
curl -s "https://urlscan.io/api/v1/scan/" \
-H "API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "http://suspicious-site.com", "visibility": "public"}'
# Check URL against URLhaus
curl -s -X POST "https://urlhaus-api.abuse.ch/v1/url/" \
-d "url=http://suspicious-site.com"
# Google Safe Browsing
curl -s "https://safebrowsing.googleapis.com/v4/threatMatches:find?key=API_KEY" \
-H "Content-Type: application/json" \
-d '{"client":{"clientId":"test"},"threatInfo":{"threatTypes":["MALWARE"],"platformTypes":["ANY_PLATFORM"],"threatEntryTypes":["URL"],"threatEntries":[{"url":"http://suspicious-site.com"}]}}'YARA Rules
YARA is the pattern matching swiss army knife for malware researchers. Create rules to identify malware families based on strings, byte patterns, and file characteristics.
Basic YARA Rule Structure
rule ExampleMalware {
meta:
author = "Analyst Name"
description = "Detects ExampleMalware family"
date = "2024-01-01"
reference = "https://example.com/report"
hash = "abc123..."
strings:
$string1 = "malicious_string" ascii wide
$string2 = { 4D 5A 90 00 } // MZ header
$regex1 = /http:\/\/[a-z0-9]+\.evil\.com/
$b64_config = "YWRtaW4==" base64
condition:
uint16(0) == 0x5A4D and // PE file
filesize < 500KB and
(2 of ($string*) or $regex1)
}Advanced YARA Patterns
rule Cobalt_Strike_Beacon {
meta:
description = "Detects Cobalt Strike Beacon payloads"
strings:
// Default Cobalt Strike watermark
$watermark = { 00 00 00 00 00 00 00 00 00 00 00 00 }
// Common strings
$s1 = "%s (admin)" ascii
$s2 = "beacon.dll" ascii
$s3 = "ReflectiveLoader" ascii
// Config markers
$config = { 00 01 00 01 00 02 }
// Sleep mask
$sleep = { 4C 8B DC 49 89 5B 08 49 89 6B 10 }
condition:
uint16(0) == 0x5A4D and
(
2 of ($s*) or
($config and $watermark) or
$sleep
)
}
rule Mimikatz_Memory {
meta:
description = "Detects Mimikatz in memory"
strings:
$s1 = "sekurlsa::" ascii wide
$s2 = "kerberos::" ascii wide
$s3 = "lsadump::" ascii wide
$s4 = "gentilkiwi" ascii wide
$s5 = "mimikatz" ascii wide nocase
condition:
3 of them
}Running YARA Scans
# Scan a single file
yara rules.yar suspicious_file.exe
# Scan a directory recursively
yara -r rules.yar /path/to/scan/
# Scan with multiple rule files
yara rule1.yar rule2.yar target.exe
# Output matching strings
yara -s rules.yar target.exe
# Scan process memory (Linux)
yara -p rules.yar 1234
# Timeout for large files
yara -t 60 rules.yar large_file.bin
# Python YARA scanning
import yara
rules = yara.compile(filepath='rules.yar')
matches = rules.match('/path/to/file')
for match in matches:
print(f"Matched: {match.rule}")Sigma Rules
Sigma is a generic signature format for SIEM systems. Write once, convert to any SIEM query language.
title: Suspicious PowerShell Download
id: 3b6ab547-8ec2-4991-b9d2-2b06702a48d7
status: experimental
description: Detects PowerShell download cradle patterns
author: Security Team
date: 2024/01/01
references:
- https://attack.mitre.org/techniques/T1059/001/
logsource:
category: process_creation
product: windows
detection:
selection_img:
- Image|endswith: '\powershell.exe'
- OriginalFileName: 'PowerShell.EXE'
selection_cli:
CommandLine|contains|all:
- 'Net.WebClient'
- 'DownloadString'
condition: all of selection_*
falsepositives:
- Legitimate scripts downloading content
level: high
tags:
- attack.execution
- attack.t1059.001
- attack.command_and_control
- attack.t1105# Convert Sigma to various formats
# Install: pip install sigma-cli
# Convert to Splunk
sigma convert -t splunk -p splunk_windows rule.yml
# Convert to Elastic/EQL
sigma convert -t lucene -p ecs_windows rule.yml
# Convert to Microsoft Sentinel
sigma convert -t azure_monitor rule.yml
# Convert to SIEM format with pipeline
sigma convert -t splunk -p splunk_windows -p windows_audit rules/*.yml
# Validate rules
sigma check rule.ymlThreat Hunting Queries
Splunk Hunting Queries
# Hunt for encoded PowerShell
index=windows EventCode=4688
| where like(CommandLine, "%powershell%") AND (like(CommandLine, "%-enc%") OR like(CommandLine, "%-e %") OR like(CommandLine, "%base64%"))
| stats count by Computer, User, CommandLine
# Unusual parent-child processes
index=windows EventCode=4688
| eval parent_child=ParentProcessName."->"ProcessName
| where NOT match(parent_child, "explorer.exe->cmd.exe|svchost.exe->msiexec.exe")
| rare parent_child
# LSASS access detection
index=windows EventCode=4663 ObjectName="*lsass*"
| stats count by Computer, ProcessName, AccessMask
| where ProcessName!="System"
# Suspicious scheduled tasks
index=windows (EventCode=4698 OR EventCode=4702)
| where like(TaskContent, "%powershell%") OR like(TaskContent, "%cmd.exe%")
| table _time, Computer, TaskName, TaskContentElastic/KQL Queries
// Encoded PowerShell detection
event.code: "4688" and process.command_line: (*powershell* AND (*-enc* OR *-e * OR *base64*))
// Potential credential dumping
event.code: "10" and winlog.event_data.TargetImage: *lsass.exe and
not process.executable: (*\Windows\System32\* OR *\Windows\SysWOW64\*)
// Lateral movement via WMI
event.code: "1" and process.parent.name: "wmiprvse.exe" and
process.name: ("cmd.exe" OR "powershell.exe")
// Persistence via registry run keys
registry.path: *\CurrentVersion\Run* and
event.action: "modification" and
registry.data.strings: (*powershell* OR *cmd* OR *.exe)Microsoft Defender/KQL
// Suspicious process injection
DeviceEvents
| where ActionType == "CreateRemoteThreadApiCall"
| where InitiatingProcessFileName !in~ ("csrss.exe", "lsass.exe", "services.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine
// PowerShell web requests
DeviceProcessEvents
| where FileName =~ "powershell.exe"
| where ProcessCommandLine has_any ("WebClient", "Invoke-WebRequest", "wget", "curl", "DownloadString")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine
// Potential Cobalt Strike activity
DeviceNetworkEvents
| where RemotePort in (80, 443, 8080, 8443)
| where InitiatingProcessFileName in~ ("rundll32.exe", "regsvr32.exe", "mshta.exe")
| summarize count() by InitiatingProcessFileName, RemoteIP, RemotePortIOC Management
STIX/TAXII Format
{
"type": "indicator",
"spec_version": "2.1",
"id": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade",
"created": "2024-01-01T00:00:00.000Z",
"modified": "2024-01-01T00:00:00.000Z",
"name": "Malicious IP Address",
"description": "C2 server used by APT group",
"indicator_types": ["malicious-activity"],
"pattern": "[ipv4-addr:value = '192.168.1.100']",
"pattern_type": "stix",
"valid_from": "2024-01-01T00:00:00.000Z",
"kill_chain_phases": [
{
"kill_chain_name": "mitre-attack",
"phase_name": "command-and-control"
}
],
"labels": ["c2", "apt"]
}OpenIOC Format
<?xml version="1.0" encoding="UTF-8"?>
<ioc xmlns="http://openioc.org/schemas/OpenIOC_1.1"
id="12345678-1234-1234-1234-123456789012">
<short_description>Malware Family XYZ</short_description>
<authored_by>Security Team</authored_by>
<authored_date>2024-01-01T00:00:00</authored_date>
<definition>
<Indicator operator="OR">
<IndicatorItem condition="is">
<Context document="FileItem" search="FileItem/Md5sum"/>
<Content type="md5">d41d8cd98f00b204e9800998ecf8427e</Content>
</IndicatorItem>
<IndicatorItem condition="contains">
<Context document="ProcessItem" search="ProcessItem/name"/>
<Content type="string">malware.exe</Content>
</IndicatorItem>
</Indicator>
</definition>
</ioc>IOC Enrichment Pipeline
Evidence to Capture
An indicator without age, source and confidence cannot be operationalised — it can only be pasted into a blocklist.
Indicator value and type:
The value canonicalised and defanged for transport, such as hxxps:// and example[.]com, with its type recorded as hash, domain, IP or URL.
First and last seen: Both timestamps from the reporting source, plus when your own telemetry first observed it. Age decides whether a match means anything.
Source and confidence: Who published it, the reliability grade of that source, and the confidence the publisher stated in its own words.
Context of use: The kill-chain phase and the malware family or campaign it belongs to. An IP address on its own is a number.
Match evidence: For a hit: host, timestamp, process and the full command line or HTTP request, exported from the sensor rather than summarised by hand.
Retention decision: The expiry or review date applied, and the reason the indicator was aged out or deliberately kept.
Likely False Positives
Indicator matching is cheap, which is why most IOC alerts are noise. These are the recurring causes.
- Sinkholed and parked infrastructure: A domain now resolving to a takedown sinkhole or a parking page was malicious once. Matching it today produces noise, not a lead.
- Shared hosting and CDN ranges: Cloud IPs, CDN egress and SaaS gateways carry thousands of tenants. A match against a shared range proves nothing.
- Legitimate service abuse: Attackers use code hosting, file sharing, paste sites and messaging platforms. Blocking the service domain flags every employee who uses it legitimately.
- Expired indicators: Command-and-control domains rotate within days. A feed entry past its TTL is historical context, not an alert.
- Benign tooling hashes: Signed administrative and forensic tools appear in feeds constantly. A hash match on a tool your own team deployed is expected.
- Fuzzy hash overshoot: SSDEEP and import hash similarity group whole families, including samples that never targeted you. Similarity is clustering, not detection.
- Scanner and researcher traffic: Mass scanners and internet-wide research sweeps appear in every feed. A scan is not an intrusion.
Indicator Handling Checklist
Before operationalising
- ☐ Type and canonical value recorded
- ☐ First and last seen captured from the source and from your telemetry
- ☐ Source reliability and stated confidence recorded
- ☐ Context captured: phase, family or campaign
- ☐ TTL or review date assigned by indicator type
On a match
- ☐ Sensor log exported with host, timestamp and process
- ☐ Event time compared against indicator age
- ☐ Shared-hosting, CDN and service allowlist consulted
- ☐ Sinkhole or takedown status checked
- ☐ Benign-tool overlap ruled out before escalation
Collection and Precision
Indicator work has no patch step. The corrective action is ageing indicators honestly, enriching before blocking, and keeping the source of every value.
Age and prefer behaviour
- • Set TTL by indicator type: hours for C2 IPs, weeks for hashes, longer for infrastructure providers
- • Retire indicators automatically and keep the retirement reason
- • Prefer behavioural detections over static values below the TTP level
Enrich and attribute
- • Enrich before blocking: ASN, first seen, signature and current resolution
- • Allowlist shared hosting and service domains once, instead of tuning each rule
- • Record the contributing source per indicator so a bad feed can be traced and dropped
Review trigger
- • A publisher retracts or corrects an indicator
- • A match resolving to an allowlisted service or an internal host
- • An indicator still alerting past its TTL