Snort 2.9.2 marks Snort’s first foray into the world of "Supervisory Control And Data Acquisition", or SCADA. In this release, we have added preprocessors to support the DNP3 and Modbus protocols.
SCADA covers a broad range of networks, from industrial control processes to utility distribution. There are a slew of protocols and devices out there. These networks have some similar characteristics; they involve a central "Master" device that sends commands and reads data from several "Outstation" devices. These outstations are typically small embedded systems, and they may even communicate over serial link to a gateway which passes the messages over TCP/IP.
The following documents can help get you up to speed:
The complete Modbus specifications are free to download, but the DNP3 specs will require a paid membership at www.dnp.org. The DNP3 Primer will be enough for this blog post.
The DNP3 and Modbus preprocessors will decode their respective protocols, check for certain anomalies, and provide rule options for some of the protocol fields. The Snort Manual (XXX: LINK MANUAL, manual.snort.org/nodeXXXXX.html) will act as a reference for preprocessor and rule syntax, while this blog post will highlight some of the tasks you can perform:
Easier Rule Writing:
VRT releases a set of Modbus and DNP3 rules in their "scada.rules" file. Prior to Snort 2.9.2, these rules had to decode the protocol with "content" and "byte_test" rules. This makes for some cumbersome rules.
Here is rule 1:17782, as it was written before the Modbus preprocessor:
alert tcp $EXTERNAL_NET any -> $HOME_NET 502 ( \
msg:"SCADA Modbus write single register from external source"; \
flow:established,to_server; content:"|06|"; depth:1; offset:7; \
reference:url,www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf; \
classtype:protocol-command-decode; sid:17783; rev:1;)
This rule does a content match on the byte containing the function code. The Modbus protocol is easy enough to decode, but new rule options can make the rule much easier to read:
alert tcp $EXTERNAL_NET any -> $HOME_NET 502 ( \
msg:"SCADA Modbus write single register from external source"; \
flow:established,to_server; modbus_func:write_single_register; \
reference:url,www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf; \
classtype:protocol-command-decode; sid:17783; rev:2;)
Similar rules exist for DNP3. Take rule 1:15713 as an example:
alert tcp $EXTERNAL_NET 20000 -> $HOME_NET any ( \
msg:"SCADA DNP3 device trouble"; \
flow:established,to_client; content:"|05|d"; depth:2; \
byte_test:1,&,64,1,relative; content:"|81|"; depth:1; offset:12; \
byte_test:1,&,64,13; reference:url,www.dnp.org/About/Default.aspx; \
classtype:protocol-command-decode; sid:15713; rev:3;)
This rule is checking for DNP3’s "start" bytes (0x0564), checking the direction set in the Link-Layer header, checking for the "response" function code, then finally decoding the "Internal Indicators" field to read the "device trouble" flag. Now, we can let the preprocessor handle the boilerplate!
alert tcp $EXTERNAL_NET 20000 -> $HOME_NET any ( \
msg:"SCADA DNP3 device trouble"; \
flow:established,to_client; dnp3_ind:device_trouble; \
reference:url,www.dnp.org/About/Default.aspx; \
classtype:protocol-command-decode; sid:15713; rev:4;)
The introduction of new rule options will make it easier to tailor Snort to your particular SCADA deployment.
Anomaly Detection:
The Modbus and DNP3 preprocessors will perform some sanity checking on the packets that they inspect. DNP3 packets are interspersed with checksums, and Snort can be configured to alert when they are incorrect. A large number of alerts could indicate a faulty device or network connection.
Both protocols contain multiple length fields, and Snort’s preprocessors need to check them meticuously to make sure that the traffic is valid. For instance, Modbus packets contain an overall length for the PDU, but each Modbus function has a defined format. Let’s take Modbus function #1, "Read Coils", as an example:
Request:
Function code: 1 Byte 0x01
Starting address: 2 Bytes 0x0000 to 0xFFFF
Quantity of coils: 2 Bytes 1 to 2000
(Source: MODBUS Application Protocol, version 1.1b, page 12)
Modbus "Read Coils" requests should always be 5 bytes long. If Snort encounters a "Read Coils" request that is not 5 bytes long, an alert on rule 144:1 (MODBUS_BAD_LENGTH) will be generated.
Now, take a look at the response format:
Response:
Function code: 1 Byte 0x01
Byte count: 1 Byte N
Coil status: N Bytes <Data>
(Source: MODBUS Application Protocol, version 1.1b, page 12)
Here, the response is at least 3 bytes, but could contain up to 258 bytes. However, the Modbus header earlier in the packet contains its own length field. If the byte count in the response does not line up with the original length, then the packet is invalid and Snort will generate an alert.
This type of checking is done in several areas of the Modbus and DNP3 preprocessors. In general, we are looking for situations where invalid traffic could cause implementations to crash if they were not programmed defensively. Alerts may indicate faulty devices, or an attacker’s attempt to find vulnerabilities.
Access Control / Auditing
The new Modbus and DNP3 rule options allow you to keep a log of interesting, yet legitimate, traffic. Suppose your network contains a DNP3 outstation device, and in a typical usage scenario your master station polls this device to read data. Under normal operation, you would not send write requests to this device, so you would like to know if a write request is attempted. This can be accomplished with a simple Snort rule:
alert tcp $EXTERNAL_NET any -> $MY_OUTSTATION_IP 20000 ( \
msg:"Somebody writing to my DNP3 outstation!"; \
flow:established,to_server; dnp3_func:write; \
sid:1000000; )
This rule will alert any time a DNP3 Write request is seen going towards $MY_OUTSTATION_IP. If Snort is running inline, you could even block such requests:
drop tcp !$MY_MASTER_IP any -> $MY_OUTSTATION_IP 20000 ( \
msg:"Dropped DNP3 Restart command not originating from my Master"; \
flow:established,to_server; dnp3_func:cold_restart; \
sid:1000001; )
This rule would drop any packets containing a DNP3 "Cold Restart" command, unless the source IP on the packet matched the IP of your Master station.
Logging Error Conditions
These protocols already provide tools for devices to communicate that they are having trouble. Modbus devices can return Exception codes in place of Function codes, and DNP3 devices can use the "Internal Indications" field. Snort rules can be written to generate alerts when these errors happen.
The following rule will alert if a DNP3 Outstation reports that it has a corrupt configuration:
alert tcp $MY_OUTSTATION_IP 20000 -> $EXTERNAL_NET any ( \
msg:"DNP3 Outstation with corrupt configuration"; \
flow:established,to_client; dnp3_ind:config_corrupt; \
sid:1000002; )
Protocol-Aware Flushing
The Modbus and DNP3 preprocessors take advantage of Protocol-Aware Flushing (PAF), a Stream5 feature that we introduced back in Snort 2.9.1. For information on PAF itself, see the blog post "What Is PAF?" (
http://blog.snort.org/2011/09/what-is-paf.html)
Modbus and DNP3 were both designed for use with serial-link networks, even though they can be carried over TCP/IP. The typical PDU size can be measured in dozens or hundreds of bytes. In addition, these protocols use a binary format that made it cumbersome to write Snort rules. A rule needs to reliably read data at specific offsets within a PDU.
For Stream reassembly to be useful, the reassembled packets had to resemble complete PDUs. Stream5’s original one-size-fits-all reassembly strategy would not be suitable. Consider the following scenarios:
Scenario #1: Multiple PDUs contained in a single TCP segment
|---------------------- Packet #1 ---------------------|
[PDU #1: 50 bytes][PDU #2: 200 bytes][PDU #3: 150 bytes]
The small PDU size of SCADA protocols allows multiple PDUs to get combined into a single TCP segment. Before PAF, a Snort rule would have treated this as a single PDU. PDUs #2 and #3 would have successfully evaded the IDS.
Scenario #2: PDUs not aligned on TCP segment boundaries
|--------- Packet #1 ---------|-------- Packet #2 --------...|
[PDU #1: 50 bytes][PDU #2: 200 bytes][PDU #3: 150 bytes]....
Just like Scenario #1, PDUs #2 and #3 would evade the IDS. This situation could continue as long as PDUs don’t start on packet boundaries.
Scenario #3: TCP small-segment attack
| Pkt #1 | Pkt #2 | Pkt #3 | Pkt #4 | Pkt #5 | Pkt #6|
[PDU #1: 50 bytes][PDU #2: 200 bytes][PDU #3: 150 bytes]
This is the type of attack that previous generations of the Stream preprocessor were designed to defeat. In this scenario, no packet contains a full PDU, and packets need to be reassembled before there is any meaningful detection. However, traditional Stream5 flush points would have taken us back to Scenario #1. The PDUs are so small that it doesn’t make sense to reassemble 400 bytes into a single packet.
PAF solves this problem by letting us statefully inspect Modbus or DNP3 traffic, read the header’s length field, and set Stream5’s flush point accordingly. These new preprocessors require PAF to be enabled (this is on by default in Snort 2.9.2). With PAF, reassembled packets look like this:
Scenario #4: Packets reassembled with PAF
[--- Packet #1 --][--- Packet #2 ---][--- Packet #3 ---]
[PDU #1: 50 bytes][PDU #2: 200 bytes][PDU #3: 150 bytes]
PAF allows us to write our detection code in terms of PDUs, not packets.
DNP3 Transport-Layer Reassembly
The DNP3 protocol presents some new challenges for large content matches. For starters, CRCs are interspersed in between every 16 bytes of data:
[DNP3 Link-Layer Header][CRC][16 bytes data][CRC][ ... data+CRC pairs ... ]
So, if a Snort rule is attempting a content match somewhere in the user data, there is a likely chance that the data will be interrupted by CRCs.
On top of that, DNP3 is a three-layer protocol that provides its own segmentation. While each individual Link-Layer frame has a maximum size of 260 bytes, they can be reassembled into Application-Layer fragments with sizes closer to 2 kB. The DNP3 Transport Layer is the layer in between that provides this segmentation.
Snort’s DNP3 preprocessor will queue up segments and reassemble them, stripping out the CRCs. This provides an Application-Layer message for the "dnp3_data" rule option. When DNP3’s reassembly rules call for segments to be dropped, the Snort preprocessor will generate an alert on rule 145:3 (Bad Sequence Number) or rule 145:4 (Reassembly Reset).
Plans for Future SCADA Support
The Snort team is interested in bringing support for more SCADA protocols. If there are other SCADA protocols that are of interest on your particular network, we’d like to know so we can look into them for possible inclusion in future releases. You can reach us at
snort-team@sourcefire.com. We can always use…
- Protocol Specs
- Traffic captures (sanitized)
- Sample implementations
- Beta Testers
If you run Snort on a network with Modbus or DNP3 traffic, please try the new preprocessors and tell us what you think! We are interested in all constructive feedback. Should you encounter a bug, please use these instructions (
https://www.snort.org/community#bugs) to submit a bug report.
This blog post written by Ryan Jordan, we'd like to thank Ryan for his significant contributions to Snort and Sourcefire and we wish him luck in his future endeavors.