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

A Researcher's Guide to Schneider UMAS (Modbus fn 0x5A)

Vendor: schneider · Sector: cross · Transport: modbus-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

UMAS (Unified Messaging Application Services) is Schneider Electric’s proprietary engineering protocol for the Modicon line — M221, M340, M580, Quantum, Premium. It is how EcoStruxure Control Expert / Unity Pro / Machine Expert read PLC identity, read/write variables and memory, take an exclusive reservation of the controller, and upload/download the control program.

The researcher’s hook: UMAS is not a separate protocol on a separate port — it rides inside standard Modbus/TCP on port 502 as a single Modbus function code, 0x5A. Allow Modbus to a Modicon PLC and you allow UMAS. That means a UMAS fuzzer can layer directly on an existing Modbus transport (reuse the MBAP machinery, swap the PDU to fn 0x5A) — and the interesting surface is a sub-function dispatch table, a reservation/session state machine, and length-prefixed name/nonce/signature fields riding on a transport you already have.

2. Session & transport model

TCP connect (502) → READ_ID (unauth) → [optional: TakeReservation → session key] → …

No UMAS-specific handshake over the Modbus transport. READ_ID (identity) is unauthenticated across the whole Modicon line. A reservation (UMAS fn 0x10) returns a 1-byte session key that privileged operations must then carry in the PDU’s session byte; on newer PLCs the reservation is gated by a nonce + SHA256-signed message, on older ones (M221) it is effectively ungated (CVE-2018-7790/7791). A fuzzer should not auto-reserve — a held reservation exclusively locks out a live engineer.

3. Wire format

Modbus/TCP MBAP (7 bytes) + UMAS PDU:

MBAP: TID(2) | Proto=0x0000(2) | Length(2) | Unit(1)
UMAS PDU: 0x5A | session/pairing(1) | UMAS-function(1) | body…

Response shape nuance (important for a classifier):

UMAS function codes (sourced subset): 0x01 INIT_COMM, 0x02 READ_ID, 0x03 READ_PROJECT_INFO, 0x04 READ_PLC_INFO, 0x10 TAKE_RESERVATION, 0x11 RELEASE_RESERVATION, 0x12 KEEP_ALIVE, 0x20 READ_MEMORY_BLOCK, 0x22/0x23 READ/WRITE_ VARIABLES, 0x30/0x31 INIT/UPLOAD, 0x33/0x34 INIT/DOWNLOAD, 0x40/0x41 START/STOP_PLC, 0x50 MONITOR, 0x58 CHECK. (Labels for rarer codes are best-effort; the full 0x00–0xFF space is swept regardless.)

4. The fuzz surface

UMAS is flagged the richest parser target of the Schneider/Modicon set:

Classification line: a UMAS 0xFD status (or a Modbus 0xDA) is an exception, not a crash. The crash signal is timeout/reset/no-response, or a 0x5A reply with a status at neither offset 8 nor 9 (state confusion → UNEXPECTED_DATA).

Structured vs. coverage-guided: firmly structured — the surface is semantic (sub-function dispatch, the reservation state machine, nonce/signature length handling) layered on the Modbus transport, with no public Schneider stack to instrument. Black-box structured generation against a real Modicon (or the responder below) is the path.

5. Known vulnerabilities & bug classes

Unauthenticated READ_ID identity disclosure is a design exposure (CWE-306/CWE-200) rather than a parser bug — useful as a liveness/fingerprint oracle, not the fuzz finding.

6. Building a test harness

Robustness responder (scripts/umas_responder.py, port 5021): a safe UMAS state machine — READ_ID → 5A FE + fake identity; TakeReservation → allocate a session key (avoiding 0x00/0xFD/0xFE so it can’t be mistaken for a status), mark reserved; Release → 5A FE; KEEP_ALIVE → ok-if-reserved; unknown fn → 5A FD; non-0x5A fn → a Modbus illegal-function exception; malformed MBAP → close. Sidecar JSONL.

Because UMAS rides Modbus, the fuzzer subclasses the Modbus module and inherits the MBAP send/recv, multi-ADU drain, TID, and reconnect logic verbatim — the classifier just adds the 0xFE/0xFD/0xDA status semantics (accepting the status at offset 8 or 9 to cover both reply shapes). establish_presence() is a no-op (never auto-reserve); reservation-state cases are device-state-changing and gated by the CLI confirmation flow.

Calibration baseline (software responder, 432 cases): clean — 51 crash indicators, all in binary_corruption (33 connection_reset + 14 timeout + 4 no_response, i.e. corrupt/truncated frames), 278 exception_responses (sweep/ refusal paths), and 0 crash indicators from the other five categories (boundary / function_sweep / reservation_state / nonce_signed / sequence) — no false positives.

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/umas/fuzzer.py (UmasFuzzer(ModbusFuzzer), 6 categories, 432 cases) with scripts/umas_responder.py. Derived from the icsScanner → icsFuzzer handoff decompiled-protocols-for-icsfuzzer.md §3.