Chapter: 8 (NSM-Lite)
Duration: 90 minutes
Tools: Suricata, Zeek, Wireshark, Python (Scapy)
Points: 10
Objectives
- Run the NSM corpus through Suricata and analyze detection coverage
- Author a custom Suricata rule targeting a specific TTP
- Process the DNS tunneling corpus file through Zeek and compute FQDN label statistics
- Write a Zeek script that detects anomalous DNS behavior
- Produce a combined alert + log report across the corpus
Setup
# Ensure Suricata and Zeek are installed
suricata --version
zeek --version
# Download/update ET Open rule set
sudo suricata-update
# Create output directories
mkdir -p /tmp/nsm-lab/suricata /tmp/nsm-lab/zeek
# NSM corpus location
CORPUS=/media/laptop/data4t/laptop/jupyter/virtus-academy/courses/net-201/labs/nsm-corpus
# If corpus is not yet present, generate it (see corpus generation below)
Generate corpus (if not pre-staged)
# /tmp/gen_corpus.py
from scapy.all import *
import os
os.makedirs("/tmp/nsm-corpus", exist_ok=True)
# dns_tunneling.pcap: DNS queries with encoded data in long labels
pkts = []
for i in range(20):
# Long FQDN label simulating DNS tunneling
label = "A" * 40 + f"{i:04x}"
pkt = (IP(src="10.0.0.10", dst="8.8.8.8") /
UDP(sport=12345+i, dport=53) /
DNS(rd=1, qd=DNSQR(qname=f"{label}.evil-tunnel.com")))
pkts.append(pkt)
wrpcap("/tmp/nsm-corpus/dns_tunneling.pcap", pkts)
# port_scan_syn.pcap: SYN scan across /24
syn_pkts = [IP(src="10.0.0.5", dst=f"192.168.1.{i}")/TCP(dport=80, flags="S")
for i in range(1, 255)]
wrpcap("/tmp/nsm-corpus/port_scan_syn.pcap", syn_pkts)
# icmp_tunneling.pcap: ICMP with large payload
icmp_pkts = [IP(src="10.0.0.5", dst="8.8.8.8")/ICMP(type=8)/("A"*200)
for _ in range(15)]
wrpcap("/tmp/nsm-corpus/icmp_tunneling.pcap", icmp_pkts)
# tls_cert_anomaly.pcap: placeholder (generate simple TCP SYN to port 443)
tls_pkts = [IP(src="10.0.0.10", dst="185.199.111.0")/TCP(dport=443, flags="S")]
wrpcap("/tmp/nsm-corpus/tls_cert_anomaly.pcap", tls_pkts)
print("Corpus generated.")
python3 /tmp/gen_corpus.py
CORPUS=/tmp/nsm-corpus
Part 1: Run Corpus Through Suricata (20 min)
# Run each corpus file through Suricata
for pcap in $CORPUS/*.pcap; do
name=$(basename $pcap .pcap)
mkdir -p /tmp/nsm-lab/suricata/$name
sudo suricata -c /etc/suricata/suricata.yaml \
-r $pcap \
-l /tmp/nsm-lab/suricata/$name \
--set outputs.1.eve-log.enabled=yes \
2>/dev/null
echo "=== $name ==="
cat /tmp/nsm-lab/suricata/$name/fast.log 2>/dev/null | head -5
echo "($(wc -l < /tmp/nsm-lab/suricata/$name/fast.log 2>/dev/null) alerts)"
done
Record (complete the table):
| Corpus File | Alerts Fired | Rule Names |
|---|---|---|
| dns_tunneling.pcap | ||
| port_scan_syn.pcap | ||
| icmp_tunneling.pcap | ||
| tls_cert_anomaly.pcap |
- Which corpus files generated zero Suricata alerts? These are false negatives for the default rule set.
- Which corpus files generated the most alerts? What were the top rule names?
Part 2: Author a Custom Suricata Rule (20 min)
2.1 Write a DNS tunneling detection rule
Create /etc/suricata/rules/custom-net201.rules:
# Detect DNS queries with unusually long subdomain labels (DNS tunneling indicator)
alert dns $HOME_NET any -> any 53 (
msg:"NET201 Possible DNS Tunneling Long Label";
dns.query; pcre:"/[a-zA-Z0-9+\/]{30,}\./";
threshold: type both, track by_src, count 5, seconds 30;
classtype:policy-violation;
sid:9900001; rev:1;
)
# Detect high-rate DNS queries from single source (tunneling or DGA activity)
alert dns $HOME_NET any -> any 53 (
msg:"NET201 High Rate DNS Queries Possible Tunneling";
threshold: type threshold, track by_src, count 15, seconds 10;
classtype:policy-violation;
sid:9900002; rev:1;
)
Add to Suricata config:
# Add custom rules file to suricata.yaml rule-files section
sudo bash -c "echo ' - custom-net201.rules' >> /etc/suricata/suricata.yaml"
# Test rule syntax
sudo suricata -T -c /etc/suricata/suricata.yaml
2.2 Test your custom rule
sudo suricata -c /etc/suricata/suricata.yaml \
-r $CORPUS/dns_tunneling.pcap \
-l /tmp/nsm-lab/suricata/custom_test/ 2>/dev/null
cat /tmp/nsm-lab/suricata/custom_test/fast.log
Record:
- Did your custom rule fire on the DNS tunneling corpus file?
- What is the alert message and SID?
- Did the threshold (5 in 30 seconds) fire? How many queries exceeded it?
Part 3: Zeek DNS Analysis (20 min)
3.1 Process corpus with Zeek
cd /tmp/nsm-lab/zeek
zeek -C -r $CORPUS/dns_tunneling.pcap
# View generated logs
ls *.log
head -20 dns.log
3.2 Analyze FQDN label lengths
# Extract DNS query names from dns.log
zeek-cut query < dns.log | sort | uniq
# Compute label lengths using awk
zeek-cut id.orig_h query < dns.log | awk '{
split($2, parts, ".")
max_label = 0
for (i in parts) {
if (length(parts[i]) > max_label) max_label = length(parts[i])
}
print $1, $2, max_label
}' | sort -k3 -rn | head -20
Record:
- What is the maximum label length in the DNS tunneling corpus?
- Normal hostnames rarely exceed 15 characters in a single label. What threshold would you set for a DNS tunneling alert?
- What source IP is generating the high-label-length queries?
Part 4: Write a Zeek Detection Script (20 min)
Create /tmp/dns-tunnel-detect.zeek:
# DNS tunneling detection: flag sources with average label length > 20 chars
global dns_label_lengths: table[addr] of vector of count;
event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
{
local labels = split_string(query, /\./);
local max_len = 0;
for (i in labels) {
if (|labels[i]| > max_len)
max_len = |labels[i]|;
}
if (c$id$orig_h !in dns_label_lengths)
dns_label_lengths[c$id$orig_h] = vector();
dns_label_lengths[c$id$orig_h] += max_len;
}
event zeek_done()
{
for (src in dns_label_lengths) {
local total = 0;
local count = |dns_label_lengths[src]|;
for (i in dns_label_lengths[src])
total += dns_label_lengths[src][i];
local avg = count > 0 ? total / count : 0;
if (avg > 20) {
print fmt("ALERT: DNS tunneling suspect %s avg_label_len=%d queries=%d",
src, avg, count);
}
}
}
cd /tmp/nsm-lab/zeek
zeek -C -r $CORPUS/dns_tunneling.pcap /tmp/dns-tunnel-detect.zeek
Record:
- Did the Zeek script fire an alert?
- What average label length was computed for the suspicious source?
- Modify the threshold to 30 characters and re-run. Does the alert still fire?
Part 5: Combined Report (10 min)
# Generate a summary report across all corpus files
for pcap in $CORPUS/*.pcap; do
name=$(basename $pcap .pcap)
echo "=== $name ==="
# Suricata alert count
sa=$(cat /tmp/nsm-lab/suricata/$name/fast.log 2>/dev/null | wc -l)
echo " Suricata alerts: $sa"
# Zeek: DNS query count
cd /tmp/nsm-lab/zeek/$name 2>/dev/null && \
echo " Zeek DNS queries: $(wc -l < dns.log 2>/dev/null)" && \
cd -
done
Lab Report
- The default ET Open Suricata rules may miss the DNS tunneling in this lab's corpus. What property of the tunneling traffic makes it hard to detect with simple content-match rules?
- Your Zeek script uses average label length as a heuristic. What legitimate traffic could produce false positives with this approach? (Hint: think about long CDN hostnames or DKIM keys in DNS TXT records)
- Describe a detection approach that combines Suricata and Zeek. What does each tool contribute that the other cannot?
Grading (10 points)
| Item | Points |
|---|---|
| Corpus run through Suricata; detection table complete | 2 |
| Custom Suricata rule: fires on DNS tunneling corpus; syntax correct | 2 |
| Zeek dns.log analysis: max label length and suspicious source identified | 2 |
| Zeek detection script: alert fires; threshold modification tested | 2 |
| Lab report: tunneling detection difficulty; false positive analysis | 2 |