Observing Baseline vs Anomalous Modbus Traffic: A Beginner’s ICS/OT Security Lab

Introduction
In support of my recent research into ICS/OT Security Operations, I wanted to set up a small, controlled environment where I could
observe normal industrial network behavior
identify meaningful deviations
reason about that activity using the
MITRE ATT&CK for Industrial Control Systemsframework.
Coming from an IT and security operations background, one of the biggest mindset shifts in OT security is learning to prioritize visibility, context, and operational impact over traditional ideas of exploitation or compromise. This lab was designed to help bridge that gap in a practical, beginner-friendly way.
The goal in the following lab is not to simulate a realistic attack, but to:
Observe what normal OT traffic looks like
Introduce a small deviation from that baseline
Analyze the change using packet-level evidence
Practice thinking like an OT security analyst
This walkthrough assumes basic familiarity with Python, Wireshark, and packet analysis, but no deep prior experience with ICS/OT protocols.
If you’re already feeling lost and need an introduction to ICS/OT Security Operations, I would point you to two helpful resources:
OT Cybersecurity Quick Start Guide for IT Professionals - A brief introduction to terms and approach by Dragos
CISA - ICS Training - The ICS 300 is a great introduction to Cyber Security applied to Industrial Control systems by the Cybersecurity & Infrastructure Security Agency (USA)
Lab Overview
To keep the environment simple and focused, I used a simulated Modbus TCP PLC (Programmable Logic Controller) and a lightweight client acting as an HMI (Human Machine Interface) or polling system.
Modbus TCP was chosen due to its simplicity, widespread use, and lack of native authentication, making it a useful protocol for learning visibility and detection fundamentals.
Components
Simulated PLC: Python-based Modbus TCP server
Client/HMI: Python-based Modbus TCP client
Monitoring Tool: Wireshark
Protocol: Modbus TCP
Port: 5020
Both the server and client were run on the same host using the loopback interface (127.0.0.1). This allowed me to focus on protocol behavior and detection logic, rather than network topology.
Environment Setup
I created:
A Python script acting as a PLC, exposing holding registers: plc_server.py
from pymodbus.server import StartTcpServer from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusDeviceContext, ModbusServerContext def main() -> None: device = ModbusDeviceContext( di=ModbusSequentialDataBlock(0, [0]*100), co=ModbusSequentialDataBlock(0, [0]*100), hr=ModbusSequentialDataBlock(0, [0]*100), ir=ModbusSequentialDataBlock(0, [0]*100) ) context = ModbusServerContext(devices={1: device}, single=False) print("Starting simulated PLC on 0.0.0.0:5020") StartTcpServer(context=context, address=("0.0.0.0", 5020)) if __name__ == "__main__": main()
A second Python script acting as a client, polling those registers every few seconds: modbus_client.py
import time
from pymodbus.client import ModbusTcpClient
def main() -> None:
client = ModbusTcpClient("127.0.0.1", port=5020)
if not client.connect():
raise RuntimeError("Could not connect to PLC server")
try:
while True:
rr = client.read_holding_registers(address=0, count=10, device_id=1)
if rr.isError():
print("Read error:", rr)
else:
print("Read registers:", rr.registers)
time.sleep(5)
finally:
client.close()
if __name__ == "__main__":
main()
I ran both the server and the client on Loopback Interface (127.0.0.1)
This established a predictable, repeatable traffic pattern.
Observing the Baseline
With the client polling the PLC, I was able to view the following baseline registers in the client command line:
Read registers: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Read registers: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Read registers: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Read registers: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Read registers: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Using the filter “tcp.port == 5020” on the loopback interface in Wireshark, I was able to capture baseline traffic.
Baseline Characteristics
In the baseline capture, I observed:
Repeating Modbus TCP transactions
Function Code 03 (Read Holding Registers)
Regular polling intervals (~every 5 seconds)
No write or control commands
A single client communicating with the PLC
This is a common pattern in OT environments, where HMIs or monitoring systems frequently poll PLCs for state information.
Establishing this baseline is critical. Without it, there is no meaningful way to identify deviations.
Introducing Anomalous Behaviour
To simulate an anomaly, I modified the client so that every 30 seconds it would issue a write to a holding register.
To do this, within the While Loop, after “time.sleep(5)” and before “finally", I added the following clause to modbus_client.py:
if int(time.time()) % 30 == 0:
wr = client.write_register(address=5, value=123, device_id=1)
print("Write register 5 -> 123:", "OK" if not wr.isError() else wr)
This represents a scenario such as:
A misconfigured HMI
Maintenance activity outside an expected window
An unauthorized system interacting with the PLC
This is not an exploit - it is simply control-plane activity that deviates from the baseline.
Observing the Anomaly
In the anomaly capture, the baseline read traffic continued unchanged. However, every 30 seconds an additional Modbus TCP packet appeared.
The Modbus payload of the anomalous packet was:
00790000000601060005007b
This packet did not appear in the baseline capture thus it would warrant review by a Security Analyst or alerting logic designed to flag unexpected control-plane activity.
Breaking Down the Packet
Decoding the Modbus TCP payload reveals the nature of the anomaly.
| Field | Value | Meaning |
| Transaction ID | 0x0079 | Request identifier |
| Protocol ID | 0x0000 | Modbus |
| Length | 0x0006 | Bytes following |
| Unit ID | 0x01 | Device ID |
| Function Code | 0x06 | Write Single Register |
| Register Address | 0x0005 | Register 5 |
| Value | 0x007b | Decimal 123 |
Function Code 06 indicates a Write Single Register operation.
This is a key distinction:
Baseline traffic = observation (reads)
Anomalous traffic = influence (writes)
Why This Matters in an OT Context
In many industrial networks:
PLCs are expected to service read-only requests during normal operations
Write operations are restricted to specific systems or maintenance windows
Unexpected writes are treated as high-signal events
While this activity could be benign, it would typically prompt questions such as:
Is this write expected?
Who is authorized to issue it?
Is there an active maintenance task?
Has something changed in the environment?
From a security operations perspective, the goal is not to immediately assume malicious intent, but to validate context and reduce uncertainty.
Mapping to MITRE ATT&CK for ICS
This behavior aligns with MITRE ATT&CK for ICS techniques related to unauthorized control or manipulation of control devices, where write operations are issued outside expected operational context.
While this lab does not simulate a full attack chain, it demonstrates how:
Low-level protocol activity
Combined with baseline understanding
Can surface behaviors that warrant further investigation
In a real environment, additional telemetry such as asset inventories, engineering workstation logs, and change records would be used to bolster the investigation.
Lessons Learned
This exercise reinforced several key ideas for someone new to ICS/OT security:
Baseline is everything
Without understanding normal behavior, anomalies are meaningless.OT security is context-driven
The same packet can be benign or serious depending on timing, role, and authorization.Simple deviations can be high-signal
A single write command can matter more than thousands of read requests.Detection starts with visibility, not exploits
Packet-level inspection remains a powerful tool in OT environments.Frameworks support reasoning, not conclusions
MITRE ATT&CK helps structure thinking, but does not replace operational judgement.
Closing Thoughts
This small lab helped me better appreciate the analyst mindset required for OT security operations: patient, methodical, and grounded in operational reality. For anyone with an IT or SOC background looking to move into ICS/OT security, setting up a simple protocol-focused lab like this is an effective way to build intuition and confidence.
Until next time…
— Cyber Wizard
NOTE: This lab was inspired by the learning materials linked at the beginning of the article. The lab was designed with guidance and editorial assistance from AI tools.






