modbus: stop variable arrays over-reading into the next ADU - #5067
Open
ArockiaRajamanickam wants to merge 1 commit into
Open
modbus: stop variable arrays over-reading into the next ADU#5067ArockiaRajamanickam wants to merge 1 commit into
ArockiaRajamanickam wants to merge 1 commit into
Conversation
byteCount is a byte count, but FieldListField.count_from expects an element count. For the arrays whose elements are 16-bit registers that made the field consume twice as many items as the PDU actually holds, so when several ADUs arrived in one buffer the array swallowed the header of the following one and returned wrong values. Parsing two concatenated Read Input Registers responses gave registerVal [0x1111, 0x2222, 0x3333, 1, 0, 9], the trailing three shorts being the next ADU's transId, protoId and len, and the second ADU was left unrecoverable. No exception was raised. Divide byteCount by the element size for those arrays. The three arrays of byte-sized elements were already correct and are untouched. Read FIFO Queue is a special case: its byteCount also covers the FIFOCount field, so use FIFOCount, which is the element count the spec provides. ModbusPDU01ReadCoilsResponse additionally derived from _ModbusPDUNoPayload, whose extract_padding discards everything after the PDU, so a following ADU was dropped rather than mangled. Its counterpart ModbusPDU02ReadDiscreteInputsResponse already derives from Packet; match it. Reported and diagnosed by Insanitree in secdev#4096. Fixes: secdev#4096 AI-Assisted: yes (Claude Fable 5, via Claude Code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4096. The diagnosis is Insanitree's, from the issue thread.
The bug
FieldListField.count_fromexpects an element count, but the Modbus PDUs passbyteCount, which is a byte count. For the arrays whose elements are 16-bit registers that is off by a factor of two, so the field consumes twice as many items as the PDU holds and reads into whatever follows. Modbus/TCP is a stream protocol, so several ADUs routinely arrive in one buffer.On master (
5fb7637) and on released 2.7.0, parsing two concatenated Read Input Registers responses:The trailing three shorts are the next ADU's
transId,protoIdandlen. The remaining buffer then starts mid-header, so the second ADU is unrecoverable. Nothing raises, so this is silent data corruption in a dissector people point at ICS traffic.The fix
Divide
byteCountby the element size for the six register arrays.The three arrays of byte-sized elements (
coilStatus,inputStatus, andoutputsValueon Write Multiple Coils) were already correct, since therebyteCountand the element count coincide. They are untouched.Read FIFO Queue is a special case. Its
byteCountis built asp * 2 + 2because the spec has it cover theFIFOCountfield as well, sobyteCount // 2would be wrong by one element. It already carriesFIFOCount, which is exactly the element count, so that is used instead.ModbusPDU01ReadCoilsResponsehad a second, different problem: it derived from_ModbusPDUNoPayload, whoseextract_paddingreturnsb"", so a following ADU was not mangled but discarded entirely. Its structural counterpartModbusPDU02ReadDiscreteInputsResponsealready derives fromPacket, so this looks like an oversight rather than a decision. Changed to match.Verification
I swept all nine
byteCount-driven arrays, checking three things each: a lone ADU still parses, two concatenated ADUs give the right element count, and the second ADU survives intact.The seven were the six register arrays plus Read Coils. Note the issue slightly under-counts the blast radius: it focuses on the register over-read, but Read Coils fails a different way, by dropping the trailing ADU rather than corrupting it.
Three regression tests added to
test/contrib/modbus.uts. The suite goes 164 → 167 passing, and reverting just the source change while keeping the tests fails exactly those three, so they bind to the defect rather than to the implementation:flake8 scapy/contrib/modbus.pyis clean.mypy --ignore-missing-importsreports an identical count before and after, so nothing new was introduced.One thing I did not change
Every class deriving from
_ModbusPDUNoPayloaddrops trailing bytes, so fixed-size request PDUs also lose a following ADU in the same buffer — I confirmed that withModbusPDU01ReadCoilsRequest. Fixing that would mean changing the base class behaviour for 24 classes, which is a design call rather than a bug fix, so I left it alone and scoped this PR to the arrays plus the one response that was inconsistent with its sibling. Happy to follow up if you want the broader change.AI-Assisted: yes (Claude Fable 5, via Claude Code)is on the commit, per CONTRIBUTING. The reproduction, the nine-way sweep and the negative control are all runs I made against this branch.