- 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
151 lines
4.3 KiB
Markdown
151 lines
4.3 KiB
Markdown
# 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
|
||

|
||
|
||
|
||
### 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 |
|
||
|
||

|
||
|
||
### Step 3 — Decode the state machine logic
|
||
|
||
- Register 100 = current state (0–6)
|
||
- 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)
|
||
```
|
||
|
||

|
||
|
||
### 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
|
||
|
||
`ESPILON{m0dbu5_0p3r4t1ng_r00m}`
|