DNP3 is a complex SCADA protocol with 3 layers.
The link and transport layers for DNP3 are identical for the client (master) and server (outstation). The same test cases can be applied to both at these layers.
The application layer contains a myriad of object types defined by a group and variation. The semantic meaning of these objects depends on the function code (e.g. READ, WRITE, OPERATE) and the receiver (client/server).
DNP3 has an unsolicited reporting mode whereby the outstation initiates communications. This is very useful for testing the master response parsing without having to wait for requests.
The DNP3 fuzzer provides good coverage of the DNP3 link, transport, and application layers. Specialized test cases are provided for each layer and some targeted test cases are provided for known failure points within layers such as transport reassembly buffer overflows.
There are some known gaps within the current version. Notably the following object groups are not explicitly modeled:
- Group 0 - Device Attributes
- Group 70 - File transfer / free-form qualifier 0x5B
- Group 80 - Datasets
- Group 120 - Secure authentication objects
This is not to say that Aegis cannot find bugs related to this functionality. The app-rand-request and app-rand-unsol procedures produce pseudo-random ASDUs with many unknown function codes and qualifiers. It is just less likely that bugs in these objects will be found because they are not yet directly modeled in the app-request and app-unsol procedures.
DNP3 tests optionally use a feature of the link layer to identify if a target has failed. After every attack frame, the fuzzer sends a link layer request and expects a response.
The fuzzer no longer validates the response, it just expects some valid link layer frame to indicate the target is still alive. You can configure two types of health checks:
- LINK_STATUS (default)
Most implementations will work well with the defaults. Some non-conformant implementations may require the alternative health type.
The fuzzer can perform some automatic handshaking in response to certain messages. This keeps the target happy, particularly during the master startup sequence.
- UNCONFIRMED_USER_DATA - The fuzzer parses the application layer header and if the frame is any function code other than RESPONSE or UNSOLICITED_RESPONSE it responds with a NULL application response with the same sequence number.
- CONFIRMED_USER_DATA - The fuzzer first responses with a link layer ACK and then handles the message the same as UNCONFIRMED.
- RESET_LINK_STATES - The fuzzer responses with an ACK.
- REQUEST_LINK_STATES - The fuzzer responses with a LINK_STATUS.
A number of DNP3 specific parameters are available at the module level.
src - Source address - The link-layer source address. This is address of the fuzzer itself.
dest - Destination address - The link-layer source address. This is address of the fuzzer itself, i.e. who you are pretending to be.
master - Link-layer master bit - This setting configures the link layer 'master' bit for fuzzing masters or outstations (default).
retries - Number of health-check retries - The number of attempts the fuzzer will make to query the target with a health check before deciding it has failed.
timeout - Health-check timeout - The timeout (in milliseconds) for reading a link layer frame from the target during a health-check.
healthType - Health-check type - Enumeration [LinkStatus, ResetLink, None] that defines what kind of health check to use.
healthMode - Health-check mode - Enumeration [Before, After] that determines when the health check occurs relative to the test frame.
link - Link layer (outstation or master) - Fuzzes the link layer using many permutations of control octets, lengths, and special addresses.
transport - Transport layer (outstation or master) - Fuzzes the transport layer with unexpected lengths, sequence numbers, and over-sized ASDUs.
app-request - Application layer requests (outstation) - Fuzzes the application layer with a large number of function codes, qualifier codes, and malformed contents. Explicitly sends start/stop/count values likely to cause integer overflows.
app-unsol - Application layer unsolicited (master) - Fuzzes the application layer with a large number of unsolicited responses (master)
rand-app-request - Randomized application layer requests (outstation) - Fuzzes the application layer with semi-random requests. Capable of finding some bugs in functions, qualifiers, and objects not explicitly modeled elsewhere.
rand-app-unsol - Randomized application layer unsolicited responses (master) - Fuzzes the application layer with semi-random unsolicited responses. Capable of finding some bugs in qualifiers and objects not explicitly modeled elsewhere.
app-octet-unsol - Unsolicited 0-length octet strings (groups 110-113) (master) - Tests for a specific, but common, bug in the handling of Group 110-113 variation 0
app-octet-write - Unsolicited 0-length octet strings (groups 110-113) (outstation) - Similar to app-octet-unsol, but performs a WRITE of a large numbers of 0-length octet string headers
Your Aegis installation of contains recommended test plans for DNP3 outstations and masters.
In most cases, the only parameters you need to adjust will be the source and destination link layer addresses of the target, and possibly the host (IP) parameter if you're testing an external target. Refer to the parameters section if you need to adjust timeouts or health checking.
The recommended test plan repeats some of the procedures with different fill or random seeds. It is recommended that you follow the plan for maximum efficacy, but on slow DNP3 implementations, this could take a long time. Some DNP3 implementations can handle hundreds of test cases per second, while others only seem to handle a couple dozen. It may be worth figuring out why the implementation is slow to respond to health checks or requests. You might consider running additional random seeds besides zero if your have enough time.
The last test case in each plan generates random application layer data within appropriate encapsulation. It is arbitrarily set to 1,000,000 iterations. Run as many iterations as you can tolerate with the speed of your device.