DEV Community

TechSavant Security Lab
TechSavant Security Lab

Posted on

What's actually in that OT handover package? Triage substation and plant files offline

 Sooner or later someone hands you a zip. It might be a substation backup, an integrator's "final" engineering project, or a contractor's handover share with 400 files in it. Then they ask: can we accept this?

In IT you would run a scanner against the systems. In OT you usually can't, and often you shouldn't. The devices are fragile, the network is segmented for good reason, and the files themselves are the most complete description of the system you will ever get. A SCD lists every IED, its IP address and every GOOSE message it publishes. A capture shows which station told which PLC to stop. A switch config shows whether Telnet is still open.

So I built a tool that only reads files: OTInspect (MIT licensed; disclosure: I'm the author). This post walks through the five questions I ask of every OT file set, with real output from the tool. Every example uses fabricated sample data shipped in the repo, or public captures.

1. What is in here?

Extensions lie in OT. A .cfg can be a COMTRADE header or a router configuration. A .zip can be a TIA Portal archive. A .xml can be PLCopen, AutomationML or an OPC UA NodeSet. OTInspect identifies files by magic bytes and content (XML namespaces, COMTRADE header layout, configuration syntax) before it falls back to the extension:

$ otinspect scan samples
Total files: 17
  PLC          4
  NetConfig    3
  PCAP         2
  COMTRADE     2
  SCL          2
  Archive      1
  DeviceDesc   1
  ...
Enter fullscreen mode Exit fullscreen mode

Supported files inside ZIP archives, such as contractor packages and TIA .zapXX, are parsed in place, without extracting them to disk and with guards against zip bombs and path traversal. That matters more than it sounds: a handover package is untrusted input, and your analysis machine shouldn't be the first victim.

2. Which devices does it describe, and do the files agree with each other?

Each parser turns its format into assets: IEDs from SCL (vendor, model, configVersion, IP/MAC), controllers and I/O modules from L5X and AutomationML, switches and firewalls from their configurations, hosts from captures.

The interesting findings come from comparing sources:

[HIGH  ] SCL-IP-DUP              Duplicate IP 10.10.1.11 shared by: F87T_Q01, BCU_Q03
[HIGH  ] SCL-GOOSE-APPID-DUP     Duplicate APPID 0001: F87T_Q01/PROT/GoTrip, F21_Q02/PROT/GoTrip
[MEDIUM] XREF-UNDOCUMENTED-HOST  10.30.3.40 serves MMS/S7comm on the wire but appears in no configuration file
Enter fullscreen mode Exit fullscreen mode

The last finding is the one I care about most. A host that answers an OT protocol in a capture but appears in no SCD, PLC export or switch config is either missing from the documentation or shouldn't be there. Neither is acceptable at handover.

Network map from the HTML report

3. Who told the controller to do what?

Most capture tools tell you which protocols were spoken. For a security review, what matters is what was asked. A Modbus read and a Modbus write use the same port. So do an S7 variable read and a PLC stop.

OTInspect decodes the command layer of IEC 60870-5-104, DNP3, S7comm and EtherNet/IP/CIP. Here is a public capture of a real S7 download from the ICS-pcap collection:

[HIGH  ] PCAP-S7-PASSWORD  S7comm: Set/enter PLC password x1 [192.168.178.32 -> 192.168.178.230]
[HIGH  ] PCAP-S7-PROGRAM   S7comm: Request download x1, PI service: insert/activate block (_INSE) x1
[LOW   ] PCAP-S7-RECON     S7comm: Read SZL x5, List blocks x2, List blocks of type x1, Get block info x1
Enter fullscreen mode Exit fullscreen mode

Read those three lines bottom to top and you get the full sequence: identify the CPU (SZL reads), enumerate the program blocks, authenticate, push a block and activate it. That is what an engineer does during commissioning. It is also exactly what an attacker does. The tool can't tell them apart, and it doesn't try to. It tells you who did it, so you can ask whether 192.168.178.32 is an engineering station that was supposed to be doing downloads that day.

Routine polling, such as IEC 104 general interrogation, DNP3 class reads and clock sync, is summarised but not flagged. Otherwise every SCADA capture would drown you in "findings".

4. What changed between revisions?

The most expensive mistake I have seen in IEC 61850 projects isn't a security bug. It's a DataSet that changed while the GOOSE control block's confRev stayed the same. Subscribers keep accepting the messages and map the wrong signals. Nothing alarms until the protection scheme operates on bad data.

$ otinspect diff substation_demo.scd substation_demo_v2.scd
~ [DataSet   ] F87T_Q01/PROT/dsTrip  +1 -0: +PROT/PDIF1.Str.general[ST]
~ [GOOSE     ] F21_Q02/PROT/GoTrip  APPID: '0001' -> '0002'
~ [IED       ] F21_Q02  ip: '10.10.1.12' -> '10.10.1.22'

[HIGH  ] DIFF-GOOSE-CONFREV       GOOSE F87T_Q01/PROT/GoTrip: DataSet changed but confRev stayed '1'
[MEDIUM] DIFF-GOOSE-ADDR-CHANGED  GOOSE F21_Q02/PROT/GoTrip changed APPID; subscribers must be re-configured
[LOW   ] DIFF-HEADER-NOT-BUMPED   Configuration changed but Header version/revision did not
Enter fullscreen mode Exit fullscreen mode

--fail-on high returns exit code 2, so the same check can gate a pull request in a repository of SCL files.

5. How do I report it to someone who thinks in standards?

An auditor doesn't want "Telnet is on". They want to know which requirement it affects. Every finding carries the IEC 62443-3-3 system requirements it gives evidence against and the MITRE ATT&CK for ICS techniques it relates to. The HTML report adds a summary table per SR:

Findings with IEC 62443 and ATT&CK references

A note on ATT&CK if you maintain your own mappings: the ICS matrix has been restructured. Unauthorized Command Message (T0855) now lives under T1692.001, Default Credentials (T0812) under T1694.001, and the firmware techniques under T1693. I only caught this by checking the current matrix instead of my memory, so it is worth re-checking any older spreadsheets.

The same data is exported as SARIF for GitHub code scanning and as a CycloneDX 1.6 bill of materials (devices with their firmware as nested components, plus file hashes). If your team keeps switch configs or SCD files in Git, findings can show up as code-scanning alerts on the exact file.

Engineering notes: making a file tool safe to point at untrusted files

A tool that reads untrusted OT files is itself attack surface. A few things I had to get right:

  • Fail-closed XML. All XML goes through defusedxml. If it is missing, OTInspect refuses to parse XML instead of silently falling back to the standard library parser.
  • Bounded memory. SCD files for large substations reach hundreds of MB. Parsing SCL as a stream, one top-level section at a time, took a 74 MB synthetic SCD from about 413 MB peak RAM to 48 MB, with identical results. Captures are memory-mapped.
  • Fuzzing. Hypothesis feeds arbitrary bytes into the protocol decoders, the PCAP parser and the config parsers. The scanner catches parser exceptions so a scan never stops, but that would also hide bugs, so the fuzz tests call parsers directly.
  • Real data, not just my samples. CI downloads public captures from real PLCs and RTUs and SCL files from OpenSCD, pinned by commit and SHA-256, and checks what the tool decodes from them.
  • Verifiable releases. The Windows executables are built only by GitHub Actions and carry build-provenance attestations (gh attestation verify). Authenticode signing is in progress.

What it doesn't do

Being honest about limits matters more in OT than anywhere else:

  • It reads files. It can't tell you what is actually running on a device today.
  • Commands are decoded per packet, without TCP reassembly, so a command split across segments can be missed.
  • Cisco IOS/Stratix and FortiOS configurations are fully parsed. RUGGEDCOM, Hirschmann and Moxa are covered by heuristic checks until I have anonymized exports to test against.
  • The IEC 62443 mapping helps you prioritise and write up. It is not a compliance verdict.

Try it

pip install "otinspect @ git+https://github.com/lindseynguyen/ReadingDataOT@v0.4.1"
otinspect scan ./handover --html report.html --fail-on high
Enter fullscreen mode Exit fullscreen mode

Windows users can take the portable GUI and CLI from the releases page.

What would help most:

  1. Formats it doesn't handle yet. ABB PCM600, SEL, GE and HiOS exports are on the list.
  2. Anonymized files it gets wrong. There is an issue template, and the contributing guide explains how to strip site data first.
  3. Checks you would run in a pre-acceptance review that are missing.

Repo: https://github.com/lindseynguyen/ReadingDataOT. Stars help other OT engineers find it.

Top comments (0)