Nineteen Defects in Modbus, Starting With Seven Bytes That Make a Server Execute a Write Nobody Sent

Modbus carries no credential, so every byte an implementation parses is pre-authentication by construction. We tested four implementations and uncovered 19 defects, including a seven-byte sequence that desynchronises a server's frame parser and then has it perform a write that was never requested.

Modbus carries no credential. There is no field in the frame for one, no handshake before the first request, and nothing in the protocol that distinguishes an engineer's workstation from anything else that can reach the port. A device speaking Modbus does what the frame tells it to, and the frame is whatever arrived.

That makes every parsing defect in a Modbus implementation a pre-authentication defect, and it makes the consequences physical. The registers being written are valve positions, setpoints and motor states.

We tested four implementations and uncovered 19 defects. One of the four came back clean.

Seven bytes, and then a write nobody sent

Product A has the most serious finding in this series so far, and it is worth following carefully because the mechanism is not a memory safety bug.

Its frame parser is held by the transport rather than by the connection. One parser, one 256-byte buffer, one state machine, shared across everything that arrives on that transport.

A seven-byte sequence puts that state machine into a state the transport never resolves. From then on the parser's idea of where a frame begins is offset from where frames actually begin, permanently, and it never recovers on its own.

The consequence is not that parsing stops. It is that parsing continues against the wrong boundaries. The bytes of subsequent legitimate requests get re-divided into frames that nobody sent, and one of those synthesised frames is a write. A valid request from a legitimate client, re-cut at the wrong offset, becomes a command the device executes.

The same product has the serial version. Its serial listener handles one of the parser's three terminal states, so three bytes put it into one of the other two and the server stops answering, silently and permanently.

And it has the client-side mirror. One byte in a reply wedges the client's response parser the same way. That one is reachable by a compromised device or by anything on the wire between, and the protocol's own security extension authenticates the peer rather than the frame, which does not help here.

Two counts that must agree, never compared

A Modbus multi-write request states its size twice: a quantity of items, in sixteen bits, and a byte count, in eight. The specification fixes the relationship between them.

Product A never checks it. The decoder sizes its array from one of the two numbers and iterates on the other, and when the mismatch is finally noticed the write has already been partly applied and nothing rolls it back. A request that the device ultimately refuses still changed the process image.

Product C has the same class in a cleaner form: one of its write handlers commits the write and then refuses the request. The caller receives an exception response, and the registers have already changed.

This is the defect to take away from the family if you only take one. In a protocol that moves physical state, a refused request that has already taken effect is worse than an accepted one, because the client's own record says it did not happen.

Counts read as signed, lengths trusted as given

Product A's request parser reads a byte count with a call that returns a signed value, so any count above 127 arrives negative and becomes a negative slice length. The response parser in the same library masks the same field correctly, which makes the asymmetry the finding.

The same product trusts the length field in the TCP framing header with no lower bound and no timeout above it, so a declared length below the header's own size and a declared length that never arrives are both accepted.

Product B is embedded firmware, and its integrity check is the problem. The routine that validates the frame's checksum copies the frame onto the stack first, sized from an unchecked sixteen-bit length. The check that exists to reject corrupt frames is reached before anything has established that the frame fits.

Product C has the framing assumption that every network protocol eventually gets caught by: it treats one read from the socket as one request. Two requests that arrive in the same segment mean the second is discarded. It also builds its response with two independent length fields that truncate separately, from a single attacker-controlled value.

A checksum that is only ever computed

Product A's serial server never verifies the checksum it was sent.

The checksum routine exists and has four callers. All four are outbound or on the client side: the server computes the checksum for its own replies, the client computes it for its requests and verifies it on responses. Nothing verifies it on an inbound request.

On a serial line this matters more than it sounds. The checksum is the only thing standing between electrical noise and an executed write, and it is the entire integrity story the protocol has.

Replies that say more than they were asked

Product B builds its coil read reply in a stack buffer that is never initialised, and the handler only ever sets bits that should be on. Every bit that should be off is whatever was on the stack, so the reply carries stack contents out to the client. The same product writes response framing past the maximum response length it was given, and reads past the frame's data buffer in a multi-coil write after validating only a minimum length.

Product A turns error handling into an amplifier: every malformed frame is treated as a server-side fault and logged with a full stack trace. The cost per malformed byte sequence is paid by the device.

Product C serves every connection from a single worker that writes responses synchronously, so one client that stops reading holds that worker and every other client waits behind it.

The one that came back clean

Product D produced nothing, and the audit calls it the most thoroughly validated of the five fieldbus surfaces in the same product family.

It has one property that matters more than the clean result: it serves exactly one connection at a time. That is not a defect, and it is the single most important thing to know before pointing a campaign at it, because a run that opens a second connection gets nothing and may conclude the wrong thing about the first.

Finding these with Penzzer

Modbus is reachable two ways and both are driven.

Over the network, select the INET4 or INET6 module and choose Modbus. A run exercises the framing header and its transaction, protocol and length fields, the unit identifier, the function codes including the ones nothing implements, and the request bodies with their addresses, quantities and byte counts.

Over a serial line, select the Serial module. Every public function code is driven, including the bit and register reads, the writes, the serial line diagnostics, file record access, the FIFO queue read and device identification over the encapsulated transport. Each is fuzzed with bit-flip, truncation, garbage insertion and byte-swap mutation of the request, illegal function code and address injection, and mutation of the framing envelope itself: a corrupted checksum, a byte-swapped checksum, a frame truncated below the four-byte minimum, and a frame padded past the 256-byte maximum. Frame timing follows the standard's character-time rules at the configured baud rate rather than fixed delays, so a device is not misjudged for our own framing.

The ASCII framing is driven in its own terms, because it is the second transmission mode of the same standard: a corrupted longitudinal redundancy check, hexadecimal spelled in lower case where the standard requires upper, an odd number of hexadecimal characters so the frame cannot decode to whole octets at all, a missing start character, and a frame left without its terminator. A reply is judged through the three gates the mode adds before a message exists, each reported separately, so a device that wrongly accepts one of them is distinguished from one that wrongly accepts another.

Nine attack modules run alongside on the network side:

| Module | The shape it provokes | | --- | --- | | Mbap header abuse | The framing header's own fields, including a declared length below the header it sits in | | Length field mismatch | A declared length that is not the length of what arrived, in either direction | | Truncated adu | Requests ending part way through every field in turn | | Write request field abuse | A quantity and a byte count that disagree, and the partial write that follows | | Read request field abuse | Addresses and quantities at and past the limits of the process image | | Illegal function codes | Codes the specification reserves, and codes the device does not implement | | Per connection state reuse | State from one request still present in the next, and parser state that survives a malformed frame | | Diagnostics and device id abuse | The diagnostic subfunctions and the identification request, which are frequently the least exercised handlers | | Connection exhaustion | Connections held open, and a worker held by a client that stops reading |

‍

Because the headline defect in this article is parser state surviving across frames, the run sends a known-good request after each case and checks that the answer is the answer to that request, rather than only checking that something came back.

What this article is, and is not

The 19 defects above are a sample of what this kind of testing surfaces, not a description of Penzzer's Modbus coverage. They are what four specific implementations happened to get wrong.

The attack library spans defect classes well beyond what this article shows: memory safety, information disclosure, resource exhaustion, availability, hangs and algorithmic complexity, input validation, protocol state logic, type and format handling, and error handling. Across the 107 protocols with a protocol-aware target, a run also generates its test cases from the structure of the protocol itself, walking each field through the boundaries where parsers break.

The same limit applies in reverse. What a test looks for is a property of the test, not a claim about your device.

If you have Modbus on a plant network

The usual answer to Modbus security is that the network is segmented. Take that as given and ask the questions anyway, because every defect here is reachable by anything already inside the segment, including the engineering laptop and the historian.

Four questions. Is your frame parser per connection, or does one connection's malformed frame change how the next connection's bytes are divided? When a multi-write request is refused, is the process image guaranteed unchanged? Does your server verify the checksum on inbound frames, or only compute it on outbound ones? And when two requests arrive in the same read, do you handle both?

The first of those is the one that produced a write nobody sent, from seven bytes, on a device that answers anyone.

Other Post
Uncover Hidden Vulnerabilities

Identify security flaws before attackers do, automatically and at scale with Penzzer's intelligent fuzzing engine.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.