Classroom Glossary Public page

Lab 8: Suricata Signature Authoring and Zeek Script-as-Pipeline

456 words

Chapter: 8 (NSM-Lite)
Duration: 90 minutes
Tools: Suricata, Zeek, Wireshark, Python (Scapy)
Points: 10


Objectives

  1. Run the NSM corpus through Suricata and analyze detection coverage
  2. Author a custom Suricata rule targeting a specific TTP
  3. Process the DNS tunneling corpus file through Zeek and compute FQDN label statistics
  4. Write a Zeek script that detects anomalous DNS behavior
  5. 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
  1. Which corpus files generated zero Suricata alerts? These are false negatives for the default rule set.
  2. 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:

  1. Did your custom rule fire on the DNS tunneling corpus file?
  2. What is the alert message and SID?
  3. 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:

  1. What is the maximum label length in the DNS tunneling corpus?
  2. Normal hostnames rarely exceed 15 characters in a single label. What threshold would you set for a DNS tunneling alert?
  3. 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:

  1. Did the Zeek script fire an alert?
  2. What average label length was computed for the suspicious source?
  3. 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

  1. 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?
  2. 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)
  3. 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