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

A Researcher's Guide to FactoryTalk Linx RNA

Vendor: rockwell · Sector: manufacturing · Transport: rna · Project: icsfuzzer

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


1. What it is & where it runs

RNA is the live-data / messaging layer of Rockwell Automation’s FactoryTalk Linx, implemented in RnaDaSvr.dll inside RSLinxNG.exe. It runs on Windows-based FactoryTalk servers and engineering stations — the data-broker that sits between HMIs/historians and the plant network.

It is interesting to a researcher precisely because it is not a classic binary TLV protocol like CIP or Modbus. RNA is a thin binary envelope wrapping XML text payloads (and, on a sibling service, COM-Automation-style typed records). That mix — a lying binary length field on the outside, attacker-controlled XML attributes on the inside — produces two distinct bug families in one protocol.

2. Session & transport model

1. TCP connect (4241).
2. Client → OpenNamespace  (command=XmlCommand, transid=1, session-id= empty)
                            + XML <OpenNamespace> data block.
3. Server → same envelope; response header carries session-id=<N>.
4. Client → subsequent requests set session-id=<N>, increment transid.

The session id is recovered from the first response by scanning the header block for session-id=<N> — so a fuzzer’s connect phase must parse the response envelope, not assume a fixed offset.

3. Wire format

Envelope (same for request and response):

Offset 0:   magic        4 bytes   b'rna\xF2'
Offset 4:   header block  variable  length-prefixed (pad=True)
Offset 4+N: data block    variable  length-prefixed (pad=False)

block(data) = struct.pack('<L', len(data)) + data. A subtlety worth getting right in a generator: when padding is on, the length field reflects the padded length (the pre-pad dlen is dead code in the PoC) — verified by reading the raw source, not a summary. This is the lying-length knob for the binary_corruption category.

Header block — not fixed binary fields. It is flat text: key=value pairs, sorted by key, each null-terminated, concatenated:

"command=XmlCommand\x00session-id=\x00transid=1\x00"

Observed keys: command, transid (increments per request), session-id (empty on the first message, numeric after assignment). Whether the server actually requires sorted order is unverified — itself a header-fuzz question.

Data block — raw XML text for OpenNamespace / ConfigureItems. (The 5241 FTDiagnosticsViewer service instead carries a binary VARIANT-style record — wstr() UTF-16LE length-prefixed strings, vt_str()/vt_i4() typed wrappers — a materially different grammar, out of scope for the 4241 generator.)

4. The fuzz surface

RNA’s two bug families map cleanly onto two layers:

Layer A — the binary envelope. Wrong magic (rna\xF2), lying block-length fields (declared length ≠ bytes present), truncated blocks. This is the same “length field disagrees with payload” pattern that drives parser over-reads everywhere.

Layer B — the XML and header text.

Structured vs. coverage-guided: RNA is a structured-generator target — the known bugs are semantic (a session-state misuse; an XML count used as an allocation size), not deep byte-parser bugs. There is no open-source server to instrument for coverage-guided fuzzing.

5. Known vulnerabilities & bug classes

CVE Port Message Class
CVE-2020-5801 4241 double OpenNamespace on a valid session state-machine / unhandled exception
CVE-2020-5802 4241 ConfigureItems with count="1073741812" CWE-789/CWE-20 attacker-controlled allocation size (operator new)
CVE-2020-5806 127.0.0.1:7153 LoadIconStream local-only per advisory (binding uncertain)
CVE-2020-5807 5241 oversized wide-string log field buffer bug in the different VARIANT grammar

Tenable advisory tra-2020-71 is the CVE-level summary; the PoC scripts carry the actual bytes. Three of the four are network-reachable; CVE-2020-5806’s loopback binding is asserted by the advisory but not confirmed in the PoC (which takes an arbitrary host) — an open question.

6. Building a test harness

With no spec and no test device, the responder is explicitly framing-only: it accepts OpenNamespace, hands back a session-id, accepts ConfigureItems, and returns a well-formed envelope — enough to prove cases connect and parse, not enough to reproduce the real allocator/exception crashes (that needs Rockwell’s server-side logic). Label this limitation loudly so “cases get a well-formed response from the mock” is never mistaken for “confirmed against RSLinxNG.exe.”

Two implementation lessons from building the icsFuzzer module, both caught by the test suite rather than by inspection — generally useful for any text-in-binary- envelope protocol:

Calibration: 56 cases across 6 categories — reported honestly, below the project’s usual 150–600 density because sequence/state_machine turned out smaller once written concretely. Framing-only, so there is no crash baseline to compare against real hardware yet.

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/factorytalk_rna/fuzzer.py (6 categories, 56 cases) with scripts/factorytalk_rna_responder.py as the explicitly framing-only testbed.