Active reconnaissance¶
Extract from the Field Notes of Ponder Stibbons
The passive capture had revealed the landscape. Ports 10502 through 10520 were the stage. Now came
the delicate work of stepping onto that stage without breaking anything. Active reconnaissance is where the observer
becomes participant, where watching becomes touching, and where caution becomes paramount.
The Patrician’s directive remained absolute: learn without disrupting. In IT security, a crashed service during testing is an awkward conversation. In operational technology, a crashed service is a turbine offline, lights flickering across the city, and a conversation with the Patrician that ends careers and possibly more.
This is the account of careful, methodical active probing of the UU P&L simulator. Each action was measured. Each response was analysed. Each step forward required certainty that the previous step caused no harm.
The first touch¶
The passive map showed port 10502 as the busiest device, deep in constant conversation with its supervisor. To probe
it was to tap a shoulder mid-discussion. The approach required protocol courtesy—speak the language, observe the
customs, ask only polite questions.
The first test was the simplest: does the device speak when spoken to? The script
raw-tcp-probing.py
performed the gentlest possible Modbus query. Read a single holding register. Address zero. Function code 3.
The universal question in Modbus: “What is your status?”
$ python scripts/recon/raw-tcp-probing.py
[*] Testing Modbus TCP connectivity at 127.0.0.1:10502
[*] Connected successfully
[*] Reading holding register 0...
[✓] Response received: 1500
[*] Device is responsive to Modbus TCP queries
Connection successful. Port 10502 is a working Modbus TCP endpoint.
The response came cleanly. Register zero held the value 1500. No alarms triggered. No services crashed. The device
had answered a polite question politely. This was the permission to continue.
The value itself, 1500, suggested a setpoint. Perhaps RPM for a turbine. But interpretation required more context.
First, establish what the device claims to be.
Device identity¶
Modbus TCP specification includes Function Code 43, also known as MEI Type 14: Read Device Identification. This is the protocol’s way of asking “Who are you?” It returns vendor name, product code, model information, and firmware version. Not all devices implement it. Those that do provide a wealth of reconnaissance data in a single, legitimate, protocol-compliant query.
The script modbus_identity_probe.py
sends this query to discovered devices:
$ python scripts/recon/modbus_identity_probe.py
[*] Probing device identity on discovered Modbus endpoints...
[*] Probing 127.0.0.1:10502
VendorName: Wonderware
ProductCode: SCADA-2024
MajorMinorRevision: 1.0
VendorUrl: www.wonderware.com
ProductName: Wonderware System Platform
ModelName: InTouch SCADA
[*] Probing 127.0.0.1:10503
VendorName: Wonderware
ProductCode: SCADA-2024
MajorMinorRevision: 1.0
(Additional devices show identical information - simulator limitation)
The response revealed a curious uniformity. Every device identified itself as “Wonderware SCADA-2024”. This was clearly a simulator artefact, not operational reality. In a real deployment, each device would have distinct identity, Siemens S7-315 here, Allen-Bradley ControlLogix there, Schneider Modicon elsewhere.
But the uniformity itself was information. It confirmed we were working with a simulator. It demonstrated that the devices implemented the Read Device Identification function. And it showed that someone had configured these identities, even if they hadn’t differentiated them.
The script saved detailed results to reports/device_identity_probe_*.json for later analysis.
Telemetry reading¶
With basic connectivity and identity confirmed, the next step was reading actual operational data. The script
turbine_recon.py
performs a reconnaissance read of turbine telemetry registers, the data a SCADA operator would see:
$ python scripts/recon/turbine_recon.py
[*] Turbine Control System Reconnaissance
[*] Target: 127.0.0.1:10502 (Turbine PLC)
[*] Performing safe telemetry read (no writes, no modification)
=== TURBINE STATUS ===
Speed Setpoint (HR 0): 1500 RPM
Power Output Setpoint (HR 1): 0 MW
Current Speed (IR 0): 1503 RPM
Current Power (IR 1): 15 MW
Bearing Temperature (IR 2): 45°C
Oil Pressure (IR 3): 8 bar
Vibration Level (IR 4): 2 mm/s
Generator Temperature (IR 5): 62°C
Gearbox Temperature (IR 6): 58°C
Ambient Temperature (IR 7): 22°C
Control Mode (Coil 0): AUTO
Emergency Stop (Coil 1): INACTIVE
Maintenance Mode (Coil 2): INACTIVE
Overspeed Alarm (DI 0): OK
Low Oil Pressure (DI 1): OK
High Bearing Temp (DI 2): OK
High Vibration (DI 3): OK
Generator Fault (DI 4): OK
[*] Turbine appears to be operating normally
[*] All alarm states are OK (safe condition)
[*] Telemetry reconnaissance complete - no alarms triggered
This was the full picture. A wind turbine (the speed and power output suggested wind, not steam or hydro) operating at rated speed with normal temperatures and pressures. All discrete inputs showed “OK” status,no alarms, no faults.
The data structure revealed the PLC’s memory map:
Holding Registers 0-1: Setpoints (operator-configurable targets)
Input Registers 0-7: Live sensor readings (read-only telemetry)
Coils 0-2: Control modes (binary on/off switches)
Discrete Inputs 0-4: Alarm states (binary status indicators)
This map would guide deeper discovery. But for reconnaissance, it provided operational context: we knew what this device did, what it monitored, and what control it offered.
Protocol diversity¶
Industrial networks rarely speak only Modbus. The passive capture showed traffic on various ports. Port
10510 showed minimal traffic,only 4 packets in the entire capture. This suggested a different protocol, perhaps
event-driven rather than polled.
Port 63342 showed a different pattern entirely,lower frequency, larger packet sizes. The script
connect-remote-substation.py
tested for OPC UA, a more modern industrial protocol common in newer SCADA systems:
$ python scripts/recon/connect-remote-substation.py
[*] OPC UA Substation Reconnaissance
[*] Target: opc.tcp://127.0.0.1:63342
[*] Connecting to OPC UA server...
[✓] Connected successfully
[*] Reading server information...
Server Name: Substation Controller
Server State: Running
Build Info: UU P&L Substation Control v1.0
[*] Browsing available nodes...
Root Objects:
- BreakerStatus
- VoltageReadings
- CurrentReadings
- AlarmConditions
- SubstationConfig
[*] Reading sample values...
BreakerStatus/Main: CLOSED
VoltageReadings/PhaseA: 11.2 kV
VoltageReadings/PhaseB: 11.1 kV
VoltageReadings/PhaseC: 11.3 kV
[*] OPC UA reconnaissance complete
[*] Substation controller is accessible and functional
This was a different control domain entirely. Not turbines but substations,the electrical distribution equipment between generation and consumption. The OPC UA server exposed a structured object hierarchy with breaker states and voltage readings. Unlike Modbus’s flat register addressing, OPC UA provides named objects and organised hierarchies.
The diversity of protocols confirmed this was a comprehensive simulation: multiple control domains, multiple protocol implementations, a realistic heterogeneous environment.
Network layer probing¶
The final reconnaissance test operated below the application layer. Sometimes the most revealing information comes
not from what services say, but from how the network itself responds. The script
query-substation-controller.py
uses Scapy to send raw TCP SYN packets and analyse responses:
$ sudo .venv/bin/python scripts/recon/query-substation-controller.py
[*] Network-Layer Substation Reconnaissance
[*] Target: 127.0.0.1
[*] Using raw socket probing (requires root)
[*] Scanning common ICS ports...
Port 102 (S7comm): CLOSED (RST received)
Port 502 (Modbus): CLOSED (RST received)
Port 10502 (Custom): OPEN (SYN-ACK received)
Port 10503 (Custom): OPEN (SYN-ACK received)
Port 10510 (Custom): OPEN (SYN-ACK received)
Port 63342 (OPC UA): OPEN (SYN-ACK received)
[*] Analysing TCP/IP stack behaviour...
TTL: 64 (likely Linux/Unix host)
Window Size: 65535 (default)
TCP Options: MSS, SACK permitted, timestamps, window scale
[*] OS Fingerprint suggests: Linux 3.x/4.x
[*] Network reconnaissance complete
[*] Standard ICS ports (102, 502) are not in use
[*] Custom port range 10500-10520 hosts industrial services
This revealed the deliberate port offset—standard ICS protocols moved from their default ports to a custom range. This is common in simulator environments to avoid conflicts with other services and to allow multiple simulator instances on one machine.
The TCP/IP stack fingerprint suggested a Linux host, consistent with running the simulator on a development machine. In a real deployment, these fingerprints would show embedded operating systems, proprietary TCP stacks, and device-specific behaviours.
The gaps and the limits¶
Active reconnaissance also revealed what wasn’t present. Two recon scripts failed to find their targets:
EtherNet/IP Protocol (enumerate-device.py):
$ python scripts/recon/enumerate-device.py
[!] ERROR: Connection refused to port 44818
[*] EtherNet/IP service not available
[*] This protocol is not implemented in the simulator
EtherNet/IP is common in Allen-Bradley and Rockwell Automation systems. Its absence was noted but not concerning for a simulator focused on Modbus and OPC UA implementations.
Siemens S7 Protocol (query-plc.py):
$ python scripts/recon/query-plc.py
[!] ERROR: Permission denied on ports 102/103
[*] S7comm requires privileged ports (<1024)
[*] Run with sudo or configure capabilities for non-root access
[*] Alternatively, configure simulator to use high ports
The S7 protocol’s requirement for privileged ports (102/103) created an operational constraint. This was documented as a limitation, not a failure. The simulator could be run with capabilities or the S7 server could be reconfigured to use high ports if S7 testing became a priority.
These failures were as informative as successes. They defined the boundaries of the test environment and identified where future development might focus.
The reconnaissance map¶
Active reconnaissance transformed the passive traffic analysis into operational knowledge:
Port 10502-10506: Modbus TCP endpoints, responding to standard queries, implementing device identification, hosting turbine telemetry with realistic sensor values and control registers.
Port 10510: Minimal traffic device, responsive but quiet. Likely event-driven rather than polled. Function unclear from reconnaissance alone,requires deeper discovery.
Port 10520: Supervisory endpoint, bridges multiple protocols, aggregates data from field devices. Acts as a gateway between the control layer and monitoring layer.
Port 63342: OPC UA server, hosting substation control objects, providing structured hierarchical data access, implementing modern industrial protocol standards.
Ports 102, 502, 44818: Not in use. Standard ICS protocols either not implemented or running on alternate ports.
This map provided the foundation for the next phase. Active reconnaissance answered “what is there?” Now discovery would answer “what can we learn from what is there?”
The lesson of careful touch¶
Every probe was gentle. Every query was legitimate. Every response was analysed before proceeding. The turbines kept spinning. The voltage stayed stable. The simulator continued its faithful replication of operational systems.
Active reconnaissance in OT environments is not about speed or aggression. It’s about precision and caution. It’s about asking questions the system is designed to answer, in the language it expects, at a pace it can handle.
The Patrician’s directive was satisfied. Knowledge was gained. No lights flickered. No alarms sounded. The reconnaissance phase was complete.
Now came the deeper work: systematic discovery of memory maps, register ranges, and the detailed structure hiding within those registers. But that required a different approach entirely—one based not on probing, but on methodical enumeration.
And that, as they say, is another day’s work.