alosafuzz
ICS/OT protocol security research — fuzzing, scanning, and coordinated disclosure.

A Researcher's Guide to DNP3 (IEEE 1815)

Vendor: cross · Sector: electric · Transport: dnp3-tcp · Project: icsfuzzer

Part of the alosafuzz ICS protocol-guide series — written for fuzzing and vulnerability researchers. Defensive posture: wire format, fuzz surface, and bug classes only; no weaponized exploit or DoS code.


1. What it is & where it runs

DNP3 (Distributed Network Protocol, IEEE Std 1815-2012) is the dominant SCADA protocol of North American electric utilities — substations, RTUs, IEDs — and is heavy in water and oil/gas too. A master polls outstations for telemetry and issues control (open/close a breaker, set an analog output).

For a researcher, DNP3’s defining trait is its three independent layers, each with its own framing and its own bugs: a Data Link Layer (DLL) with a CRC on every block, a Transport Layer (TL) that fragments, and an Application Layer (AL) with a rich Group/Variation object model. That stacking multiplies the parser surface — and the CRC-per-block rule is itself a fault-injection target (does the device actually check it?). An optional Secure Authentication layer (SAv2/5/6) sits on top.

2. Session & transport model

TCP connect → (optional DLL RESET_LINK_STATES → ACK) → DLL USER_DATA(+TL+AL) → RESPONSE

No STARTDT equivalent. Application-layer sequence numbers are loosely enforced unless the request sets CON=1 (confirmation requested) — which is itself a knob: set CON=1 and withhold the confirm, or replay a sequence number.

3. Wire format — the three layers

DLL frame:

05 64 | LEN | CTRL | DST_lo DST_hi | SRC_lo SRC_hi | CRC_lo CRC_hi   (header)
[ up to 16 user bytes + 2-byte CRC ] × N                            (data blocks)

TL header (1 byte): FIR | FIN | SEQ(6). Single segment = 0xC0. Multi-segment reassembly (FIR=1/FIN=0 … never FIN=1) is a memory-exhaustion lever.

AL message: AC | FC | objects. The AC byte is FIR|FIN|CON|UNS|SEQ(4) — CON requests app confirmation, UNS marks unsolicited. FCs include READ (0x01), WRITE (0x02), SELECT/OPERATE (0x03/0x04, the two-step SBO interlock), DIRECT_OPERATE (0x05), COLD/WARM_RESTART (0x0D/0x0E), ENABLE/DISABLE_UNSOLICITED (0x14/0x15), AUTH_REQUEST (0x20).

Object model: GROUP | VARIATION | QUALIFIER | RANGE… | DATA…. The qualifier nibble pair selects range/index encoding — including limited-count forms (0x07 1-byte count, 0x08 2-byte count) that declare how many objects follow.

4. The fuzz surface

DNP3 pays off layer by layer:

Classification line: IIN2.FUNC_NOT_SUPP / OBJ_UNKNOWN / PARAM_ERROR are spec-compliant exceptions, not crashes (same role as Modbus FC|0x80). The crash signal is timeout/reset/no-response — especially on a frame with valid CRCs.

Structured vs. coverage-guided: DNP3 is primarily a structured-generator target (semantic object/qualifier/SAv5 bugs reached without deep byte-parsing), and the CRC-per-block rule means a mutation fuzzer that doesn’t fix CRCs mostly tests “does it reject bad CRC.” For a coverage-guided campaign you’d instrument an open-source stack — but note opendnp3 is archived (see §7), which limits that path.

5. Known vulnerabilities & bug classes

No single headline CVE is cited here; DNP3’s durable, repeatedly-productive classes:

6. Building a test harness

Robustness responder (scripts/dnp3_responder.py, port 20001): no DLL handshake, validates start bytes / LEN≥5 / header CRC / block CRCs / truncation; FC=READ → RESPONSE (FC=0x81, IIN=0x0000); unknown AL FC → IIN2.FUNC_NOT_SUPP; unknown G/V → IIN2.OBJ_UNKNOWN; sidecar JSONL.

Harness notes:

Live target: confirmed robust — DNP3 vs. OpenPLC (opendnp3) across 4 runs, 228 cases, 0 findings; SAv5/SAv6 bypass cases all rejected. A good robustness baseline and a template for differential testing against weaker stacks.

7. Open-source implementations & tooling (and a landmine)

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/dnp3/fuzzer.py (8 categories, 228 cases), live-validated against opendnp3/OpenPLC; responder scripts/dnp3_responder.py.