OT/ICS Security Testing: Modbus, PLCs and Industrial Networks

Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package python3-pymodbus 3.14.0-1
mbpoll 1.5.2-2
can-utils 2023.03-1
tshark 4.6.6-1
Applies to Kali Linux (concepts apply to any authorized OT/ICS engagement)
Lab environment Pentest lab
Cert prep CompTIA PenTest+ PT0-003 — specialized systems (OT/ICS)
Privilege sudo for modprobe / vcan and packet capture; normal user for Modbus client tools
Scope Why OT testing differs from IT, ICS architecture, common industrial protocols overview, Modbus TCP framing and address conventions, Modbus Security (TLS/port 802), local Modbus read/write and repeated-write risk, tshark Modbus analysis, classic CAN vs CAN FD without implying CAN FD authentication, virtual CAN labs, OT safety rules and controls. Not RFID/NFC/Bluetooth labs and not production OT exploitation.
Short on time?Beta Get the gist of this 15-minute read in seconds, then ask follow-up questions

In this article we will practice OT/ICS security testing concepts on Kali with local Modbus TCP and virtual CAN simulators only.

Operational technology (OT) security testing uses a different risk model from ordinary IT penetration testing because actions can affect physical processes, availability, and safety. You get plain-English ICS components, how Modbus TCP differs from Modbus Security, and hands-on register and frame demos that never touch production equipment.

IMPORTANT
Run OT tests only on systems you own or are explicitly authorized to assess. Production control networks and safety systems are off limits without a written scope that names stop conditions. Put OT constraints in your rules of engagement the same way you would for AD or web testing.

Why OT security testing differs from IT pentesting

IT engagements often focus on confidentiality and integrity first. OT security decisions must account for availability, reliability, deterministic operation, and process safety in addition to confidentiality and integrity. A wrong write to a holding register can change a setpoint, trip a drive, or put operators into an unexpected state.

Expect these differences up front:

  • Impact can be physical or process-related, not only data leakage
  • Maintenance windows and operator coordination matter more than “quiet hours”
  • Legacy protocols may lack cryptographic authentication or integrity protection
  • Passive observation and simulation often come before active writes
  • Stop conditions must be explicit (who can halt the test, and how)

If your only experience is Kali against DVWA or Metasploitable, do not treat a Modbus TCP port like a vulnerable web app. Same toolkit skills, different risk model.


OT/ICS architecture

Industrial control systems stack sensors and actuators under controllers, then expose operators and engineers through HMIs and engineering tools. Names vary by vendor, but the roles stay familiar:

Piece Role
Sensor / actuator Measures or moves the process (temperature, valve, motor)
PLC Runs control logic; often talks industrial protocols on the plant network
HMI Operator screen for monitoring and setpoints
SCADA Supervisory view across sites or process areas
Engineering workstation Where logic is written, downloaded, and sometimes left with powerful access

A simplified mental model:

text
  Sensor/Actuator ──► PLC ──► Plant network (e.g. Modbus TCP)
                         │
                         ├──► HMI / SCADA
                         └──► Engineering workstation

Attack surface discussions usually start with who can reach the PLC protocol port, who can download logic, and whether field buses (CAN, serial Modbus) are segmented from corporate IT.


Common OT/ICS protocols

Modbus is common in labs and exams, but it is only one industrial protocol family. You do not need a lab for every row here; treat the table as orientation when you inventory a plant network.

Protocol Typical context
Modbus TCP PLC/HMI industrial control over Ethernet
Modbus RTU Serial Modbus on RS-485 and similar links
EtherNet/IP Common industrial automation networks
DNP3 Utilities and many SCADA systems
OPC UA Industrial data interoperability
PROFINET Industrial Ethernet automation
CAN Vehicles, machines, and embedded control buses

This guide drills into Modbus TCP and classic CAN with safe simulators. Other protocols stay awareness-level unless a later engagement needs them.


Modbus TCP security basics

Classic Modbus TCP is a request/response protocol on TCP port 502 in most deployments. A client (often an HMI, SCADA poller, or engineering tool) issues requests; a server (often a PLC or gateway) responds.

Core vocabulary:

  • Client / server: the client issues requests; the server exposes coils, discrete inputs, input registers, and holding registers defined by the device or application
  • Function codes: for example 0x03 (Read Holding Registers) and 0x06 (Write Single Register)
  • Framing: the Modbus PDU is the function code plus function-specific data; the Modbus TCP ADU is the MBAP header plus that PDU

Vendors decide what particular addresses mean. Register 1 is not universally a temperature or setpoint. In the simulator below, register 1 is only a fake process variable for demonstration.

Classic Modbus TCP provides no built-in cryptographic authentication, confidentiality, or message-integrity protection. TCP provides reliable transport and error detection, but it does not authenticate the Modbus sender or protect against deliberate modification. If you can reach the port and craft a valid PDU, many devices still accept the write.

Modbus addressing conventions

Addressing routinely confuses readers because protocol addresses may be zero-based, human documentation often shows one-based or 4xxxx holding-register numbers, and tools expose offsets differently.

mbpoll uses one-based references by default; -0 switches to zero-based PDU addressing. Device manuals may document addresses as offsets or as 4xxxx holding-register numbers, so always confirm the vendor’s addressing convention before interpreting a register.


Modbus TCP vs Modbus Security

Traditional Modbus TCP and Modbus Security are not equivalent.

Classic Modbus TCP Modbus Security
Typical port 502 802 (IANA)
Transport Plain TCP TLS protecting Modbus application traffic
Identity Often none Mutual X.509 certificates
Authorization Typically absent or implementation-specific Role-based authorization information carried through an X.509v3 certificate extension
Application framing MBAP header + Modbus PDU Modbus application traffic protected by TLS
PDU Function code + function data Same Modbus PDU semantics

The Modbus Organization’s Modbus/TCP Security specification wraps familiar Modbus application data in TLS and expects mutual certificate authentication. Finding port 502 open does not mean port 802 (or vendor TLS variants) is configured. Finding Modbus Security does not mean every legacy client on 502 disappeared. Devices can expose both.


Build a safe Modbus simulator on Kali

Stay on a local simulator. Do not point these commands at a plant PLC.

Install the packages used below if they are missing. On Kali, use apt (or apt-get) with sudo:

bash
sudo apt-get install -y python3-pymodbus mbpoll can-utils tshark

Confirm the versions you care about with dpkg:

bash
dpkg -l python3-pymodbus mbpoll can-utils tshark | awk '/^ii/{printf "%-22s %s\n",$2,$3}'
output
can-utils              2023.03-1
mbpoll                 1.5.2-2
python3-pymodbus       3.14.0-1
tshark                 4.6.6-1

Those packages cover the Modbus server library, a Modbus master CLI, virtual CAN tools, and packet analysis.

Create a working directory:

bash
mkdir -p ~/ot-ics-lab

mkdir -p exits quietly when the path already exists. Write a minimal holding-register server that listens on loopback port 5020 (non-privileged) with register 1 initialized to 25:

bash
cat > ~/ot-ics-lab/modbus_sim.py << 'EOF'
#!/usr/bin/env python3
from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusServerContext,
    ModbusDeviceContext,
)
from pymodbus.server import StartTcpServer

store = ModbusDeviceContext(
    hr=ModbusSequentialDataBlock(1, [25] + [0] * 99),
    ir=ModbusSequentialDataBlock(1, [0] * 100),
    co=ModbusSequentialDataBlock(1, [0] * 100),
    di=ModbusSequentialDataBlock(1, [0] * 100),
)
context = ModbusServerContext(devices=store, single=True)
print("Listening Modbus TCP on 127.0.0.1:5020 (holding reg 1 = 25)", flush=True)
StartTcpServer(context=context, address=("127.0.0.1", 5020))
EOF

cat writes the file and exits with no output when it succeeds. Start the simulator in a second terminal (or background it):

bash
python3 ~/ot-ics-lab/modbus_sim.py
output
Listening Modbus TCP on 127.0.0.1:5020 (holding reg 1 = 25)

Leave that process running for the read, write, and capture steps below.


Read and write Modbus registers

Treat holding register 1 as a fake process variable for this lab only. Read it with mbpoll as unit ID 1 on port 5020 (one-based reference, the mbpoll default):

bash
mbpoll -a 1 -r 1 -c 1 -t 4 -p 5020 -1 127.0.0.1
output
Protocol configuration: ModBus TCP
Slave configuration...: address = [1]
                        start reference = 1, count = 1
Communication.........: 127.0.0.1, port 5020, t/o 1.00 s, poll rate 1000 ms
Data type.............: 16-bit register, output (holding) register table

-- Polling slave 1...
[1]: 	25

The [1]: 25 line is the current fake process variable. Write a new value (42) with the same tool:

bash
mbpoll -a 1 -r 1 -t 4 -p 5020 -1 127.0.0.1 42
output
Protocol configuration: ModBus TCP
Slave configuration...: address = [1]
                        start reference = 1, count = 1
Communication.........: 127.0.0.1, port 5020, t/o 1.00 s, poll rate 1000 ms
Data type.............: 16-bit register, output (holding) register table

Written 1 references.

Read again to confirm the value changed:

bash
mbpoll -a 1 -r 1 -c 1 -t 4 -p 5020 -1 127.0.0.1
output
-- Polling slave 1...
[1]: 	42

A reachable classic Modbus endpoint can accept a write that changes a value the process logic treats as truth. On a real controller, that value might drive a valve or interlock, which is why production testing needs owner coordination and a defined process state.


Repeated Modbus writes and replay risk

Sending the same mbpoll command again is not a packet-replay attack; it simply demonstrates that classic Modbus provides no application-level freshness token preventing the same operation from being requested again. A true replay test would capture and retransmit a previously observed transaction.

Run the same write once more against the simulator:

bash
mbpoll -a 1 -r 1 -t 4 -p 5020 -1 127.0.0.1 42
output
Written 1 references.

The server accepts the duplicate request. In a plant capture scenario, an attacker who recorded a legitimate “open valve” or “raise setpoint” PDU could try to re-inject that transaction later. Defenses that matter here are authentication, integrity, sequence or session binding where the product supports them, and network controls that keep untrusted hosts off the control VLAN.


Analyze Modbus traffic with tshark

Prefer analysis over blind writes. With the simulator still listening, move into the lab directory so the pcap lands next to your simulator script:

bash
cd ~/ot-ics-lab

Start a loopback capture in that directory (5020 is the lab port, not the default 502):

bash
sudo tshark -i lo -f 'tcp port 5020' -w modbus-lab.pcap

tshark keeps running until you stop it. In another terminal, generate a read while the capture is active:

bash
mbpoll -a 1 -r 1 -c 1 -t 4 -p 5020 -1 127.0.0.1

Then generate a write so the pcap includes function 0x06 as well:

bash
mbpoll -a 1 -r 1 -t 4 -p 5020 -1 127.0.0.1 42

Return to the capture terminal and stop tshark with Ctrl+C. Force the Modbus/TCP dissector onto port 5020 and filter Modbus frames:

bash
tshark -r ~/ot-ics-lab/modbus-lab.pcap -d tcp.port==5020,mbtcp -Y mbtcp

Trimmed sample from this lab’s capture (0x03 = Read Holding Registers, 0x06 = Write Single Register):

output
    4   0.020389    127.0.0.1 → 127.0.0.1    Modbus/TCP 78  unknown: Trans:     1; Unit:   1, Func:   3: Read Holding Registers. Unable to classify as query or response.
    6   0.021866    127.0.0.1 → 127.0.0.1    Modbus/TCP 77  unknown: Trans:     1; Unit:   1, Func:   3: Read Holding Registers. Unable to classify as query or response.
   14   0.046729    127.0.0.1 → 127.0.0.1    Modbus/TCP 78  unknown: Trans:     1; Unit:   1, Func:   6: Write Single Register. Unable to classify as query or response.
   16   0.047474    127.0.0.1 → 127.0.0.1    Modbus/TCP 78  unknown: Trans:     1; Unit:   1, Func:   6: Write Single Register. Unable to classify as query or response.

The “Unable to classify as query or response” note is common on loopback captures; the function codes still teach the request shape. For GUI capture habits, use the Wireshark and tcpdump lesson. For building and dissecting packets in Python, keep Scapy practice in Python for pentesters and decode Modbus ADUs only against lab traffic.

Example MBAP + PDU hex from a read-holding request in this lab (func=0x03):

text
000100000006010300000001

Broken down: transaction 0001, protocol 0000, length 0006, unit 01, function 03, start 0000, quantity 0001. That is MBAP plus PDU, not “PDU equals MBAP.” Analysis first; writes second, and only on authorized targets.

Stop the Modbus simulator with Ctrl+C in its terminal when you finish the Modbus sections.


CAN security basics

Controller Area Network (CAN) appears in vehicles and some industrial or machine equipment. Frames carry an identifier and a data payload. The bus is fundamentally a broadcast medium: nodes that care about an ID process the frame; others ignore it.

Classical CAN data frames carry up to 8 data bytes. CAN FD extends that to as much as 64 bytes and can use a faster data phase. Neither feature inherently authenticates the transmitter.

Classic CAN does not authenticate the sender at the protocol level. CAN FD increases payload capacity and supports faster data phases, but it does not by itself add sender authentication. Security controls must come from higher-layer protocols, secure gateways, message-authentication schemes, or application-specific protections.


Test CAN frames with vcan

Load the virtual CAN module and bring up vcan0. These steps need sudo once per boot:

bash
sudo modprobe vcan

modprobe prints nothing when the module loads. Create the virtual interface next (skip this line if vcan0 already exists):

bash
sudo ip link add dev vcan0 type vcan

Bring the interface up so cansend and candump can use it:

bash
sudo ip link set up vcan0

Confirm the interface is present:

bash
ip -brief link show vcan0
output
vcan0            UNKNOWN        <NOARP,UP,LOWER_UP>

In one terminal, dump frames with timestamps:

bash
candump -td vcan0

In another terminal, send one frame with ID 0x123 and a four-byte payload:

bash
cansend vcan0 123#DEADBEEF

cansend exits silently on success. Send the identical frame again to model the effect of retransmitting a previously observed frame:

bash
cansend vcan0 123#DEADBEEF

Because you manually construct the second frame, this is a replay concept demonstration rather than a packet-capture and reinjection workflow. Stop candump with Ctrl+C after the second send.

Sample candump output:

output
 (000.000000)  vcan0  123   [4]  DE AD BE EF
 (000.003857)  vcan0  123   [4]  DE AD BE EF

Identical ID and payload appear twice. That illustrates broadcast delivery and why integrity or gateway filtering matters before you trust CAN data in a safety function.


OT pentest safety and rules of engagement

Carry these into every ICS-related RoE discussion:

  • No uncontrolled production testing
  • Agree the process state before any write or download
  • Coordinate with owner/operators (and safety officers when applicable)
  • Document stop conditions and who can call them
  • Prefer passive monitoring and simulators before active writes
  • Separate IT jump hosts from control networks unless the scope says otherwise

Authorized OT work is not limited to a test PLC. Many legitimate engagements are passive network analysis, architecture or configuration review, authenticated asset review, engineering workstation review, or tightly controlled network testing. If safe active testing cannot be supported, restrict the engagement to approved passive observation, configuration/architecture review, or simulator/test-system validation rather than improvising writes against production controllers.


OT security controls

Protocol-specific controls still sit on top of effective basics:

Area Practical controls
Network Segment OT from IT; firewall Modbus/CAN gateways; deny default any-any
Protocol Prefer Modbus Security (TLS/802) or vendor TLS where supported; restrict unused function codes where the device or industrial firewall supports it
CAN Gateways, filtering, application-layer auth where available; physical access control
Operations Asset inventory, anomaly monitoring, change control for engineering workstations
Physical Cabinet locks, port blockers, controlled access to plant floor gear

Integrity and authentication matter most where classic protocols never had them. Segmentation buys time when legacy devices cannot be upgraded quickly.


Troubleshooting

Symptom Likely cause Fix
mbpoll: Connection refused Simulator not listening or wrong port Start modbus_sim.py; confirm -p 5020 and 127.0.0.1
pymodbus import fails in a venv Distro package not on that interpreter Use /usr/bin/python3 or install python3-pymodbus for that environment
Wrong register value vs manual Addressing convention mismatch Confirm vendor docs; try mbpoll -0 for zero-based PDU addressing
modbus-lab.pcap missing Capture never started or wrong directory Run sudo tshark … -w modbus-lab.pcap before mbpoll, then stop with Ctrl+C
ip link add … vcan fails Interface already exists Skip add; run sudo ip link set up vcan0
candump silent Wrong iface or no frames Confirm vcan0 is UP; send with cansend on the same name
tshark shows raw TCP only Port not mapped to Modbus dissector Add -d tcp.port==5020,mbtcp (or your lab port)

Summary

OT and ICS testing is not “IT pentest with weird ports.” Availability, reliability, deterministic behavior, and process safety reshape scope, timing, and which techniques you ever run live. You practiced what is safe on Kali: a local Modbus register change, a repeated write that shows missing freshness (not a capture/replay lab), Modbus traffic analysis with tshark, and identical frames on virtual CAN.

The Modbus Security distinction matters for reports: TLS and certificates on port 802 are not the same as plaintext 502, and PDU is function code plus data while the ADU adds the MBAP header. CAN FD grows payload and speed; it does not authenticate the sender by itself. Write OT stop conditions into the RoE before you touch any real controller, and keep active writes off production unless the engagement explicitly supports them.


References


Frequently Asked Questions

Why is OT pentesting different from IT pentesting?

OT work must account for availability, reliability, deterministic operation, and process safety in addition to confidentiality and integrity. A failed or careless test can stop a line or create an unsafe process state, so scope, timing, and stop conditions are tighter than a typical IT lab scan.

Why is classic Modbus considered insecure?

Many deployments still use Modbus TCP without cryptographic authentication, confidentiality, or message-integrity protection. Any host that can reach the service and speak valid function codes may read or write registers. Modbus Security adds TLS and certificates on port 802; that is a different protocol profile, not a rename of port 502.

What is the difference between a Modbus PDU and an ADU?

The PDU is the function code plus function-specific data. On Modbus TCP, the ADU is the MBAP header plus that PDU. Modbus Security protects the Modbus application traffic with TLS; it does not redefine the PDU fields.

Does sending the same mbpoll write count as a replay attack?

No. Running the same client command again shows that classic Modbus has no application-level freshness token for that operation. A true replay test captures and retransmits a previously observed network transaction.

Does CAN FD authenticate CAN frames?

No. CAN FD increases payload size (up to 64 data bytes) and can use a faster data phase. Sender authentication still requires higher-layer protocols, secure gateways, message-authentication schemes, or application-specific controls.

Can I practice OT attacks on a production PLC?

Only with explicit written authorization, a defined process state, operator coordination, and stop conditions. Prefer simulators and dedicated test systems for active writes. This article’s commands target local software only.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)