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

A Researcher's Guide to EtherNet/IP & CIP

Vendor: rockwell · Sector: manufacturing · Transport: ethernet-ip · 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

EtherNet/IP carries the Common Industrial Protocol (CIP) over TCP/IP and UDP/IP. It is the dominant protocol of Rockwell/Allen-Bradley automation (ControlLogix, CompactLogix, MicroLogix) and is spoken by thousands of ODVA-member devices — drives, I/O blocks, HMIs, gateways. Two messaging styles: explicit (request/response, TCP 44818) and implicit I/O (cyclic, UDP 2222).

For a researcher EtherNet/IP is a two-layer target with a lot of depth: a fixed 24-byte ENIP encapsulation header (session, length, status) wrapping a CIP request whose richness — a path of logical segments addressing class/instance/attribute, a service-code space, and the Connection Manager’s Forward_Open / Unconnected_Send routing — is where the interesting bugs live. The CIP path and the Unconnected_Send embedded-message are the two highest-value surfaces, and both have recent (2026) Rockwell CVEs.

2. Session & transport model

TCP connect → RegisterSession (0x0065) → session handle → Send RR Data (0x006F) → …

A session handle is assigned by the target in RegisterSession and must ride in the ENIP header of subsequent commands (Unregister Session and Send RR Data require it). A fuzzer completes RegisterSession in connect(), stores the handle, and patches it into outgoing frames — except where the test is a wrong/zero handle. Some commands (NOP, List Identity/Services/Interfaces) need no session.

3. Wire format — ENIP encapsulation + CIP

ENIP header (24 bytes, little-endian): Command (2) | Length (2) | Session Handle (4) | Status (4) | Sender Context (8, echoed) | Options (4, must be 0) | Data. Commands: NOP 0x0001 (no response), List Services 0x0004, List Identity 0x0063, Register 0x0065, Unregister 0x0066, Send RR Data 0x006F, Send Unit Data 0x0070.

Common Packet Format (CPF) inside Send RR Data: Interface Handle (4) | Timeout (2) | Item Count (2) | items {Type(2), Length(2), Data}. Unconnected explicit messaging = a Null Address Item (0x0000, len 0) + an Unconnected Data Item (0x00B2) carrying the CIP request.

CIP request: Service(1, bit7=0) | PathSize(1, in WORDs) | Path(PathSize×2) | Data. Response: Service|0x80 | 0x00 | GeneralStatus(1) | AddlStatusSize(1) | AddlStatus… | Data.

CIP path = logical segments 0x20|type|format — Class (0x20), Instance (0x24), Attribute (0x30), Connection Point (0x2C), with 1/2/4-byte formats (the 2/4-byte forms carry a pad byte). Common targets: Identity 20 01 24 01, Message Router 20 02 24 01, Connection Manager 20 06 24 01.

Key CIP services: Get/Set_Attribute_Single (0x0E/0x10), Get/Set_Attributes_All (0x01/0x02), Multiple_Service_Packet (0x0A), Forward_Open (0x54), Forward_Close (0x4E), Unconnected_Send (0x52), Large_Forward_Open (0x5E). Forward_Open carries connection parameters (RPI, network connection params with a 10-bit size field) and a Connection Path Size + path; Unconnected_Send carries an embedded message length + embedded CIP request + a route path.

4. The fuzz surface

Classification line: ENIP status non-zero or CIP general status non-zero → exception, not crash — except ENIP 0x0001 (invalid command) / 0x0003 (malformed) which disconnect and are crash-worthy. The crash signal is timeout/reset on a well-formed, session-valid request.

Structured vs. coverage-guided: heavily structured — the bugs are CIP-semantic (path-size/embedded-length lies, connection-manager state, service dispatch) reached through a real session. The recent Rockwell path/embedded-length CVEs were found by structured length-mismatch generation, exactly this module’s forte.

5. Known vulnerabilities & bug classes

6. Building a test harness

Robustness responder (scripts/ethernetip_responder.py, port 44819): module-level _next_session_handle() for unique per-connection handles; handles Register/ Unregister, List Identity/Services/Interfaces, NOP, Send RR Data; CIP Get/Set attribute, Forward_Open/Close, Large_Forward_Open, Multiple_Service_Packet, Unconnected_Send; a fake Identity object; sidecar JSONL.

Harness notes:

Live target reality: OpenPLC’s EtherNet/IP is a minimal I/O stub — only RegisterSession + Forward_Open/Close are implemented; List Identity / Send RR Data / Unconnected_Send / Large_Forward_Open all silently time out (not crashes — the server is healthy, verified by a fresh RegisterSession after each category). The implemented Forward_Open/Close path took 36 malformed cases with 0 crashes (a real negative result). A meaningful CIP-object/unconnected-messaging assessment needs a full-stack device (ControlLogix, Moxa MGate), not OpenPLC — an important targeting lesson: confirm the stack implements the surface before trusting a “clean” run.

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/ethernetip/fuzzer.py (10 categories incl. Logix tag + PCCC + the 2026 path/embedded-length CVE cases); responder scripts/ethernetip_responder.py.