Suricata: Network Signatures for Malware Detection
- Suricata: Network Signatures for Malware Detection
Suricata is an open-source network intrusion detection and prevention system (IDS/IPS) maintained by the Open Information Security Foundation (OISF). For malware analysis, its most important use is offline: you run a sample under INetSim, capture the traffic, then run Suricata against the PCAP to test signatures you are developing.
Suricata’s rule language is a superset of Snort’s. Chapter 14 of Practical Malware Analysis covers Snort rule fundamentals — those rules run on Suricata without modification. This page covers the Suricata-specific additions that are most useful for detecting malware.
Installation
sudo apt install suricata
suricata --build-info | head -5 # confirm version and enabled features
Update the rule sets (optional for offline analysis, but useful for reference):
sudo suricata-update
Offline Analysis Against a PCAP
For malware analysis you almost never run Suricata inline. You capture with Wireshark or tcpdump while the sample runs under INetSim, then replay the capture:
suricata -r capture.pcap -S my_rules.rules -l /tmp/suricata-out/ -k none
| Flag | Purpose |
|---|---|
-r <file> |
Read from PCAP instead of live interface |
-S <file> |
Load only this rules file (skip default rule sets) |
-l <dir> |
Write logs to this directory |
-k none |
Disable checksum validation (captures from VMs often have bad checksums) |
Alerts are written to /tmp/suricata-out/fast.log (one line per alert) and /tmp/suricata-out/eve.json (full structured event log).
cat /tmp/suricata-out/fast.log
# 05/06/2026-14:32:01.123456 [**] [1:9000001:1] TROJAN Beacon UA [**] ...
# Parse EVE JSON with jq
jq 'select(.event_type=="alert")' /tmp/suricata-out/eve.json
Rule Structure
Every Suricata rule has two parts: an action and header, then an options block in parentheses.
alert http $HOME_NET any -> $EXTERNAL_NET any \
(msg:"TROJAN Malicious Beacon"; \
http.user_agent; content:"Wefa7e"; \
flow:established,to_server; \
classtype:trojan-activity; sid:9000001; rev:1;)
Action
For analysis, always use alert. drop and reject are for inline IPS deployments.
Header
alert <proto> <src_ip> <src_port> -> <dst_ip> <dst_port>
- Protocol:
tcp,udp,icmp,http,dns,tls,smtp,ftp— Suricata parses application-layer protocols automatically when you name them here - Use
$HOME_NETand$EXTERNAL_NETvariables (defined insuricata.yaml) to avoid hardcoding IP ranges - Use
$HTTP_PORTSfor HTTP traffic,anywhen port is unknown or varies
Direction matters. For C2 beacons going out:
$HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS
For C2 responses coming in (useful for command channel signatures):
$EXTERNAL_NET any -> $HOME_NET any
Core Options (from PMA Ch. 14)
These work identically in Suricata and Snort:
| Keyword | Purpose |
|---|---|
msg |
Alert message string |
content:"string" |
Match literal bytes in payload |
nocase |
Case-insensitive content match |
pcre:"/pattern/" |
Perl-compatible regular expression |
flow:established,to_server |
Only match in client→server direction of an established TCP session |
isdataat:<n>,relative |
Assert at least n bytes exist after the previous match |
distance:<n> |
Skip n bytes after previous match before searching for next content |
within:<n> |
Next content must appear within n bytes of previous match |
classtype |
Rule category (trojan-activity, policy-violation, etc.) |
sid |
Unique numeric rule ID |
rev |
Rule revision |
reference |
Link to external source (CVE, URL, MD5) |
Pipe symbols enclose hex: |0d 0a| is CRLF. The bang ! negates a content match: content:!"Referer:" matches packets where Referer: is absent.
The absence of a header is a signal. Legitimate browsers always send Referer:, Accept-Language:, and Accept-Encoding:. Malware that constructs HTTP manually (WinSock) often omits them. Combining a content match with a negated header check dramatically reduces false positives — a technique covered in detail in PMA Ch. 14.
Suricata Application-Layer Keywords
These are Suricata-specific additions that make rules more precise and faster because Suricata parses the protocol before applying the keyword, rather than scanning raw bytes.
HTTP
http.method; content:"POST";
http.uri; content:"/checkin"; startswith;
http.user_agent; content:"Mozilla/4.0 (compatible; MSIE 6.0)";
http.header; content:"Accept: * / *";
http.header; content:!"Referer:";
http.request_body; content:"uid=";
http.response_body; content:"<!-- adsrv?";
http.host; content:"badsite.com";
http.cookie; content:"session=";
Using http.user_agent instead of a raw content search is both faster and more accurate — it only looks at the User-Agent field, so a matching string elsewhere in the request does not trigger it.
startswith and endswith are Suricata-only modifiers: they anchor the content match to the start or end of the buffer rather than searching the whole field.
DNS
dns.query; content:"malicious-domain.com"; nocase;
Matches the queried domain name. Useful for detecting DGA beaconing (high-entropy names, unusual TLDs) or hardcoded C2 domains. Combine with pcre to match patterns rather than exact names:
alert dns $HOME_NET any -> any 53 \
(msg:"Possible DGA Domain"; \
dns.query; pcre:"/^[a-z]{8,15}\.(com|net|org)$/i"; \
classtype:trojan-activity; sid:9000010; rev:1;)
TLS / SSL
tls.sni; content:"badsite.com";
tls.cert_subject; content:"CN=evil";
tls.ja3_hash; content:"e7d705a3286e19ea42f587b344ee6865";
tls.ja3s_hash; content:"ec74a5c51106f0419184d0dd08fb05bc";
tls.sni matches the Server Name Indication field in the TLS ClientHello — visible even in encrypted traffic. tls.ja3_hash and tls.ja3s_hash fingerprint the TLS negotiation itself. Different malware families produce characteristic JA3 hashes because they use specific TLS libraries and cipher suite orderings. JA3 hashes can be looked up against community databases (ja3er.com, abuse.ch).
DoT and DoH: DNS-over-TLS traffic on port 853 can be detected by tls.sni if the malware sends a legitimate SNI. DNS-over-HTTPS can be detected by looking at the URI in the HTTPS stream (if you have the key) or by http.host matching known DoH resolvers.
Writing Effective Malware Signatures
PMA Ch. 14 dedicates significant space to the process of developing signatures that are both accurate and durable. The key principles, applied to Suricata:
1. Identify the source of each network element
Before writing a rule, determine where each byte of the network traffic comes from (PMA Ch. 14, “Knowing the Sources of Network Content”):
| Source | Signature value | Suricata approach |
|---|---|---|
| Hard-coded string | High — stable across all runs and hosts | Use content or application-layer keyword |
| Host-derived (hostname, CPU) | Medium — stable per host, varies between hosts | Use pcre to match the structure, not the value |
Time-derived (GetTickCount) |
Low for content — but length/encoding is stable | Use isdataat or pcre with length anchor |
Random (rand()) |
None for content — but character set may be stable | Use pcre to match character class, not value |
| Library-generated boilerplate | None — identical to legitimate traffic | Don’t target this; find something else |
2. Multiple independent rules
A single rule that targets five elements simultaneously becomes useless the moment the attacker changes one of them. Write independent rules for each stable element:
# Rule 1: User-Agent (hard-coded in malware)
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"TROJAN Lab14-01 UA"; \
http.user_agent; content:"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)"; \
http.header; content:!"Referer:"; nocase; \
flow:established,to_server; \
classtype:trojan-activity; sid:9000020; rev:1;)
# Rule 2: URI structure (random content but stable separators)
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"TROJAN Lab14-01 URI"; \
http.uri; pcre:"/\/([0-9]{1,3}:){4}[0-9]{8}\/$/"; \
flow:established,to_server; \
classtype:trojan-activity; sid:9000021; rev:1;)
If the attacker changes the User-Agent, rule 2 still fires. If they change the URI encoding scheme, rule 1 still fires.
3. Target elements present on both endpoints
The attacker must change client code AND server code to evade a signature based on a shared protocol element. Targeting only the client-side request (easy to change) is less durable than targeting a handshake exchange that both sides must agree on (PMA Ch. 14, “Understanding the Attacker’s Perspective”).
4. Avoid false positives before claiming success
Run your rules against a baseline capture of normal browsing. If they fire on legitimate traffic, they are not usable. Common false-positive sources:
- Generic User-Agent strings shared with legitimate software (Webmin, Python requests, curl)
- Short URI patterns that appear in legitimate API traffic
- Content matches that hit binary file transfers
Detecting Common Malware Behaviors
HTTP C2 beacon
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"TROJAN Possible C2 Beacon - Missing Browser Headers"; \
http.method; content:"GET"; \
http.header; content:!"Accept-Language:"; nocase; \
http.header; content:!"Accept-Encoding:"; nocase; \
http.header; content:!"Referer:"; nocase; \
flow:established,to_server; \
threshold:type both, track by_src, count 5, seconds 300; \
classtype:trojan-activity; sid:9000030; rev:1;)
The threshold keyword fires only when the pattern repeats — a single missing header is unremarkable; five times in five minutes is a beacon.
Downloader (URLDownloadToFile / WinINet)
alert http $EXTERNAL_NET $HTTP_PORTS -> $HOME_NET any \
(msg:"TROJAN Executable Download - PE Header in Response"; \
http.response_body; content:"|4d 5a|"; startswith; \
classtype:trojan-activity; sid:9000031; rev:1;)
Detects a response whose body begins with the PE MZ magic bytes — an executable being downloaded.
DNS tunneling / exfiltration
alert dns $HOME_NET any -> any 53 \
(msg:"TROJAN Possible DNS Exfiltration - Long Subdomain"; \
dns.query; pcre:"/^[a-zA-Z0-9+\/=]{30,}\./"; \
classtype:trojan-activity; sid:9000032; rev:1;)
Matches DNS queries where the first label is 30+ characters of Base64-looking data — a common DNS exfiltration pattern (PMA Ch. 11, “DNS Exfiltration”).
Command hidden in HTTP comment (Lab14-03 pattern)
alert http $EXTERNAL_NET any -> $HOME_NET any \
(msg:"TROJAN Lab14-03 Command Channel"; \
http.response_body; content:"<!-- adsrv?"; \
classtype:trojan-activity; sid:9000033; rev:1;)
Self-signed or unusual TLS certificate
alert tls $EXTERNAL_NET any -> $HOME_NET any \
(msg:"TROJAN Self-Signed TLS Cert to Internal Host"; \
tls.cert_issuer; content:"O=Panda Lab"; \
classtype:trojan-activity; sid:9000034; rev:1;)
EVE JSON Output
Suricata’s EVE JSON log is structured and queryable. Each event is one JSON object per line.
# All alerts
jq 'select(.event_type=="alert")' eve.json
# DNS queries made by the sample
jq 'select(.event_type=="dns" and .dns.type=="query") | .dns.rrname' eve.json
# HTTP requests with full details
jq 'select(.event_type=="http") | {ts:.timestamp, host:.http.hostname, uri:.http.url, ua:.http.http_user_agent}' eve.json
# TLS connections with SNI and JA3
jq 'select(.event_type=="tls") | {sni:.tls.sni, ja3:.tls.ja3.hash, ja3s:.tls.ja3s.hash}' eve.json
# All unique destination IPs contacted
jq 'select(.event_type=="flow") | .dest_ip' eve.json | sort -u
File extraction (Suricata can write files transferred over HTTP/SMTP to disk):
# In suricata.yaml, enable file-store:
# - file-store:
# enabled: yes
# dir: /tmp/suricata-files
jq 'select(.event_type=="fileinfo")' eve.json # list extracted files
Rule Testing Workflow
A repeatable workflow for malware signature development:
# 1. Capture traffic while sample runs under INetSim
tcpdump -i eth0 -w capture.pcap
# 2. Draft rules in hw4.rules
# 3. Test rules against capture
suricata -r capture.pcap -S hw4.rules -l /tmp/out/ -k none
# 4. Check alerts
cat /tmp/out/fast.log
# 5. Check for false positives against a clean baseline capture
suricata -r baseline.pcap -S hw4.rules -l /tmp/baseline-out/ -k none
cat /tmp/baseline-out/fast.log # should be empty
# 6. Refine rules; repeat from step 3
Rule Management
Rules files are plain text, one rule per line. Comments start with #.
# Validate rule syntax without running a capture
suricata -T -S hw4.rules
# "Configuration provided was successfully loaded." means no syntax errors
Suricata SID ranges (by convention):
| Range | Use |
|---|---|
| 1–999999 | Snort community rules |
| 1000000–1999999 | Emerging Threats rules |
| 2000000–2999999 | Emerging Threats Pro |
| 9000000+ | Local / custom rules |
Use the 9000000+ range for your own rules to avoid conflicts.
Further Reading
- Sikorski & Honig, Practical Malware Analysis, Ch. 14 — Malware-Focused Network Signatures
- Suricata documentation — complete keyword reference
- Emerging Threats rule set — community signatures; useful as examples
- JA3 fingerprinting — TLS client fingerprinting methodology
- abuse.ch JA3 database — known-malicious JA3 hashes