A purely passive, offline OT security tool that reads captured network traffic (PCAP/PCAPNG) and identifies RTUs, FRTUs, and IEDs together with their specific security vulnerabilities and misconfigurations — without ever sending a packet to the network.
- Overview
- Why Passive Scanning?
- Supported Protocols
- Supported Vendors & Device Types
- Vulnerability Detection
- Installation
- Usage
- Output Formats
- Detection & Analysis Pipeline
- Project Structure
- Architecture Deep-Dive
- Vulnerability Reference
- Risk Scoring Model
- Vendor Fingerprinting
- Protocol Analyzer Details
- Extending the Scanner
- Limitations
- Legal & Ethical Use
- Dependencies
RTUs (Remote Terminal Units) and FRTUs (Feeder Remote Terminal Units) are the workhorses of electric grid automation, water treatment, pipeline monitoring, and other critical infrastructure. Unlike PLCs, they:
- Often implement IEC 60870-5-104 or DNP3 as primary SCADA protocols
- May participate in IEC 61850 substation automation (GOOSE protection signalling at Layer 2, MMS configuration over TCP/102)
- Are frequently deployed in harsh environments with limited patching capability
- Run on embedded firmware with long service lives (10–25 years)
This scanner helps security teams identify:
| What | How (passive) |
|---|---|
| RTU/FRTU device inventory | Protocol fingerprinting, OUI lookup |
| Missing Secure Authentication | Absence of DNP3 SA function codes, no IEC 62351 |
| Unencrypted control channels | Cleartext IEC-104, MMS without TLS detected |
| GOOSE security weaknesses | Simulation flag, low TTL, confRev drift |
| Unsafe command patterns | Direct Operate (bypass SBO), file transfers |
| Network hygiene issues | Multiple masters, excessive peers |
Active scanners send probe packets. In OT environments this is dangerous:
- Protection relays can misinterpret unsolicited packets as control commands
- DNP3 outstations may trip into fault mode on malformed requests
- GOOSE publishers may flood on unexpected traffic bursts
- IEC-104 sessions are single-master — injected connections break live comms
This scanner reads a PCAP file captured via a SPAN port, network TAP, or historian capture and performs all analysis offline. Nothing is sent to the network. The same PCAP used for forensic investigation can be fed directly into this scanner.
| Protocol | Transport | Port(s) | Notes |
|---|---|---|---|
| DNP3 | TCP, UDP | 20000, 10001-10002 | Stateful; tracks SA, control commands |
| IEC 60870-5-104 | TCP | 2404 | Stateful; tracks masters, command types |
| IEC 61850 MMS | TCP | 102 | ASN.1 BER; identifies LN names |
| IEC 61850 GOOSE | Ethernet (EtherType 0x88B8) | — | Layer-2; full PDU parsing |
| IEC 61850 SV | Ethernet (EtherType 0x88BA) | — | Layer-2; presence detection |
| Modbus/TCP | TCP | 502 | MEI device identification |
| SEL Fast Message | TCP | 702 | SEL-exclusive; FC 0xB0 flagged |
| Omron FINS | UDP | 9600 | Omron-exclusive |
| MELSEC MC Protocol | TCP | 5006, 5007 | Mitsubishi-exclusive; 3E/4E frames |
| OPC-UA | TCP | 4840 | Port tracking only |
| ICCP / TASE.2 | TCP | 2000, 2001 | Port tracking only |
| EtherNet/IP | TCP | 44818 | Port tracking |
| BACnet/IP | UDP | 47808 | Port tracking |
| Niagara Fox | TCP | 1911 | Port tracking |
| Vendor | Method | Device Types Identified |
|---|---|---|
| ABB | OUI, DNP3/Modbus strings, GOOSE gcbRef (REF, REC, REL, RED, RET, RAR) | RTU560, RTU500, REF615, REL670 |
| GE Grid Solutions | OUI, CIP vendor ID, GOOSE gcbRef (P64, T60, L90) | D20MX, D200, UR-series |
| Siemens | OUI, protocol strings, GOOSE gcbRef (7SL, 7SD, 7UT, 7SA) | SICAM RTU, SIPROTEC |
| Schneider Electric | OUI, Modbus MEI, GOOSE (PRO, P14, P24) | SCADAPack, Easergy, MiCOM |
| SEL (Schweitzer) | SEL Fast Message (exclusive), OUI, GOOSE (SEL) | SEL-3505, SEL-651R, SEL-421 |
| Emerson | OUI, protocol strings | ROC-800, ControlWave, Bristol |
| Honeywell | OUI, protocol strings | RTU2020, Experion |
| Eaton / Cooper | OUI | FRTU devices |
| Noja Power | OUI, protocol strings | RC-10 FRTU |
| Landis+Gyr | OUI, protocol strings | Advanced Metering devices |
| Mitsubishi Electric | MELSEC MC (exclusive) | MELSEC RTU series |
| Omron | FINS (exclusive) | CS1/CJ RTUs |
| Yokogawa | OUI, protocol strings | Field controllers |
| Hirschmann / Belden | OUI | RUGGEDCOM gateways |
| Rockwell Automation | OUI, EtherNet/IP | RTU/PLC hybrids |
| Device Type | Description |
|---|---|
| RTU | Remote Terminal Unit — SCADA field device (DNP3, IEC-104) |
| FRTU | Feeder RTU — Distribution automation device |
| IED | Intelligent Electronic Device — Protection/control relay |
| Relay | Protection relay (SEL, Siemens SIPROTEC, GE Multilin) |
| Gateway | Protocol gateway or RTU concentrator |
Unlike the PLC scanner which focuses on device identification, this scanner performs active vulnerability assessment from passive traffic. Findings are generated when PCAP evidence confirms:
| ID | Severity | Trigger Condition |
|---|---|---|
| RTU-DNP3-001 | High | No SA challenges (FC 0x20/0x21/0x83) ever seen in session |
| RTU-DNP3-002 | Critical | Control commands (FC 3–6) sent without preceding SA exchange |
| RTU-DNP3-003 | High | Direct Operate (FC 5) used — bypasses Select-Before-Operate |
| RTU-DNP3-004 | High | Cold/Warm Restart (FC 13/14), Stop/Start App (FC 17/18) sent |
| RTU-DNP3-005 | Critical | File Open (FC 25) observed — firmware injection risk |
| RTU-DNP3-006 | High | More than 2 distinct master IPs talking to the outstation |
| RTU-DNP3-007 | Medium | DNP3 session observed over UDP (replay-injection risk) |
| ID | Severity | Trigger Condition |
|---|---|---|
| RTU-104-001 | High | IEC-104 APDU seen without a TLS handshake (IEC 62351-3) |
| RTU-104-002 | High | More than 2 master IPs open STARTDT sessions |
| RTU-104-003 | Critical | Control commands (type 45/46/47/48–50) observed in cleartext |
| RTU-104-004 | Medium | Clock Sync (type 103) not preceded by any authentication |
| RTU-104-005 | Low | General Interrogation (type 100) requested more than 20 times |
| ID | Severity | Trigger Condition |
|---|---|---|
| RTU-61850-001 | Critical | GOOSE frames have no IEC 62351-6 security tag (Reserved1 ≠ 0) |
| RTU-61850-002 | Critical | GOOSE simulation = TRUE bit observed in live traffic |
| RTU-61850-003 | Medium | GOOSE timeAllowedToLive ≤ 2000 ms detected |
| RTU-61850-004 | Medium | GOOSE confRev value changes across packets |
| RTU-61850-006 | High | MMS (TCP/102) session detected without TLS (IEC 62351-4) |
| ID | Severity | Trigger Condition |
|---|---|---|
| RTU-GEN-001 | High | Unencrypted industrial protocol(s) exposed on this device |
| RTU-GEN-002 | Medium | Three or more different protocols detected on one device |
| RTU-GEN-003 | Low | Industrial ports open but no protocol payload successfully parsed |
| RTU-GEN-004 | Medium | More than 10 distinct IP peers communicating with this device |
- Python 3.8 or later
- One of: scapy (preferred) or dpkt (fallback)
pip install -r requirements.txtOr install individually:
pip install scapy>=2.5.0 dpkt>=1.9.8 colorama>=0.4.6Windows note: On Windows, scapy requires WinPcap or Npcap for live capture. For offline PCAP reading (the intended use case), Npcap is not required — scapy reads files natively.
For best results, capture traffic from:
| Collection point | Protocols captured |
|---|---|
| SPAN / mirror port on OT switch | All IP protocols + Layer-2 GOOSE/SV |
| Network TAP (passive) | All IP protocols + Layer-2 GOOSE/SV |
| RTU serial port converter TAP | DNP3 serial / IEC-104 serial |
| Historian / data concentrator | IP-transported protocols (DNP3, IEC-104) |
GOOSE requires Ethernet frame access. If your capture tool discards Layer-2 frames before IP, GOOSE traffic will not be captured. Use a physical TAP or a mirror port that includes non-IP multicast traffic.
python rtu_scanner.py substation_traffic.pcapProduces three files: rtu_scan_results.json, rtu_scan_results.csv, rtu_scan_results.html.
python rtu_scanner.py capture.pcapng -f html -o substation_2024python rtu_scanner.py traffic.pcap -vPrints each protocol detection as packets are processed — useful for debugging detection logic.
python rtu_scanner.py traffic.pcap --min-packets 5Suppresses devices seen in fewer than 5 packets (reduces noise from broadcast/ARP).
python rtu_scanner.py traffic.pcap -f json -o findings
python python-script.py findings.json # post-process with your toolingusage: rtu_scanner.py [-h] [-o PREFIX] [-f {json,csv,html,all}] [-v] [--min-packets N] PCAP_FILE
positional arguments:
PCAP_FILE Path to the .pcap / .pcapng capture file
optional arguments:
-h, --help Show this help message and exit
-o PREFIX, --output PREFIX
Output filename prefix (default: rtu_scan_results)
-f {json,csv,html,all}, --format {json,csv,html,all}
Report format (default: all)
-v, --verbose Print per-packet protocol detections
--min-packets N Minimum packet count to include a device (default: 1)
Full machine-readable report, one object per device. Includes all protocol detection details, all vulnerability findings (with evidence, remediation, and references), and risk scoring.
{
"scan_metadata": {
"tool": "RTU / FRTU Passive Vulnerability Scanner",
"version": "1.0",
"scan_file": "substation_traffic.pcap",
"timestamp": "2024-01-15T09:30:00",
"total_devices": 4,
"total_vulnerabilities": 12
},
"devices": [
{
"ip": "10.1.1.10",
"mac": "00:1A:8C:12:34:56",
"vendor": "ABB",
"rtu_make": "ABB",
"rtu_model": "RTU560",
"device_type": "RTU",
"dnp3_address": 5,
"risk_level": "critical",
"risk_score": 26,
"vulnerabilities": [
{
"vuln_id": "RTU-DNP3-002",
"title": "Unauthenticated DNP3 Control Commands",
"severity": "critical",
"category": "command-security",
"description": "2 control command(s) observed with no Secure Authentication",
"evidence": {
"control_commands": 2,
"auth_challenges": 0,
"session_key": ["10.1.1.1", "10.1.1.10"]
},
"remediation": "Enable DNP3 Secure Authentication v5 or v6 per IEEE 1815-2012 and IEC 62351-5.",
"references": ["IEC 62351-5", "IEEE 1815-2012", "NERC CIP-007"]
}
]
}
]
}One row per device, columns: ip, mac, vendor, rtu_make, rtu_model, device_type, protocols, open_ports, dnp3_address, iec104_address, risk_level, risk_score, vuln_count, critical_vulns, high_vulns, medium_vulns, low_vulns, vuln_ids, master_stations, packet_count.
Ideal for importing into Excel, Splunk, or a CMDB.
Standalone dark-themed interactive report requiring no external dependencies. Features:
- Summary banner — total devices, total vulnerabilities, risk breakdown
- Per-device cards — expandable rows showing all detail
- Vulnerability cards — colour-coded by severity (red=critical, orange=high, yellow=medium, cyan=low)
- Evidence collapsibles — PCAP-derived data backing each finding
- Remediation text — actionable steps with regulatory references
- Sortable protocol list — all detected protocols per device
PCAP / PCAPNG File
│
▼
┌──────────────────────────────────────────────────────────────┐
│ PCAPAnalyzer │
│ │
│ ┌─── For each Ethernet frame: ────────────────────────┐ │
│ │ │ │
│ │ EtherType == 0x88B8 ──► GOOSEAnalyzer │ │
│ │ EtherType == 0x88BA ──► SVAnalyzer │ │
│ │ │ │
│ │ EtherType == 0x0800 (IP) │ │
│ │ │ │ │
│ │ ├── TCP/20000 ──► DNP3Analyzer (stateful) │ │
│ │ ├── TCP/2404 ──► IEC104Analyzer (stateful) │ │
│ │ ├── TCP/102 ──► IEC61850MmsAnalyzer │ │
│ │ ├── TCP/502 ──► ModbusAnalyzer │ │
│ │ ├── TCP/702 ──► SELProtocolAnalyzer │ │
│ │ ├── UDP/9600 ──► (port tracked) │ │
│ │ └── TCP/5006-7 ──► (port tracked) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Post-processing (after all packets): ─────────────┐ │
│ │ │ │
│ │ Link GOOSE publishers to IP devices (by MAC) │ │
│ │ FingerprintEngine.fingerprint(device) per device │ │
│ │ → Exclusive protocol check │ │
│ │ → Vendor string matching │ │
│ │ → GOOSE gcbRef prefix lookup │ │
│ │ → MAC OUI lookup (fallback) │ │
│ │ │ │
│ │ VulnerabilityEngine.assess(device) per device │ │
│ │ → run_dnp3_checks() (RTU-DNP3-001…007) │ │
│ │ → run_iec104_checks() (RTU-104-001…005) │ │
│ │ → run_iec61850_checks() (RTU-61850-001…006) │ │
│ │ → run_general_checks() (RTU-GEN-001…004) │ │
│ │ → De-duplicate, sort by severity │ │
│ │ → Calculate risk score and risk level │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ ReportGenerator │
│ print_summary() │
│ to_json(prefix.json) │
│ to_csv(prefix.csv) │
│ to_html(prefix.html) │
└──────────────────────────────────────┘
Key design principle: Protocol analyzers for DNP3 and IEC-104 are stateful. They accumulate session state across all packets in the capture. After the last packet is processed, the vulnerability engine inspects the full accumulated state to detect patterns that span multiple packets (e.g., "control commands sent with no SA exchange in the entire session").
rtu_passive_scanner/
│
├── rtu_scanner.py ← CLI entry point (run this)
├── requirements.txt ← Python dependencies
├── README.md ← This file
│
└── scanner/
├── __init__.py
├── models.py ← RTUDevice, VulnerabilityFinding,
│ DNP3SessionState, IEC104SessionState,
│ GOOSEPublisherState, ProtocolDetection
├── core.py ← PCAPAnalyzer: PCAP reading, packet dispatch,
│ Layer-2 GOOSE/SV handling, finalisation
│
├── protocols/
│ ├── __init__.py
│ ├── base.py ← BaseProtocolAnalyzer + BaseL2Analyzer ABCs
│ ├── dnp3.py ← DNP3 analyzer (stateful; SA, command tracking)
│ ├── iec104.py ← IEC 60870-5-104 (stateful; command tracking)
│ ├── iec61850_mms.py ← IEC 61850 MMS over TCP/102 (ASN.1 BER)
│ ├── goose.py ← IEC 61850 GOOSE (Layer-2) + SV analyzer
│ ├── modbus.py ← Modbus/TCP with MEI device identification
│ └── sel_protocol.py ← SEL Fast Message over TCP/702
│
├── vuln/
│ ├── __init__.py
│ ├── dnp3_checks.py ← RTU-DNP3-001 … RTU-DNP3-007
│ ├── iec104_checks.py ← RTU-104-001 … RTU-104-005
│ ├── iec61850_checks.py ← RTU-61850-001 … RTU-61850-006
│ ├── general_checks.py ← RTU-GEN-001 … RTU-GEN-004
│ └── engine.py ← VulnerabilityEngine: orchestrate, score, rank
│
├── fingerprint/
│ ├── __init__.py
│ ├── oui_db.py ← 120+ ICS/RTU OUI entries, lookup_oui()
│ └── engine.py ← FingerprintEngine: multi-source identification
│
└── report/
├── __init__.py
└── generator.py ← ReportGenerator: JSON / CSV / HTML output
The PLC scanner uses stateless analyzers — each packet is analysed independently. The RTU scanner introduces stateful analyzers to detect multi-packet security patterns:
# DNP3Analyzer maintains a dict of session states
self._sessions: Dict[Tuple[str, str], DNP3SessionState] = {}
# Per-session state accumulates across the entire capture
session.auth_challenges # FC 0x20 count
session.direct_operate # FC 5 occurrences (bypass SBO)
session.file_opens # FC 25 occurrences (firmware injection)The vulnerability engine is called after all packets are processed and receives the full accumulated state:
# After all packets:
dnp3_sessions = self._dnp3_analyzer.get_sessions()
iec104_sessions = self._iec104_analyzer.get_sessions()
goose_pubs = self._goose_analyzer.get_sessions()
for device in results:
self._vuln_engine.assess(
device,
dnp3_sessions=dnp3_sessions,
iec104_sessions=iec104_sessions,
goose_publishers=goose_pubs,
mms_device_ips=self._mms_ips,
)GOOSE operates below IP — it is a raw Ethernet multicast with EtherType 0x88B8. The core engine handles this before the standard IP processing path:
# In the scapy reader loop:
eth_type = pkt[Ether].type
if eth_type == ETH_GOOSE: # 0x88B8
raw = bytes(pkt[Ether].payload)
result = self._goose_analyzer.analyze_frame(src_mac, dst_mac, eth_type, raw, ts)
self._handle_goose_result(src_mac, result, ts)
continue # Do not process as IP
if eth_type == ETH_SV: # 0x88BA
self._sv_analyzer.analyze_frame(src_mac, dst_mac, eth_type, raw, ts)
continueAfter all packets are processed, _link_goose_to_devices() matches each GOOSE publisher (keyed by (src_mac, app_id)) to an RTUDevice by MAC address.
The GOOSE PDU is ASN.1 BER encoded. The scanner implements a full TLV parser:
GOOSE Frame on wire:
┌────────┬────────┬──────────┬──────────┬─────────────────────────┐
│ AppID │ Length │ Reserved1│ Reserved2│ PDU (ASN.1 Application │
│ 2 bytes│ 2 bytes│ 2 bytes │ 2 bytes │ [1] = tag 0x61) │
└────────┴────────┴──────────┴──────────┴─────────────────────────┘
│
▼
ASN.1 context tags:
0x80 gocbRef (source IED + control block)
0x81 TTL (timeAllowedToLive, ms)
0x82 datSet (dataset name)
0x83 goID (GOOSE identifier)
0x84 t (UTC timestamp)
0x85 stNum (state number — trip detection)
0x86 sqNum (sequence number)
0x87 simulation (TRUE = test mode in live = VULN)
0x88 confRev (configuration revision)
0x89 ndsCom (needs commissioning = misconfigured)
0x8A numDatSetEntries
0xAB allData (actual values, not parsed)
Security tag detection: Reserved1 != 0 indicates IEC 62351-6 authentication is present. If both Reserved fields are 0x0000, authentication is absent — RTU-61850-001.
The engine applies checks in module order, then applies post-processing:
def assess(self, device, dnp3_sessions, iec104_sessions, goose_publishers, mms_device_ips):
findings = []
findings += run_dnp3_checks(device, dnp3_sessions) # if DNP3 detected
findings += run_iec104_checks(device, iec104_sessions) # if IEC-104 detected
findings += run_iec61850_checks(device, goose_publishers, mms_device_ips)
findings += run_general_checks(device) # always
# De-duplicate by vuln_id (keep highest packet_count)
findings = _dedup(findings)
# Sort: critical > high > medium > low > info
findings.sort(key=lambda f: -SEVERITY_WEIGHT.get(f.severity, 0))
# Aggregate score
device.risk_score = sum(SEVERITY_WEIGHT[f.severity] for f in findings)
device.risk_level = _score_to_level(device.risk_score)Both protocols run on TCP port 102. The MMS analyzer distinguishes them by inspecting the COTP DT payload for the S7 protocol ID byte (0x32). If present, the analyzer returns without processing (letting the PLC scanner's S7 analyzer handle it). If absent, the payload is parsed as MMS ASN.1.
Severity: High Category: Authentication
What it means: DNP3 Secure Authentication (SAv5 defined in IEEE 1815-2012 / IEC 62351-5) was never observed in the captured session. Without SA, any device on the OT network can send valid DNP3 control commands.
Evidence collected: Count of SA challenge frames (FC 0x20), SA reply frames (FC 0x21), and Aggressive Mode frames (FC 0x83) across the session.
Remediation:
- Upgrade RTU firmware to support DNP3 SAv5 or SAv6
- Configure master station to require authentication before accepting responses
- Apply HMAC-SHA-256 or AES-GMAC as the authentication algorithm
References: IEC 62351-5, IEEE 1815-2012, NERC CIP-007 R5
Severity: Critical Category: Command Security
What it means: Control commands (Select FC 3, Operate FC 4, Direct Operate FC 5, Direct Operate No Ack FC 6) were observed in a session where no Secure Authentication exchange was ever recorded. An attacker with network access can replicate this pattern to send arbitrary control commands.
Remediation:
- Enable DNP3 Secure Authentication before allowing control commands
- Implement network segmentation (firewall between master station and RTUs)
- Deploy OT-aware intrusion detection to alert on control commands from unexpected sources
Severity: High Category: Command Security
What it means: The master station uses Direct Operate (FC 5) which skips the mandatory Select-Before-Operate two-step safety mechanism. SBO ensures the outstation "selects" (arms) a point before it can be "operated" (commanded), providing a safety check window.
Remediation:
- Configure master station SCADA software to use SBO (Select + Operate, FC 3 + FC 4)
- Disable Direct Operate at the RTU if the vendor firmware supports this restriction
Severity: High Category: Availability
What it means: Restart (Cold Restart FC 13, Warm Restart FC 14) or application-level start/stop commands (FC 17, FC 18) were observed. Without authentication, an attacker can force RTU restarts, causing outages.
Severity: Critical Category: Integrity
What it means: DNP3 File Open (FC 25) was observed — this mechanism is used for firmware uploads, configuration file transfers, and event log retrieval. Without authentication, an attacker can replace the RTU's firmware or configuration file.
Severity: High Category: Misconfiguration
What it means: More than two distinct IP addresses are issuing DNP3 commands to this outstation. RTUs are typically served by a primary and backup master station. Additional masters may indicate an unauthorised system.
Severity: Medium Category: Protocol
What it means: DNP3 was detected over UDP rather than TCP. UDP provides no connection state, making sessions trivially spoofable. DNP3 over UDP is particularly vulnerable to replay attacks.
Severity: High Category: Encryption
What it means: IEC 60870-5-104 APDU was detected in cleartext without a preceding TLS handshake. IEC 62351-3 mandates TLS for IEC-104 to protect data confidentiality and integrity.
Remediation:
- Deploy IEC 62351-3 TLS 1.2 or 1.3 for all IEC-104 sessions
- Use client certificate authentication to prevent rogue master connections
- If TLS is not supportable on the existing RTU, implement a TLS-offloading gateway
Severity: Critical Category: Command Security / Encryption
What it means: Control commands (Single Command type 45, Double Command type 46, Regulating Step type 47, Setpoint types 48–50) were observed in unencrypted IEC-104 sessions. These commands can open/close breakers and adjust setpoints.
Severity: Critical Category: Authentication / Protocol
What it means: IEC 61850 GOOSE messages carry protection trip signals (breaker open/close, fault notification) with no authentication. Any device on the substation LAN can inject a forged GOOSE message that causes a breaker to trip, potentially causing a blackout.
The lack of IEC 62351-6 authentication is detected by checking the Reserved1 field in the GOOSE Ethernet header. When authentication is present, this field carries the security extension indicator (non-zero). When zero, no authentication is applied.
Remediation:
- Upgrade IED firmware to support IEC 62351-6 GOOSE authentication
- Deploy a GOOSE firewall / OT-aware switch that validates GOOSE message signatures
- VLAN-isolate substation LAN and restrict physical access
References: IEC 62351-6, IEC 61850-8-1, CIGRE WG B5.38
Severity: Critical Category: Protocol / Misconfiguration
What it means: The simulation bit in a GOOSE message was set to TRUE while the message was observed on the operational network. The IEC 61850 standard specifies that receivers shall ignore GOOSE messages with simulation = TRUE for actual control purposes. An attacker exploiting this can send trip signals that are accepted by test equipment but ignored by production IEDs — or conversely, can flood the network with simulation messages to trigger IED test routines.
Remediation:
- Verify that test/commissioning GOOSE publishers are isolated from the operational network
- Implement network segmentation between test and production segments
- Configure IEDs to reject simulation GOOSE on production VLANs
Severity: Medium Category: Availability
What it means: The timeAllowedToLive (TTL) field in observed GOOSE messages was ≤ 2000 ms. This means if GOOSE messages stop (e.g., publisher RTU fails or link drops), the receiving IED will consider the data stale and take a default action within 2 seconds. Very low TTLs increase the risk of nuisance trips during brief network interruptions.
Severity: Medium Category: Misconfiguration
What it means: The confRev (configuration revision) field changed values across observed GOOSE messages from the same publisher. confRev increments when the GOOSE Control Block configuration changes. Unexpected changes may indicate an uncoordinated configuration update or an attempt to confuse IED receivers.
Severity: High Category: Encryption
What it means: IEC 61850 MMS (Manufacturing Message Specification) sessions were detected on TCP/102 without TLS encryption. MMS carries IED configuration, logical node status, and control messages. IEC 62351-4 mandates TLS for MMS.
Each vulnerability finding contributes a weighted score:
| Severity | Weight |
|---|---|
| Critical | 10 |
| High | 6 |
| Medium | 3 |
| Low | 1 |
| Info | 0 |
The aggregate score maps to a risk level:
| Score | Risk Level |
|---|---|
| ≥ 20 | Critical |
| ≥ 10 | High |
| ≥ 4 | Medium |
| ≥ 1 | Low |
Example: A device with RTU-DNP3-002 (Critical=10) + RTU-104-001 (High=6) + RTU-61850-001 (Critical=10) = score 26 → Critical risk level.
De-duplication: If the same vulnerability ID is triggered by multiple sessions, only the highest-evidence instance is retained in the final report.
The fingerprinting engine applies four evidence sources in priority order:
Some protocols are used by only one vendor family:
| Protocol | Vendor | Confidence |
|---|---|---|
| SEL Fast Message | SEL | High |
| Omron FINS | Omron | High |
| MELSEC MC Protocol | Mitsubishi Electric | High |
DNP3 Group 0 (Device Attributes) and Modbus MEI (FC43/0x0E) responses embed vendor names and product codes in plaintext. The engine extracts these and matches against a keyword table:
| Keyword | Vendor Identified |
|---|---|
schweitzer |
SEL |
sel- |
SEL |
abb |
ABB |
ge |
GE Grid Solutions |
multilin |
GE Grid Solutions |
siemens |
Siemens |
sicam |
Siemens |
schneider |
Schneider Electric |
scadapack |
Schneider Electric |
easergy |
Schneider Electric |
emerson |
Emerson |
bristol |
Emerson (Bristol) |
controlwave |
Emerson (ControlWave) |
honeywell |
Honeywell |
noja |
Noja Power |
landis |
Landis+Gyr |
The GOOSE Control Block Reference (gcbRef) encodes the IED name, which follows vendor naming conventions:
| Prefix | Vendor | Typical IED |
|---|---|---|
REF |
ABB | REF615 feeder protection |
REC |
ABB | REC615 bay controller |
REL |
ABB | REL670 line protection |
RED |
ABB | RED670 differential |
RET |
ABB | RET670 transformer |
SEL |
SEL | SEL-xxx IEDs |
P64 |
GE Grid Solutions | MiCOM P643 |
T60 |
GE Grid Solutions | Transformer relay |
7SL |
Siemens | SIPROTEC 7SL |
7SD |
Siemens | SIPROTEC 7SD |
7UT |
Siemens | SIPROTEC 7UT |
7SA |
Siemens | SIPROTEC 7SA |
PRO |
Schneider Electric | MiCOM Pro series |
The OUI database contains 120+ entries for ICS and RTU manufacturers. The scanner normalises all MAC formats (00:1A:8C, 00-1A-8C, 001A8C) before lookup.
DNP3 (Distributed Network Protocol 3) is the dominant SCADA protocol for electric utilities in North America and increasingly worldwide.
Frame structure:
DNP3 Data Link Frame:
┌──────┬──────┬──────┬─────────┬──────────┬──────────┬─────────────┐
│ 0x05 │ 0x64 │ LEN │ CTRL │ DST ADDR │ SRC ADDR │ CRC + Data │
│ 1B │ 1B │ 1B │ 1B │ 2B LE │ 2B LE │ │
└──────┴──────┴──────┴─────────┴──────────┴──────────┴─────────────┘
Sync bytes Len DL flags Outstation Master
Security-relevant function codes tracked:
| FC | Hex | Name | Security Relevance |
|---|---|---|---|
| 3 | 0x03 | Select | Part of SBO — normal |
| 4 | 0x04 | Operate | Part of SBO — normal |
| 5 | 0x05 | Direct Operate | SBO bypass — RTU-DNP3-003 |
| 6 | 0x06 | Direct Operate No Ack | SBO bypass + silent |
| 13 | 0x0D | Cold Restart | Service disruption |
| 14 | 0x0E | Warm Restart | Service disruption |
| 17 | 0x11 | Start Application | Remote code execution risk |
| 18 | 0x12 | Stop Application | Service disruption |
| 25 | 0x19 | Open File | Firmware injection |
| 32 | 0x20 | Authenticate Challenge | SA present (good) |
| 33 | 0x21 | Authenticate Reply | SA present (good) |
| 131 | 0x83 | Aggressive Mode | SAv5 present (good) |
IEC-104 is the dominant SCADA protocol in Europe and Asia-Pacific. It runs over TCP, typically on a dedicated connection between a single master station and each RTU.
APDU structure:
IEC-104 APDU:
┌───────┬──────┬──────────────────────────┐
│ 0x68 │ LEN │ APCI (4 bytes control) │
└───────┴──────┴──────────────────────────┘
│
├── I-frame (Info): seq numbered data
├── S-frame (Supervisory): ACK
└── U-frame (Unnumbered): STARTDT/STOPDT/TESTFR
I-frame ASDU:
┌─────────┬──────────┬──────┬───────────────────┐
│ Type ID │ SQ+count │ COT │ Common Address │
│ 1 byte │ 2 bytes │ 2B │ 2 bytes │
└─────────┴──────────┴──────┴───────────────────┘
Control command type IDs tracked:
| Type ID | Name | Severity |
|---|---|---|
| 45 | Single Command | Critical |
| 46 | Double Command | Critical |
| 47 | Regulating Step Command | Critical |
| 48 | Setpoint (scaled) | Critical |
| 49 | Setpoint (normalised) | Critical |
| 50 | Setpoint (short float) | Critical |
| 51 | Bitstring Command | High |
| 100 | General Interrogation | Low (if excessive) |
| 103 | Clock Synchronisation | Medium |
GOOSE (Generic Object Oriented Substation Event) is a Layer-2 multicast used for fast protection signalling. Trip signals must arrive within milliseconds — GOOSE achieves sub-millisecond delivery by bypassing TCP/IP.
Destination MAC convention:
01:0C:CD:01:XX:XX— IEC 61850 GOOSE01:0C:CD:02:XX:XX— GSSE (obsolete, not analysed)01:0C:CD:04:XX:XX— Sampled Values
Key GOOSE security observations:
| Field | Normal Value | Anomaly Detected |
|---|---|---|
| Reserved1 | 0x0000 | 0x0000 → no IEC 62351-6 auth (RTU-61850-001) |
| simulation | FALSE | TRUE → test mode in live traffic (RTU-61850-002) |
| timeAllowedToLive | > 2000 ms | ≤ 2000 ms → replay window risk (RTU-61850-003) |
| confRev | Stable | Changes observed → config drift (RTU-61850-004) |
| ndsCom | FALSE | TRUE → device needs commissioning |
MMS (Manufacturing Message Specification, ISO 9506) is the application protocol for IEC 61850 station bus communication. It is used for IED configuration, logical node status reads, and control services.
The scanner identifies MMS by:
- TCP destination or source port 102
- Absence of the S7 protocol ID byte (0x32) in the COTP DT payload
- Presence of ASN.1 application tags: 0x61 (MMS Initiate), 0xA8 (Confirmed-Request), 0xA9 (Confirmed-Response)
IEC 61850 Logical Node prefixes extracted from MMS traffic:
| Prefix | Function |
|---|---|
| XCBR | Circuit breaker |
| XSWI | Switch / disconnector |
| CSWI | Switch controller |
| RREC | Autorecloser |
| PDIS | Distance protection |
| PTOC | Overcurrent protection |
| PDIF | Differential protection |
| RBRF | Breaker failure |
| MMXU | Measurement unit |
| LPHD | Physical device header |
SEL Fast Message is a proprietary binary protocol used by Schweitzer Engineering Labs (SEL) IEDs on TCP port 702. It is exclusive to SEL devices.
Frame structure:
SEL Fast Message:
┌─────┬──────┬──────┬──────────┬──────────┬──────────┐
│ SOH │ MSG │ LEN │ Device │ Function │ Data │
│0x01 │ Type │ 1B │ ID 2B │ Code 1B │ variable │
└─────┴──────┴──────┴──────────┴──────────┴──────────┘
FC 0xB0 (Fast Operate Command): Flagged as a security-relevant event — direct actuation of relay outputs.
- Create
scanner/protocols/myprotocol.pyinheriting fromBaseProtocolAnalyzer:
from .base import BaseProtocolAnalyzer, AnalysisResult
MY_PORT = 12345
class MyProtocolAnalyzer(BaseProtocolAnalyzer):
def can_analyze(self, sport, dport, proto, payload):
return (proto == "TCP" and
(sport == MY_PORT or dport == MY_PORT) and
len(payload) >= 6)
def analyze(self, src_ip, dst_ip, sport, dport, proto, payload, timestamp):
device_ip = dst_ip if dport == MY_PORT else src_ip
# Parse payload ...
det = self._make_detection(
protocol="My Protocol",
port=MY_PORT,
confidence="high",
timestamp=timestamp,
vendor_name="Acme Corp",
)
return [(device_ip, det)]- Register in
scanner/core.py:
from .protocols.myprotocol import MyProtocolAnalyzer
# In PCAPAnalyzer.__init__():
self._my_analyzer = MyProtocolAnalyzer()
self._ip_analyzers.append(self._my_analyzer)- Add the port to
INDUSTRIAL_PORTSincore.py.
- Add a function to the appropriate check module (or create a new one):
# scanner/vuln/myprotocol_checks.py
from ..models import RTUDevice, VulnerabilityFinding
def run_myprotocol_checks(device: RTUDevice, sessions: dict):
findings = []
for (master, rtu), sess in sessions.items():
if rtu != device.ip:
continue
if sess.dangerous_condition:
findings.append(VulnerabilityFinding(
vuln_id="RTU-MY-001",
title="My Dangerous Condition",
severity="high",
category="authentication",
description=f"Dangerous condition observed: {sess.dangerous_condition}",
evidence={"detail": sess.dangerous_condition},
remediation="Apply fix X.",
references=["IEC 62351-5", "NERC CIP-007"],
))
return findings- Import and call from
scanner/vuln/engine.py:
from .myprotocol_checks import run_myprotocol_checks
# In VulnerabilityEngine.assess():
if "My Protocol" in proto_names:
findings += run_myprotocol_checks(device, my_sessions)Edit scanner/fingerprint/oui_db.py:
OUI_DATABASE = {
# ...existing entries...
"AC:DE:48": "Private", # Format: first 3 octets, colon-separated
"00:E0:4C": "Realtek",
}| Limitation | Explanation |
|---|---|
| Vulnerabilities in encrypted sessions | TLS-protected IEC-104/MMS traffic is opaque; presence of TLS is confirmed but content is not inspected |
| Firmware version enumeration | Requires active querying or OOB data; passive capture only reveals what the device broadcasts in protocol headers |
| Authentication configuration at RTU | SA may be configured but not yet used in the captured time window |
| Patched vs. unpatched firmware | Cannot determine CVE exposure without firmware version + CPE matching |
| Physical security posture | Network capture reveals no information about physical access controls |
| Out-of-band management channels | Serial console, craft port, or cellular management not visible in Ethernet capture |
Short captures (< 5 minutes) may miss:
- DNP3 SA exchanges (may occur only at session start)
- confRev changes (may be infrequent)
- Backup master station connections (activated only on primary failure)
For best results, capture at least 30–60 minutes of normal operational traffic, or capture across a known operational cycle.
- RTU-DNP3-001: A DNP3 SA session may have started before the capture window began. The scanner only sees what is in the PCAP.
- RTU-GEN-004: High peer counts in historian aggregators and gateway nodes are expected — apply
--min-packetsthresholds and review device role.
This tool is designed for authorised defensive security assessments of OT/ICS infrastructure.
Permitted use cases:
- Security audits of infrastructure you own or manage
- Authorised penetration testing engagements with written permission
- Incident response investigations with appropriate authorisation
- Academic and laboratory research
- CTF competitions and training environments
Always obtain written authorisation before:
- Capturing traffic from operational OT networks
- Running any analysis tool in an OT environment
- Sharing captured PCAP files (they contain sensitive operational data)
Regulatory considerations:
- NERC CIP-002 through CIP-011 governs cybersecurity for bulk electric system assets
- IEC 62443 applies to industrial automation and control systems
- GDPR / local privacy laws may apply to traffic captures containing personal data
- Capture retention and handling procedures should align with your organisation's data classification policy
| Package | Version | Purpose |
|---|---|---|
scapy |
≥ 2.5.0 | Primary PCAP reader (preferred) |
dpkt |
≥ 1.9.8 | Fallback PCAP reader if scapy unavailable |
colorama |
≥ 0.4.6 | ANSI colour codes on Windows terminal |
All dependencies are pure Python (no C extensions required for offline PCAP analysis).
Python compatibility: Python 3.8 and later. No str | None union syntax is used; all type hints use Optional[str] from typing for 3.8 compatibility.
# Verify dependencies
python -c "import scapy; print('scapy', scapy.__version__)"
python -c "import dpkt; print('dpkt', dpkt.__version__)"RTU/FRTU Passive Vulnerability Scanner — part of the OT-Security toolkit Defensive use only. No packets sent. Analysis is purely offline.