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

A Researcher's Guide to PROFINET (DCP + PNIO-CM)

Vendor: siemens · Sector: manufacturing · Transport: dce-rpc, raw-ethernet · 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

PROFINET is the dominant Siemens-ecosystem industrial Ethernet protocol — PLCs, distributed I/O (ET 200), drives, and field devices across discrete manufacturing and process automation. It is actually a family of sub-protocols at different layers; two matter most for a network fuzzer:

(Real-time cyclic I/O — PNIO-RT, EtherType 0x8892 with a FrameID — is L2 and latency-bound; out of scope here.) PROFINET p-net was heavily fuzzed by Nozomi in 2025 (10 memory-corruption CVEs in the UDP-RPC layer), so new work should target other stacks or other sub-surfaces rather than re-run that campaign.

2. Session & transport model

DCP: connectionless L2 — Identify-All multicast → devices reply with their name/IP/role; Set services change name/IP/reset.

PNIO-CM: DCE/RPC over UDP. The DCE/RPC layer carries a Bind then a Request (often concatenated in one datagram — a fuzzer must split them into separate UDP sends). The CM interface UUID is DEC401D0-A91B-11D0-96E1-00A024517A00. Operations: Connect (establish an AR), Read/Write (device records by Index), Control, Release. The AR must be of a valid type before records are accessible.

3. Wire format

DCP frame (L2): Ethernet (dst = PROFINET multicast, EtherType 0x8892) + frame-ID + DCP header (ServiceID / ServiceType / Xid / ResponseDelay / DataLength)

PNIO-CM (DCE/RPC over UDP): the DCE/RPC header (version, packet type Request/ Response, flags, object UUID, interface UUID, activity UUID, opnum) + an NDR body. For Read/Write, the body is an IODReadReq/IODWriteReq block carrying a sequence number, AR UUID, API, slot/subslot, and an Index (the record selector) plus a length and data. Device-management indices live in the 0xF000–0xF8FF range (e.g. 0xF000 real-identification, 0xF020/0xF040 records).

4. The fuzz surface

PNIO-CM (IP, the main target):

DCP (L2, privileged phase): option/suboption sweep, BlockLength lies, oversized Set-name values, malformed Identify filters — TLV-abuse territory, but L2-gated and therefore conceptual until run with raw-socket access.

Classification line: a PNIO error status / IODReadRes/IODWriteRes with a non-zero error is an exception, not a crash. Timeout/reset on a well-formed, AR-valid request is the crash signal. (DCP cases generate bytes even without scapy; without L2 access they’re emitted but not sent — anomaly=None.)

Structured vs. coverage-guided: PNIO-CM is a structured-generator target (AR state, Index dispatch, DCE/RPC framing). The memory-safety surface of the UDP-RPC layer is exactly what Nozomi coverage-guided-fuzzed in p-net (RT-Labs) in 2025 — so a coverage-guided campaign should pick a different stack (Siemens’ proprietary, or another open implementation) to avoid re-walking patched ground.

5. Known vulnerabilities & bug classes

6. Building a test harness

Robustness responder (scripts/profinet_responder.py, port 34965, UDP): stateless handle_datagram() — Bind→BindACK, Request→dispatch by opnum; Connect checks AR type (valid 0x0001/0x0002/0x0004/0x0006), Read serves known indices 0xF000–0xF040, Write rejects 0xF000 (read-only); sidecar JSONL.

Harness lessons:

Calibration (software responder): 161+ cases across 8 categories (boundary, binary_corruption, state_machine, sequence, dcp_discovery, dcp_configuration, pnio_connect, record_rw); 97 tests pass. (No standalone research note — this guide is built from the module and its calibration.)

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/profinet/fuzzer.py (8 categories, 161+ cases, DCP generate-only) with scripts/profinet_responder.py (UDP PNIO-CM robustness mock).