modbus-protocol
theoryLesson 1 — The Modbus Protocol
History and Context
Modbus was created in 1979 by Modicon (now part of Schneider Electric) for communication between programmable logic controllers over RS-232 serial lines. The design goal was simple: a reliable, deterministic master/slave protocol for factory floor automation. Security was never a consideration — the assumption was that industrial networks were physically isolated from everything else.
That assumption has been wrong for decades.
Modbus is now the most widely deployed industrial protocol on the planet. It runs in power generation and distribution, water treatment plants, oil and gas pipelines, building HVAC systems, manufacturing lines, and rail infrastructure. A 1979 serial protocol is controlling critical infrastructure in 2026, and it has never gained authentication, encryption, or any integrity verification mechanism. That is the attack surface.
Protocol Variants
Modbus RTU is the original serial variant. It runs over RS-232 or RS-485 (differential pair, supports multiple devices on the same cable). The frame uses binary encoding with a CRC-16 checksum appended. Physical access or access to a serial-to-TCP bridge is required to reach RTU devices.
Modbus ASCII is a variant where the same frame is encoded in ASCII hex, typically used on RS-232 connections. Less common, slower, uses LRC checksum instead of CRC.
Modbus TCP is the Ethernet adaptation introduced in 1999. The RTU PDU is wrapped in a 6-byte header called the MBAP (Modbus Application Protocol header) and carried over TCP port 502. There is still no authentication, no encryption, and no session concept beyond the TCP connection. This is the variant you will encounter on networks and on the internet.
Protocol Model
Modbus is a pure request/response protocol. One master (client in TCP terminology) initiates all transactions. One or more slaves (servers) respond. Slaves never send unsolicited data.
In Modbus TCP: - The client opens a TCP connection to port 502 - The client sends a request frame - The server responds - The connection may be kept open or closed after each transaction
There is no handshake. There is no login. If you can reach port 502, you can read and write the device. This is not a misconfiguration — it is the protocol by design.
MBAP Header Structure
Every Modbus TCP frame starts with a 6-byte MBAP header followed by the PDU (Protocol Data Unit):
+------------------+------------------+------------------+------------------+
| Transaction ID | Protocol ID | Length | Unit ID |
| 2 bytes | 2 bytes (= 0x0000)| 2 bytes | 1 byte |
+------------------+------------------+------------------+------------------+
| Function Code | Data ... |
| 1 byte | |
+--------------------------------------------------------------------------- +
- Transaction ID: Client-assigned, echoed by server. Allows matching responses to requests.
- Protocol ID: Always 0x0000 for Modbus.
- Length: Number of bytes following, starting from Unit ID.
- Unit ID: Identifies the slave device (equivalent to RTU address). Range 1–247. Value 255 is sometimes used as a broadcast or "ignore" value depending on implementation.
- Function Code: Specifies the operation (read coils, write register, etc.).
Data Model
Modbus organizes device data into four tables:
| Table | Prefix | Type | Access | Typical Use |
|---|---|---|---|---|
| Coils | 0x (00001–09999) | 1-bit | Read/Write | Digital outputs: relay on/off, valve open/close |
| Discrete Inputs | 1x (10001–19999) | 1-bit | Read-only | Digital inputs: pushbutton state, limit switch |
| Input Registers | 3x (30001–39999) | 16-bit | Read-only | Analog inputs: temperature, pressure, flow rate |
| Holding Registers | 4x (40001–49999) | 16-bit | Read/Write | Setpoints, configuration, control parameters |
Addressing note: Modbus PDUs use zero-based addressing. Register 40001 in HMI display corresponds to address 0 in the PDU. Register 40011 is address 10. This discrepancy causes constant confusion. When you read address 0 with FC03, you're reading what the HMI calls register 40001.
Register size: Each register holds 16 bits (unsigned, 0–65535). Multi-word data types use consecutive registers: - 32-bit integer: 2 registers (big-endian or little-endian, device-dependent) - 32-bit float (IEEE 754): 2 registers - 64-bit values: 4 registers - ASCII strings: each register holds 2 characters
Function Codes
Function codes are single-byte identifiers that define the operation. The attacker-relevant subset:
| FC | Hex | Name | Description |
|---|---|---|---|
| 01 | 0x01 | Read Coils | Read 1–2000 coil states |
| 02 | 0x02 | Read Discrete Inputs | Read 1–2000 discrete input states |
| 03 | 0x03 | Read Holding Registers | Read 1–125 holding registers |
| 04 | 0x04 | Read Input Registers | Read 1–125 input registers |
| 05 | 0x05 | Write Single Coil | Write one coil: 0xFF00 = ON, 0x0000 = OFF |
| 06 | 0x06 | Write Single Register | Write one holding register |
| 15 | 0x0F | Write Multiple Coils | Write 1–1968 coils in one request |
| 16 | 0x10 | Write Multiple Registers | Write 1–123 registers in one request |
| 43 | 0x2B | Encapsulated Interface Transport | Includes Read Device Identification (MEI type 14) |
Error responses: If a request fails, the server returns the function code with the high bit set (e.g., FC03 error = 0x83) followed by an exception code. Exception code 0x02 means "illegal data address" — the address does not exist in this device. Exception code 0x01 means "illegal function" — FC not supported. These responses are useful for mapping the device's register space.
FC43 — Read Device Identification: This function code encapsulates the MEI (Modbus Encapsulated Interface) transport. MEI type 0x0E is the Device Identification sub-function. It returns structured objects identifying the device:
| Object ID | Name | Content |
|---|---|---|
| 0x01 | VendorName | Manufacturer name |
| 0x02 | ProductCode | Model number |
| 0x03 | MajorMinorRevision | Firmware version |
| 0x05 | VendorURL | Support URL |
| 0x06 | ProductName | Human-readable product name |
Not all devices implement FC43. Those that do hand you a complete device fingerprint without any authentication.
Why This Protocol Is Still Everywhere
Modicon designed Modbus for a world of dedicated serial cables running at 9600 baud between a single master and a handful of slaves in a sealed cabinet. The protocol works reliably in that environment. Retrofitting authentication onto a protocol that runs on PLCs with 32KB of RAM and no operating system is genuinely difficult.
The installed base is enormous. Replacing Modbus-capable devices means replacing hardware that costs tens or hundreds of thousands of dollars per installation, requires recertification for safety-critical applications, and involves production downtime. Many of these systems have 20-30 year operational lifespans.
The result: a protocol designed with no security, widely deployed on networks connected to the internet, controlling physical processes that affect people's lives. Understanding it is not optional for anyone working in OT/ICS security.
Key Takeaways
- Modbus TCP runs on port 502 with no authentication and no encryption
- Four data tables: coils (1-bit RW), discrete inputs (1-bit RO), input registers (16-bit RO), holding registers (16-bit RW)
- FC03 reads holding registers; FC05/FC06/FC16 write coils and registers
- FC43 returns device identification — vendor, model, firmware version
- PDU addresses are zero-based; HMI displays add 40001 offset for holding registers
- The protocol is by design unauthenticated; exposure on routable networks is the real vulnerability