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

A Researcher's Guide to IEC 60870-5-104

Vendor: cross · Sector: electric · Transport: iec104-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

IEC 60870-5-104 (“IEC 104”) is the TCP/IP adaptation of the IEC 60870-5-101 telecontrol application layer — the European counterpart to DNP3. It is heavy in European electric power (control-centre ⇄ RTU/IED), water, and gas SCADA.

What makes it distinct from Modbus/DNP3 for a researcher is that it is connection-oriented with real session state: no application data flows until a STARTDT handshake completes, and a sliding-window sequence-number scheme (SSN/RSN) governs every I-frame — the device validates these numbers, so they are a genuine stateful fuzz surface rather than an echoed correlation field. On top of that sits the IEC 101 ASDU with a large TypeID space and timestamped control commands.

2. Session & transport model

TCP connect → STARTDT_ACT (U) → STARTDT_CON (U) → [DATA_TRANSFER]
  → I-frame(ASDU) ⇄ I-frame/S-frame → … → STOPDT_ACT → STOPDT_CON

Three frame kinds, selected by the low bits of control-field byte 1: I-frame (information, carries an ASDU, increments SSN), S-frame (supervisory, pure ACK via RSN), U-frame (unnumbered session control: STARTDT/STOPDT/TESTFR act/con). A fuzzer must complete STARTDT before its I-frame cases are in the DATA_TRANSFER state — and “I-frame before STARTDT” is itself a state-machine test (some devices process it anyway).

3. Wire format — APDU & ASDU

APDU: 0x68 | LEN | CF1 CF2 CF3 CF4 | ASDU…. Start byte always 0x68; LEN counts CF1..CF4 + ASDU (4..253).

Control field:

ASDU: TypeID | VSQ | COT_lo COT_hi | CA_lo CA_hi | InformationObjects….

Key TypeIDs: monitor 1/3/9/11/13/30 (single/double point, measured values, timestamped), control 45/46 (single/double command), 48/50/51 (set-points normalised/scaled/float), 100 (General Interrogation), 101 (counter interrogation), 103 (clock sync, CP56Time2a). TypeIDs 128–255 are vendor; many reserved ranges (64–99 …) should return COT=44.

4. The fuzz surface

Classification line: COT 44–47 (and P/N negative confirmation) are exceptions, not crashes — the direct analogue of Modbus FC|0x80. The crash signal is timeout/reset/no-response on a well-formed, in-state frame.

Structured vs. coverage-guided: firmly structured — the value is in the stateful SSN/RSN and session-machine bugs and the ASDU semantics, all reached only after a real STARTDT. A coverage-guided campaign would instrument an open-source stack (lib60870, below) for the ASDU/APDU byte parser, complementary to the structured state fuzzing.

5. Known vulnerabilities & bug classes

Durable, repeatedly-productive IEC 104 classes (field-level):

6. Building a test harness

Robustness responder (scripts/iec104_responder.py, port 2405): answers STARTDT_ACT→CON, TESTFR_ACT→CON, STOPDT_ACT→CON; returns COT=44 for all I-frames (no process image modelled); closes on bad start byte, LEN<4 / >253, truncated body, I-frame before STARTDT, invalid CF1; sidecar JSONL.

Harness notes:

Calibration baseline (software responder, 537 cases): clean — 69 crash indicators (35 timeout from S-frames/truncated writes, 18 connection_reset from bad start/LEN/CF1, 16 no_response post-close), 0 malformed_response false positives, all explained by 40 expected sidecar closes. On real hardware, a crash indicator with no matching sidecar close is a finding candidate.

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/iec104/fuzzer.py (11 categories, 537 cases); responder scripts/iec104_responder.py.