Updated October 2026: ThreatFox now requires a free Auth-Key and returns at most the last seven days of IOCs.
Continuing our posts on why and how to alert on Indicators of Compromise with Suricata’s Datasets, this post shows how to alert on known-malicious files seen on the network, using SHA256 hashes published by abuse.ch ThreatFox.
But first: why use hashes as IOCs at all, when attackers can change them so easily?
Why hash IOCs are still relevant today
Relying only on malicious file hashes won’t make a solid defence, but as an extra detection angle they help, for three reasons.
1. Hash IOCs are everywhere
Most, if not all, commercial and free threat intelligence feeds publish hash IOCs. Traditionally they go into EDR and antivirus tools to block malicious files on endpoints. Suricata lets you do the same at the network level, which also covers servers, IoT devices and anything else without an EDR agent.
2. Not every attacker is that advanced
Once a system is on the internet, it will see threats of every kind. Some may come from a real APT, but most will be opportunistic attacks by criminals who reuse published malware and tools without changing their hashes. That’s a detection opportunity: attackers aren’t invincible, and we should use that.
3. Hashes have no false positives
Unlike IDS signatures, which can produce false positives for many reasons, a file’s hash either matches a known-bad hash or it doesn’t. As long as you trust your feed, a match means you saw that exact file.
How Suricata detects files by hash
Suricata extracts files from several protocols, including HTTP, HTTP/2, SMTP, FTP, SMB and NFS, and can hash them as they pass. Rules then match the hash with the filemd5, filesha1 and filesha256 keywords. Which one you use depends on the hashes your feeds publish. This post sticks to SHA256, today’s standard.
We used Ubuntu 24.04 with Suricata 8.0 (Ubuntu install guide), and ran the same steps on Suricata 7.0.
Before you start: get a ThreatFox Auth-Key
ThreatFox requires an Auth-Key header on every API request; without one, the API answers 401 Unauthorized. Keys are free: sign in to the abuse.ch Authentication Portal and generate one from your profile.
You’ll also need curl and jq:
sudo apt install -y curl jq
Preparing the hash list
Put your key in a variable, then download the last seven days of IOCs from the ThreatFox API:
export THREATFOX_AUTH_KEY='your-auth-key'
curl -s -X POST https://threatfox-api.abuse.ch/api/v1/ \
-H "Auth-Key: ${THREATFOX_AUTH_KEY}" \
-d '{ "query": "get_iocs", "days": 7 }' > iocs_list.json
days accepts 1 to 7; any other value returns illegal_days. For older indicators, ThreatFox publishes export files, which need the same key.
Check that ThreatFox accepted the request. Without a key you get {"error": "Unauthorized"}, and with a key ThreatFox doesn’t recognise, {"query_status": "unknown_auth_key"}; neither contains any IOCs:
jq -e '.query_status == "ok"' iocs_list.json
Then extract the SHA256 hashes and copy the list next to your rules:
jq -r '.data[] | select(.ioc_type == "sha256_hash") | .ioc' iocs_list.json | sort -u > threatfox_sha256.lst
wc -l threatfox_sha256.lst
sudo mkdir -p /etc/suricata/rules
sudo install -m 644 threatfox_sha256.lst /etc/suricata/rules/
Make sure the list isn’t empty: Suricata refuses to load a filesha256 rule whose list has no entries.
Creating the Suricata rule
alert http any any -> any any (msg:"ThreatFox IOC: malicious file transferred over HTTP"; flow:established; filesha256:/etc/suricata/rules/threatfox_sha256.lst; classtype:trojan-activity; sid:1000002; rev:1;)
The rule alerts when a file whose SHA256 is in the list crosses the network over HTTP, in either direction. The filesha256 keyword also makes Suricata compute the hash, so nothing else needs switching on. To cover other protocols, copy the rule and change http to http2, smtp, ftp-data, smb or nfs, giving each copy its own sid.
Save the rule in its own file:
sudo tee /etc/suricata/rules/iocs.rules > /dev/null <<'EOF'
alert http any any -> any any (msg:"ThreatFox IOC: malicious file transferred over HTTP"; flow:established; filesha256:/etc/suricata/rules/threatfox_sha256.lst; classtype:trojan-activity; sid:1000002; rev:1;)
EOF
Then add that file to rule-files in /etc/suricata/suricata.yaml. Use its absolute path and leave default-rule-path as it is, because suricata-update keeps writing suricata.rules there (/var/lib/suricata/rules on current packages):
default-rule-path: /var/lib/suricata/rules
rule-files:
- suricata.rules
- /etc/suricata/rules/iocs.rules
Let Suricata see whole files
This is the step the first version of this post missed. Suricata can only hash a file it has seen from start to finish, and two default settings cut files short:
response-body-limit(andrequest-body-limit, for uploads) in the HTTP section: 100 KB by default. A larger file downloaded over HTTP is cut off at 100 KB, never hashed, and never alerts.stream.reassembly.depth: 1 MB by default, for every TCP protocol. A file that runs past it is cut off too.
In our tests on Suricata 7.0 and 8.0 with the defaults, a 200 KB file and a 3 MB file both went unhashed ("state": "TRUNCATED" in eve.json). With both limits raised to 10 MB, both raised the alert. Edit the existing keys in /etc/suricata/suricata.yaml:
app-layer:
protocols:
http:
libhtp:
default-config:
request-body-limit: 10mb
response-body-limit: 10mb
stream:
reassembly:
depth: 10mb
These keys are already in the file, so change their values rather than adding a second copy. 0 removes a limit entirely, but higher limits cost memory for every flow, so on a busy sensor raise them step by step and watch memory use.
Test the configuration and restart Suricata:
sudo suricata -T -c /etc/suricata/suricata.yaml
sudo systemctl restart suricata
suricata -T also fails if suricata.rules doesn’t exist yet. If you have never run suricata-update, run sudo suricata-update first.
Verifying that alerting works
You don’t need a live malware sample to test the rule: any file works once its hash is in the list. On a second machine whose traffic Suricata can see, create a 2 MB test file, note its hash, and serve it over HTTP:
head -c 2000000 /dev/urandom > ioc-test.bin
sha256sum ioc-test.bin
python3 -m http.server 8000
On the Suricata host, add that hash to the list and reload the rules:
echo '<the sha256 from above>' | sudo tee -a /etc/suricata/rules/threatfox_sha256.lst
sudo systemctl reload suricata
Then download the file from the Suricata host:
curl -o /dev/null http://<second-machine-ip>:8000/ioc-test.bin
Check fast.log, or eve.json for the full picture:
sudo grep ':1000002:' /var/log/suricata/fast.log
sudo jq 'select(.event_type == "alert" and .alert.signature_id == 1000002)' /var/log/suricata/eve.json
Here is a shortened alert from our Suricata 8.0 test, with the addresses changed:
{
"timestamp": "2026-10-08T03:06:58.406643+0000",
"event_type": "alert",
"src_ip": "192.168.1.30",
"src_port": 8000,
"dest_ip": "192.168.1.20",
"dest_port": 57678,
"proto": "TCP",
"alert": {
"action": "allowed",
"signature_id": 1000002,
"signature": "ThreatFox IOC: malicious file transferred over HTTP",
"category": "A Network Trojan was detected",
"severity": 1
},
"http": {
"hostname": "192.168.1.30",
"http_port": 8000,
"url": "/ioc-test.bin",
"http_method": "GET",
"status": 200
},
"files": [
{
"filename": "/ioc-test.bin",
"gaps": false,
"state": "CLOSED",
"sha256": "10029d095ad4134ed193f2695a8429beae302937c245853b05222a2f61b5154b",
"stored": false,
"size": 2000000
}
],
"app_proto": "http"
}
"state": "CLOSED" and the sha256 field show that Suricata saw the whole file. If your fileinfo events show "state": "TRUNCATED" instead, go back to the limits above. Rebuilding the list with the steps above removes the test hash again.
Testing with a real sample. If you’d rather use real malware in an isolated lab, pick a hash from your list and download the sample from MalwareBazaar. Samples come as password-protected zips (the password is infected). Unzip it on the serving machine, not on the Suricata host, and serve the file as above; the zip itself has a different hash.
How IDSTower can help
The hash list you just built goes stale fast: ThreatFox adds new IOCs all the time, and the API only returns the last seven days. Keeping it current, on every sensor, is what IDSTower does for you:
- ThreatFox is built in. Enable the abuse.ch ThreatFox feed under Settings → Feeds and add your Auth-Key; IDSTower downloads the IOCs from then on (how to enable a feed).
- Every host stays current. New indicators reach every managed Suricata host automatically. MISP, TAXII/STIX and your own CSV or JSON feeds work the same way (threat intelligence feeds).
- The rules are written for you. For SHA256 hashes, IDSTower adds file-hash rules for downloads and uploads over HTTP, HTTP/2, SMTP, FTP, SMB and NFS. For domains from the same feeds, it alerts on DNS queries, HTTP Host headers and TLS SNI. Each indicator’s score becomes the alert’s priority.
Try it in the live demo (no signup), or get a free license.
