writing-and-ethics

theory

Lesson 5 β€” Writing Registers and the Ethics of OT Research

The Write Operations

Writing to Modbus is mechanically simple. The consequences in the physical world are not.

FC05 β€” Write Single Coil

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("127.0.0.1", port=502)
client.connect()

# Write coil at address 5 to ON
# Modbus wire encoding: ON = 0xFF00, OFF = 0x0000
client.write_coil(address=5, value=True, slave=1)

# Write coil OFF
client.write_coil(address=5, value=False, slave=1)

client.close()

A coil maps to a digital output. On a PLC controlling a pump station, coil 5 might be the enable signal for pump motor 2. Writing True starts the motor. Writing False stops it. If this coil controls a valve in a water treatment plant, writing False closes it, stopping flow to a distribution zone. If it controls a circuit breaker in a substation, writing True closes the breaker β€” or trips it, depending on the coil's function.

You cannot know which without the PLC program and the I/O wiring diagram.

FC06 β€” Write Single Register

# Write value 1500 to holding register at address 10
client.write_register(address=10, value=1500, slave=1)

Holding registers hold setpoints. Register 10 at value 1500 might be: - A temperature setpoint in tenths of degrees (150.0Β°C) - A pressure limit in mbar - A flow rate target in liters/hour - A PID controller proportional gain

Writing 0 to a pressure setpoint may disable a safety limit. Writing the maximum value (65535) to a temperature setpoint may cause a heater to run until a physical safety cutoff trips β€” or past it, if the cutout has failed.

FC16 β€” Write Multiple Registers

# Write values 100, 200, 300 to registers starting at address 0
client.write_registers(address=0, values=[100, 200, 300], slave=1)

FC16 is atomic for the block: all registers in the request are written in a single transaction. This matters when multiple registers form a compound value (e.g., a 32-bit setpoint in registers 0 and 1). Writing them with two FC06 requests creates a window where register 0 has the new high word but register 1 still has the old low word β€” potentially an out-of-range intermediate value. FC16 avoids this.

FC15 β€” Write Multiple Coils

# Write 8 coils starting at address 0
client.write_coils(address=0, values=[True, False, True, True, False, False, False, True], slave=1)

FC15 sets multiple digital outputs simultaneously. In relay logic, simultaneous coil states can matter: a motor contactor interlocked with a bypass relay must not both be energized at the same moment. Writing coil states non-atomically creates transient states that physical interlock logic is designed to prevent β€” writing them atomically with FC15 bypasses the time window but still respects (or violates) the logic, depending on what you write.


What Writes Do in the Real World

This is not theoretical. These are documented incidents:

Water treatment, Oldsmar FL (2021): An operator's HMI was accessed remotely (likely via TeamViewer). The attacker changed the sodium hydroxide (lye) setpoint from 111 ppm to 11,100 ppm β€” a 100x increase that would have made the water caustic. The change was caught because an operator was watching the screen. If this had been a Modbus write to a setpoint register with no operator monitoring, the change would have propagated to the dosing pump.

Power grid, Ukraine (2015/2016): The BlackEnergy and Industroyer malware included Modbus TCP modules that sent FC05 write commands to open circuit breakers in substations. Breakers controlling power distribution to 230,000 homes were opened by software. No physical presence, no authentication, just TCP packets to port 502.

Manufacturing safety systems: A function test of a Modbus write to a safety relay coil (intended for a lab device) in a production environment caused an unexpected machine stop. The machine had $40,000 of product in mid-process. The process could not be resumed from that state. The product was scrapped.


The Legal Framework

Unauthorized access to a computer system is criminal in every jurisdiction with computer crime laws. For industrial control systems, additional laws apply:

France: Code pΓ©nal art. 323-1 through 323-7 covers unauthorized access and modification of data in automated processing systems. Art. 323-3 specifically covers introduction, modification, or deletion of data. For systems qualifying as critical national infrastructure (OIV β€” OpΓ©rateur d'Importance Vitale), the offense is aggravated under art. 323-4-1 with sentences up to 10 years imprisonment. Enedis, water utilities, and major manufacturers are classified OIV in France.

European Union: NIS2 Directive (2022/2555) requires member states to establish criminal penalties for attacks on "essential entities" including energy, water, transport, and healthcare. Penalties must be "effective, proportionate and dissuasive."

United States: The Computer Fraud and Abuse Act (CFAA, 18 U.S.C. Β§ 1030) covers unauthorized access to "protected computers," which includes any computer affecting interstate commerce. Industrial control systems are explicitly covered. Under 18 U.S.C. Β§ 1030(a)(5), causing damage to critical infrastructure is a federal felony with up to 20 years imprisonment.

United Kingdom: Computer Misuse Act 1990, amended by the Police and Justice Act 2006. Unauthorized modification of computer material (which includes register writes) carries up to 10 years. The Critical National Infrastructure designation applies to energy and water systems.

The practical reality: Every Modbus device on the internet is potentially part of critical infrastructure. You do not need to know whether a specific device is OIV or CNI before you decide not to touch it. The default answer is: do not scan, do not connect, do not read, do not write without written authorization from the asset owner. Shodan is the boundary of unauthenticated passive research.


Demonstrating Impact Without Exercising It

In authorized penetration testing, the goal is demonstrating impact β€” not causing it. The hierarchy of evidence for a Modbus write vulnerability:

Level 1 β€” Document read access: Screenshot/log of FC03 successfully reading holding registers. Register values showing live sensor data or setpoints. This alone demonstrates unauthenticated read access to operational data.

Level 2 β€” Document write capability: Show that FC05/FC06/FC16 returns a success response (no exception). Do not write to addresses that map to active outputs or setpoints. Write to addresses that are mapped but clearly non-operational (e.g., a "timestamp" register, a "counter" register that is read-only from the process perspective but write-capable from Modbus).

Level 3 β€” Controlled demonstration in agreement with client: Write a test value to a specific register agreed with the client engineer, with them monitoring the HMI, with a rollback plan in place. This is only done when the client explicitly requests proof of write impact and has isolated the affected output from the physical process.

Most penetration testing engagements for OT environments stop at Level 1 or 2. The business risk of an unintended physical consequence during a test exceeds the marginal value of proving Level 3 impact when Level 2 evidence is already conclusive.


Responsible Disclosure for Exposed PLCs

You run a Shodan search and find a PLC exposed on port 502, confirmed Modbus, clearly belonging to a water utility. You did not actively scan it β€” Shodan had already indexed it. What do you do?

Step 1 β€” Document without touching: Record the IP, organization (from Shodan), and the banner. Do not connect to the device.

Step 2 β€” Identify the asset owner: WHOIS for the IP. BGP routing data for the ASN. The Shodan org field. If it resolves to a recognizable utility name, go to their public website for security contacts.

Step 3 β€” Report to the national CERT: - France: CERT-FR (cert.ssi.gouv.fr, contact via the reporting form). For OIV, ANSSI has a dedicated incident point of contact. - EU: ENISA maintains a directory of national CSIRTs. - US: CISA ICS (ics.cisa.gov, report form available). For immediate risk, the 24/7 operations center is +1 (888) 282-0870.

Step 4 β€” Report directly to the asset owner (if you can identify them): Send to security@[domain] or a named security contact if available. Include: IP address, port, what you observed (Shodan banner), that you did not actively scan beyond what Shodan had already indexed, and a recommendation to firewall port 502 from the internet.

What to include in the report: IP address, port, exposed protocol, what data is accessible (from the Shodan banner, not from your own probing), and the specific recommended remediation (firewall ACL, VPN requirement, or disable remote access entirely).

What not to do: Do not include a demonstration that you accessed register data unless you have authorization. Do not publish the IP publicly before the owner has had time to remediate. Do not contact media before the owner and CERT are notified.


Safe Practice Environments

If you want to develop Modbus attack skills without touching real infrastructure, these environments are appropriate:

OpenPLC Runtime (open-source, Apache 2.0): Implements a full IEC 61131-3 PLC runtime with Modbus TCP support. Runs on Linux, Raspberry Pi, and in Docker. Write your own ladder logic programs and interact with the simulated I/O over Modbus.

docker pull autonomylogic/openplc
docker run -d -p 8080:8080 -p 502:502 autonomylogic/openplc

SCADA-LTS (open-source, GPL): A SCADA/HMI application that can connect to OpenPLC over Modbus and display register values and coil states visually. Lets you see the HMI effect of your Modbus writes.

# Full lab environment: OpenPLC + SCADA-LTS via Docker Compose
# See: github.com/SCADA-LTS/SCADA-LTS

PyModbus server mode: pymodbus can act as a server, letting you write tests against a local simulated device:

from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.datastore import ModbusSequentialDataBlock

store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [0] * 100),   # Discrete inputs
    co=ModbusSequentialDataBlock(0, [0] * 100),   # Coils
    hr=ModbusSequentialDataBlock(0, [0] * 100),   # Holding registers
    ir=ModbusSequentialDataBlock(0, [0] * 100),   # Input registers
)
context = ModbusServerContext(slaves=store, single=True)

StartTcpServer(context=context, address=("127.0.0.1", 5020))

This runs a full Modbus TCP server on port 5020 that you can read and write freely. Use it to develop and test your scripts before any authorized engagement.


Key Takeaways

  • FC05 writes coils (digital outputs: motors, valves, breakers); FC06/FC16 writes registers (setpoints, limits, parameters)
  • In the real world, a Modbus write can start machinery, change chemical dosing, open circuit breakers, or disable safety systems
  • Unauthorized Modbus writes to critical infrastructure are serious criminal offenses in every jurisdiction β€” sentences are measured in years, not months
  • In authorized testing, demonstrate write capability without exercising it on operational outputs; get written authorization and agree on scope before any write operation
  • For Shodan-discovered exposed PLCs: document passively, report to the asset owner and your national CERT, do not probe the device yourself
  • Build your skills in isolated lab environments (OpenPLC + SCADA-LTS in Docker, or pymodbus server mode) β€” never on production systems