← Back to Writeups

Snort IDS between PLC and HMI (Modbus)

This run-through is about how to use Snort to catch sketchy Modbus/TCP traffic in a small ICS lab.

Main goals:

  1. Catch non-Modbus junk on port 502
  2. Catch Write Single Register attempts (Function Code 6)
  3. Catch illegal values being written to a PLC register (traffic light logic)

Setup (quick)

  • Snort config checked in snort.lua
  • HOME_NET set to my lab subnet
  • EXTERNAL_NET set to any
  • Rules stored in local.rules

Rule syntax check:

sudo snort -T -c /etc/snort/snort.lua

Start Snort in passive mode:

sudo snort -c /etc/snort/snort.lua -i eth0 --daq pcap --daq-mode passive -l /var/log/snort/

Passive mode keeps things safe and observable while testing.


Part 1A - Detect non-Modbus traffic on TCP/502

Rule:

alert tcp any any -> any 502 (msg:"Non-Modbus traffic on port 502 - Invalid Protocol ID"; byte_test:2,!=,0,2; sid:1000001; rev:1;)
  • In Modbus/TCP, MBAP Protocol ID (bytes 2-3) should be 0x0000
  • If it is non-zero, that payload is not valid Modbus

Test packet (crafted bad Protocol ID):

echo -e "\x00\x01\x00\x01\x00\x06\x01\x03\x00\x00\x00\x0A" | nc -w1 10.211.55.6 502

Result:

  • Built-in Snort Modbus inspector alert fired
  • Custom rule sid:1000001 also fired

That is a solid confirmation the rule is doing its job.


Part 1B - Detect Write Single Register (Function Code 6)

Rule:

alert tcp any any -> any 502 (msg:"Modbus Write Single Register - Function Code 6"; byte_test:1,=,6,7; sid:1000002; rev:1;)
  • Byte 7 of the Modbus frame is the function code
  • Value 6 means Write Single Register

Test packet:

echo -e "\x00\x01\x00\x00\x00\x06\x01\x06\x00\x01\x00\xFF" | nc -w1 10.211.55.6 502

Result:

  • sid:1000002 fired correctly

Part 1C - Same detection, cleaner rule (Modbus inspector)

Instead of raw byte offsets, I rewrote Part 1B using Snort's Modbus inspector keyword:

alert tcp any any -> any 502 (msg:"Modbus Write Single Register - Function Code 6"; modbus_func:6; sid:1000002; rev:1;)

Why this is nicer:

  • Reads like intent, not byte math
  • Easier to maintain
  • Less likely to confuse future-you during tuning

Part 2 - Detect illegal values in a PLC register

Now the practical OT defense piece: detect writes that break process logic.

For a traffic light register with legal values 0-3, alert on anything greater than 3.

Final rule (targeting register 23 / %QW23):

alert tcp any any -> any 502 (msg:"Illegal value written to Modbus register 23 - traffic light"; modbus_func:6; byte_test:2,=,23,8; byte_test:2,>,3,10; sid:1000003; rev:1;)

What this checks:

  • modbus_func:6 -> write single register only
  • byte_test:2,=,23,8 -> target register address 23
  • byte_test:2,>,3,10 -> written value must be greater than 3 to alert

Validation flow:

  1. Send legal value (for example 2) -> no alert
  2. Send illegal value (for example 99) -> alert fires

Result:

  • Snort fired only on illegal write activity
  • No alert on legal writes
  • Good signal quality for this lab scenario

Attack simulation note

I used a quick pymodbus write to push an illegal value into the PLC register.

That simulates a real ICS attack pattern: attacker injects out-of-range control values to mess with physical behavior.

Snort caught it immediately with sid:1000003.


Final ruleset

alert tcp any any -> any 502 (msg:"Non-Modbus traffic on port 502 - Invalid Protocol ID"; byte_test:2,!=,0,2; sid:1000001; rev:1;)
alert tcp any any -> any 502 (msg:"Modbus Write Single Register - Function Code 6"; modbus_func:6; sid:1000002; rev:1;)
alert tcp any any -> any 502 (msg:"Illegal value written to Modbus register 23 - traffic light"; modbus_func:6; byte_test:2,=,23,8; byte_test:2,>,3,10; sid:1000003; rev:1;)

Takeaways

  • Start passive and verify visibility first
  • Build rules in small chunks and validate each one
  • Prefer protocol-aware keywords when possible (modbus_func)
  • Write policy rules tied to process constraints, not just signatures
  • Always test both positive and negative cases (alert + no-alert)

That is how you keep detections useful and avoid noisy IDS output in ICS labs.