# Chapter 20 Labs: Operational Technology and Critical Infrastructure Security

This pack covers the five hands-on labs in Chapter 20. Three labs are fully containerised; two require host-side setup (a GUI desktop tool and a multi-VM virtualisation framework that cannot be meaningfully faked in compose). The containerised labs share one Docker Compose environment.

## Prerequisites

**All labs:**
- Docker Desktop >= 4.28 or Docker Engine >= 25 + Compose plugin
- 4 GB free disk (images: kalilinux/kali-rolling ~700 MB, honeynet/conpot ~250 MB)
- The `./output/` directory (created by `mkdir -p output`) is volume-mounted for pcap files and detection logs

**Lab 20.3 (Wireshark GUI portion) - host only:**
- Wireshark 4.4.x with Modbus/Modbus-TCP/DNP3 dissectors (default in current builds)
- Sample ICS captures: see Wireshark wiki sample captures page, or generate one via this compose (the detection script writes `output/modbus_lab.pcap`)

**Lab 20.6b (GRFICS) - host only:**
- VirtualBox >= 7.0
- 16 GB RAM minimum (5-VM setup)
- 50 GB free disk

## Quick Start (containerised labs)

```bash
# From this directory:
mkdir -p output
docker compose up -d --build

# Wait for conpot to be healthy:
docker compose ps

# Open a shell on the tools runner:
docker exec -it ch20-tools bash
```

---

## Lab 20.3: Dissecting ICS Traffic in Wireshark

**Objective:** Read Modbus/TCP and DNP3 traffic at the packet level, identify function codes and register writes that map to the four OT attack primitives (spoof, replay, unauthorised write, on-path manipulation).

**Setup - host setup (not containerised)**

Wireshark is a GUI application and does not containerise meaningfully. Run it on your host.

1. Install Wireshark 4.4.x from https://www.wireshark.org/download.html (ships Modbus and DNP3 dissectors built in by default).
2. Confirm dissectors: open Wireshark, Analyze > Enabled Protocols, search for "Modbus", "Modbus/TCP", and "DNP 3.0". All three should be present and enabled.

**Obtaining a Modbus pcap:**

Option A - Use the pcap generated by this lab pack:
```bash
# Start the compose stack, then run the detection script:
docker exec ch20-tools bash /scripts/lab20_6a_detect.sh
# The pcap is written to ./output/modbus_lab.pcap - open it in Wireshark on your host.
```

Option B - Wireshark wiki sample captures:
- https://wiki.wireshark.org/SampleCaptures - search "Modbus" or "ICS"
- Several Modbus/TCP captures are provided for training use

Option C - Capture live against conpot:
```bash
# With the compose stack running, probe conpot from the tools container:
docker exec ch20-tools python3 /scripts/lab20_5_probe.py
# Then start a capture on the Docker bridge interface (bridge name from: docker network ls)
```

**Walkthrough:**

1. Open the Modbus pcap in Wireshark and apply the display filter `modbus`. Observe the decoded frames.
2. Apply `mbtcp` to see the Modbus/TCP transport header. Note the lack of any authentication field.
3. Filter for write frames: `modbus.func_code == 5 || modbus.func_code == 6 || modbus.func_code == 16`. For each write, note the Reference Number (register/coil address) and Data value.
4. Click a write frame and expand the Modbus tree. Confirm: no credential, no sender identity, no integrity check anywhere in the frame. The controller accepts the command on arrival.
5. For DNP3 captures (use a Wireshark wiki sample): apply `dnp3`, expand the DNP 3.0 Application Layer, and locate the Function Code (e.g., Operate, Direct Operate) and any Control Relay Output Block objects.
6. Write up two findings per the lab brief: one Modbus write frame (FC, register, value, primitive it enables, compensating control); one DNP3 control command (FC, object, primitive, compensating control).

**Expected output:** You should be able to see Modbus function code 6 or 16 frames where the Data field contains the new register value, with no authentication object anywhere in the exchange.

**Verify completion:** You can answer: "In the write frame I captured, the function code was ___, the target register was ___, the value written was ___, and the attack primitive most directly enabled is ___ (spoofing / replay / unauthorised write / on-path manipulation)."

**Cleanup:** Close Wireshark. No persistent state.

---

## Lab 20.4: Zone-and-Conduit Drill - Partition a Fictional Plant

**Objective:** Apply an IEC 62443 zone-and-conduit analysis to a flat /24 network ("Meridian Water Treatment") and produce a zone map, conduit enumeration, SL-T assignments, and a prioritised remediation backlog.

**Setup - host setup (not containerised)**

This is a paper/analytical exercise. No software or container is required. Use pen and paper, a whiteboard, or any diagramming tool (draw.io, Lucidchart, or the companion notebook `20_purdue_zone_analyser.ipynb` referenced in the chapter).

**Walkthrough:**

**Step 1 - Draw zones.** Group these assets of equal security requirement:
- Control zone: chlorine-dosing PLCs + chemical-feed VFDs (Purdue Level 1)
- Supervisory zone: SCADA server + two operator HMIs (Level 2)
- Operations zone: process historian + maintenance workstation (Level 3)
- DMZ zone (Level 3.5): the broker between OT and IT
- Enterprise zone: corporate business network, email, billing system (Level 4)

**Step 2 - Assign target Security Levels (SL-T).** Worst-case consequence drives the number:
- Control zone: SL-T 3 (dosing chlorine at wrong concentration endangers public health - moderate-resource, IACS-specific attacker capability required to resist)
- Supervisory zone: SL-T 2 (operator blinding is serious but slower-consequence)
- Operations zone: SL-T 2 (historian compromise means evidence loss, not immediate safety impact)
- DMZ: SL-T 2 (boundary enforcement, not process control)
- Enterprise zone: SL-T 1 (standard IT risk, not safety-critical)

**Step 3 - Define conduits.** Every cross-zone flow must pass through an enumerated conduit with explicit controls:
- HMI to PLC (Supervisory -> Control): protocol-aware firewall, Modbus-only allow-list, no inbound connections from Control to Supervisory
- Historian replication (Operations <- Supervisory): one-way data diode or push-only data feed
- Vendor support (Enterprise -> Control via DMZ): monitored jump host in DMZ, per-session authorisation, MFA, recorded sessions, no persistent tunnel

**Step 4 - Kill the dangerous adjacency.** The vendor laptop VPN tunnelling directly from Level 4 to Level 1 is an un-conduited cross-level path. Re-route it: vendor laptop connects to DMZ jump host only; DMZ jump host brokers an inward session to the Control zone with per-session MFA and logging. This satisfies IEC 62443 FR5 (Restricted Data Flow).

**Step 5 - Score.** For each zone, list achieved SL (SL-A) based on controls actually present today (likely SL 0 or SL 1 on a flat /24), compare to SL-T, and list the SL-T minus SL-A gaps as a prioritised remediation backlog.

**Expected output:** A zone diagram with five named zones, a conduit table (flow, protocol, controls, SL-T of conduit), SL-T/SL-A gap table, and a numbered remediation backlog. If using `20_purdue_zone_analyser.ipynb`, run it before and after removing the vendor-laptop-to-PLC adjacency and confirm the isolation score improves.

**Verify completion:** Your remediation backlog explicitly includes "remove direct vendor VPN to Control zone; route via DMZ jump host (FR5)" as item 1, and your conduit table has an entry for every named flow with controls specified.

**Cleanup:** No persistent state (paper/diagramming exercise).

---

## Lab 20.5: A Modbus Honeypot with Conpot - Observing Attacker Behaviour

**Objective:** Stand up a low-interaction ICS honeypot (Conpot) emulating a Siemens S7-200 PLC on Modbus/TCP, probe it the way an attacker's scanner would, observe the function codes in the logs, and map the observed behaviour to ATT&CK for ICS.

**Setup:**

```bash
# Start just the honeypot (tools are optional for this lab):
docker compose up -d conpot
docker compose ps      # wait for "healthy"
```

Conpot answers on host port 20502 (mapped from container 502) and host port 20080 (mapped from 80).

**Walkthrough:**

**Step 1 - Confirm the emulated PLC is listening:**
```bash
nc -z 127.0.0.1 20502 && echo "Modbus port open"
```

**Step 2 - Read holding registers (reconnaissance, FC 03):**
```bash
# Using the tools container:
docker compose up -d tools
docker exec -it ch20-tools python3 - <<'EOF'
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient("172.30.20.10", port=502)
c.connect()
result = c.read_holding_registers(0, count=10)
print("Registers 0-9:", result.registers)
c.close()
EOF
```

Or run the bundled probe script for a full read-reconnaissance-write sequence:
```bash
docker exec -it ch20-tools python3 /scripts/lab20_5_probe.py
```

**Step 3 - Watch the Conpot logs:**
```bash
docker logs -f ch20-conpot
```
Each connection appears with source IP, port, and the Modbus function code requested. A read (FC 03) is reconnaissance. An unsolicited write (FC 06 or 16) to a holding register is the FrostyGoop behaviour: Impair Process Control (ATT&CK for ICS TA0106).

**Step 4 - Map to ATT&CK for ICS:**
- Unauthenticated connection on port 502: Initial Access / Discovery surface
- Register reads (FC 03, FC 01): Collection / Discovery
- Register write (FC 06): Impair Process Control (TA0106)

**Step 5 - Write a one-paragraph note** arguing why this device should never have been internet-reachable, and what Purdue level, zone, conduit, and Security Level (IEC 62443) it should sit behind.

**Expected output:**
- Conpot logs show the connection and function codes
- The probe script output shows registers read and a write acknowledged with no authentication exchange
- Your ATT&CK mapping connects each observed frame type to a tactic

**Verify completion:** `docker logs ch20-conpot` shows at least one FC 03 (read) and one FC 06 (write) entry from your probe.

**Cleanup:**
```bash
docker compose stop conpot
```

---

## Lab 20.6a: OT Detection - A Minimal Zeek/Suricata Rule Set for Modbus Anomalies

**Objective:** Stand up a passive monitor, parse a Modbus/TCP capture with Zeek to build a behaviour baseline, and write and validate a Suricata rule that fires on Modbus writes to a read-only zone - confirming the alert appears only when a write occurs, not on reads.

**Setup:**

```bash
# Build and start the full stack (conpot + tools):
docker compose up -d --build
docker compose ps    # wait for both services healthy/running
```

**Walkthrough:**

**Step 1 - Generate a Modbus pcap** (or use a provided sample):
```bash
docker exec -it ch20-tools bash /scripts/lab20_6a_detect.sh
```
The script: (a) starts tcpdump, (b) runs the probe script against conpot, (c) stops tcpdump, (d) runs tshark, Zeek, and Suricata over the capture automatically.

The pcap is written to `./output/modbus_lab.pcap` which is host-mounted, so you can open it in Wireshark for Lab 20.3.

**Step 2 - Manual tshark function-code inventory:**
```bash
docker exec ch20-tools tshark -r /output/modbus_lab.pcap \
  -Y modbus -T fields -e modbus.func_code | sort | uniq -c
```
Expected: FC 01 (read coils), FC 03 (read holding registers), FC 06 (write single register) - counts will vary.

**Step 3 - Zeek passive parse:**
```bash
docker exec ch20-tools bash -c "
  mkdir -p /output/zeek && cd /output/zeek
  zeek -r /output/modbus_lab.pcap -C 2>&1
  ls -la && cat modbus.log 2>/dev/null | head -20
"
```
`modbus.log` shows each request's function code, unit ID, and address - your passive asset-and-behaviour baseline without sending a single packet to the device.

**Step 4 - Inspect the Suricata rule** (see `./tools/rules/modbus-write.rules`):
The rule keys on the function-code byte at offset 7 in the TCP payload (after the 6-byte MBAP header) and fires on FC 05, FC 06, and FC 16. It is scoped to the conpot IP (`172.30.20.10 502`) so it only alerts where a write should never occur.

**Step 5 - Run Suricata over the capture:**
```bash
docker exec ch20-tools bash -c "
  mkdir -p /output/suricata
  suricata -r /output/modbus_lab.pcap -S /rules/modbus-write.rules \
    -l /output/suricata --set default-log-dir=/output/suricata 2>&1
  echo '--- Alerts ---'
  cat /output/suricata/fast.log 2>/dev/null || echo '(no fast.log yet)'
"
```

**Step 6 - Verify rule polarity.** To confirm the rule stays silent on reads, generate a read-only capture (comment out the write in the probe script and re-run), then re-run Suricata - `fast.log` should be empty.

**Expected output:**
- `fast.log` contains one or more MODBUS Write-Single-Register FC06 alerts
- Re-running against a read-only capture produces an empty `fast.log`
- `modbus.log` from Zeek shows every request's function code without any packet sent to the device

**Verify completion:** `cat output/suricata/fast.log | grep FC06` returns at least one line.

**Cleanup:**
```bash
docker compose down
```

---

## Lab 20.6b: GRFICS - Recon and a Controlled Attack Against a Virtual ICS

**Objective:** Execute an end-to-end ICS compromise (recon, Modbus write, physical consequence in the 3D simulation) inside the self-contained GRFICS virtual lab, then layer on segmentation and monitoring from Lab 20.6a to demonstrate defence.

**Setup - host setup (not containerised)**

GRFICS (Graphical Realism Framework for Industrial Control Simulation) is a five-VM VirtualBox setup (3D process simulator, soft PLC, HMI, pfSense firewall, attacker workstation). It cannot be faked in Docker Compose without losing the physical simulation and the realistic network topology that make the lab meaningful.

**Upstream project:** https://github.com/Fortiphyd/GRFICSv2

**Canonical install steps:**

1. Install VirtualBox 7.x: https://www.virtualbox.org/wiki/Downloads
2. Clone the GRFICSv2 repository: `git clone https://github.com/Fortiphyd/GRFICSv2`
3. Follow the README to import the five pre-built OVA images and configure host-only networking.
4. Start all five VMs. The 3D simulation VM will open a graphical window showing the chemical process.
5. On the attacker VM, identify the PLC with `nmap -sV <plc_ip>` (within the host-only segment only).

**Walkthrough (after GRFICS is running):**

1. **Recon:** From the attacker VM, scan the OT segment with nmap to find the soft PLC and its Modbus service. Note how the flat segment gives the attacker direct reach to the PLC.
2. **Capture baseline:** Start Wireshark on the attacker VM and capture HMI-to-PLC Modbus polling. Identify the registers the HMI reads and which ones drive the process - your normal baseline.
3. **Controlled attack:** Using pymodbus or `mbpoll`, write to a process setpoint or output register. Watch the 3D simulation respond (tank overflow, runaway reaction). This is ATT&CK for ICS Impair Process Control (TA0106).
4. **Defend:** Add Zeek/Suricata monitoring (using rules from Lab 20.6a) to the GRFICS network (TAP the segment or run on the attacker VM). Confirm the write alert fires. Then add a pfSense rule segmenting the attacker VM away from the PLC and confirm the same attack now fails.

**Expected output:** The 3D simulation shows the physical consequence of the Modbus write. The detection rule fires. After segmentation, the same write attempt fails (connection refused or filtered).

**Verify completion:** Screenshot of (a) the 3D simulation showing the process deviation, and (b) the Suricata fast.log alert from Lab 20.6a's rules applied to GRFICS traffic.

**Safety note:** Run GRFICS only on an isolated host-only VirtualBox network. The attacker VM techniques demonstrated (nmap, Modbus write) are illegal and dangerous against real infrastructure or any system you do not own.

**Cleanup:** Shut down all five VMs in VirtualBox. No Docker state to clean up.

---

## Teardown (containerised labs)

```bash
# Stop and remove containers + networks (keep pcap outputs):
docker compose down

# Remove everything including the built tools image:
docker compose down --rmi local

# Remove output files:
rm -rf output/*
```

---

## Safety and Authorisation

All offensive techniques in these labs (Modbus writes, nmap scans, traffic capture) are demonstrated only against:
- The provided Conpot container (a honeypot emulator, not a real PLC)
- The GRFICS simulation VMs (a closed virtual lab, not real infrastructure)

Never direct these techniques at production OT systems, industrial control networks, or any system you do not own and are not explicitly authorised to test. Unauthenticated Modbus writes to a real PLC can cause physical process deviations that injure people. The chapter's safety-and-availability-first priority (SAIC) applies with double force in a lab: the lesson must not become the incident.
