Bug #9181
openModbus: a midstream flow picked up from a server response is oriented backwards and is never swapped
Description
Affected version: 8.0.4 (the jasonish/suricata:8.0.4 image)
Setting: stream.midstream: true
Summary¶
When Suricata picks up a Modbus/TCP session midstream and the first packet it sees is a response from the server (source port 502), it takes the server as the client. The flow stays oriented that way for its whole life. The Modbus parser then reads every real request as a response and every response as a request. It raises unsolicited_response events, and modbus keywords with flow:to_server match the server's responses. APPLAYER_WRONG_DIRECTION_FIRST_DATA is never raised, and the flow is never swapped. A session the sensor happens to pick up from a client request is oriented correctly. So with midstream pickup on, the result depends on which packet of a polling cycle the sensor sees first.
Steps to reproduce¶
The attached make_pcaps.py writes two synthetic captures. Both use RFC 5737 documentation addresses (client 192.0.2.10:49152, server 192.0.2.20:502) and have no three-way handshake. Each holds four Read Holding Registers (FC 3) request and response pairs.
modbus-midstream-server-first.pcapstarts with the server's response to a request the capture did not see.modbus-midstream-client-first.pcapstarts with a client request. This is the control.
repro.rules:
alert tcp any any -> any any (msg:"modbus unsolicited response"; app-layer-event:modbus.unsolicited_response; sid:1000001; rev:1;) alert tcp any any -> any any (msg:"modbus FC3 request (to_server)"; flow:to_server; modbus:function 3; sid:1000002; rev:1;) alert tcp any any -> any any (msg:"applayer wrong direction first data"; app-layer-event:applayer_wrong_direction_first_data; sid:1000003; rev:1;)
Run, with the image's default suricata.yaml:
suricata -c /etc/suricata/suricata.yaml -S repro.rules -r modbus-midstream-server-first.pcap \ --runmode=single -k none -l out \ --set stream.midstream=true --set app-layer.protocols.modbus.enabled=yes
Observed¶
| Capture | Flow record | 1000002 (FC 3, to_server) |
1000001 (unsolicited_response) |
1000003 (wrong direction) |
|---|---|---|---|---|
| server first | 192.0.2.20:502 -> 192.0.2.10:49152, app_proto: modbus |
5, all on server-to-client packets | 8 | 0 |
| client first (control) | 192.0.2.10:49152 -> 192.0.2.20:502, app_proto: modbus |
4, on the 4 real requests | 0 | 0 |
In the server-first capture, the five server responses are matched as FC 3 requests, and the four real requests are not. The parser reports every transaction as an unsolicited response in both directions.
Expected¶
When the first data the engine sees on a midstream Modbus flow is server-to-client and shaped like a response, the Modbus probing parser should signal the reverse direction. The flow is then swapped, as happens for protocols whose probe does this, so that to_server is the client's direction and the parser sees requests as requests. SMB has a midstream-specific probe for this purpose.
Code pointers¶
Checked against the suricata-8.0.4 tag.
rust/src/modbus/modbus.rs,modbus_probe: it takes_directionand_rdirbut uses neither. It probes the payload withDirection::Unknownand returnsALPROTO_MODBUS,ALPROTO_UNKNOWNorALPROTO_FAILED, so it can never report that the data arrived in the reverse direction.rust/src/smb/smb.rs,smb_probe_tcp_midstream: called fromsmb_probe_tcpwhenSTREAM_MIDSTREAMis set. It classifies the record as a request or a response and sets*rdirwhen that disagrees with the direction it arrived in.src/app-layer-detect-proto.c,AppLayerProtoDetectPPGetProto: it sets*reverse_flow = truewhen a probing parser returns a valid protocol with anrdirdifferent from the direction probed.src/app-layer.c,TCPProtoDetect: onreverse_flow, for a session withSTREAMTCP_FLAG_MIDSTREAMset andSTREAMTCP_FLAG_MIDSTREAM_SYNACKnot set, it callsPacketSwapandFlowSwapand swaps the direction flags.
So the swap exists for midstream TCP sessions. It needs only the protocol's probe to set rdir, and modbus_probe does not.
A response from a Modbus server and a request from a client share the MBAP header layout. They can still be told apart in common cases: the source port is 502 or another configured detection port and the destination port is not, and some function codes have different body lengths in a request and a response (for example, FC 3's response carries a byte count). A probe that uses the port asymmetry alone, as the detection ports already define, would fix the reported case.
Workaround¶
None found in configuration. The only midstream option in 8.0.4, stream.midstream-policy, takes pass-flow or ignore, and neither changes direction. Adding sp to app-layer.protocols.modbus.detection-ports made no difference. A rule can report the condition once per flow, for example alert tcp any 502 -> any any (flow:to_server,established; app-layer-protocol:modbus; flowbits:isnotset,mb.rev; flowbits:set,mb.rev; ...), but it cannot correct it.
Attachments¶
make_pcaps.py, which writes both captures, using the Python standard library only;modbus-midstream-server-first.pcap;modbus-midstream-client-first.pcap;repro.rules.
Files
No data to display