ESPILON-CTF-2026-Writeups/OT/Operating_Room
2026-03-22 19:18:58 +01:00
..
README.md ESPILON CTF 2026 — Write-ups édition 1 (33 challenges) 2026-03-22 19:18:58 +01:00

Operating Room -- Solution

Overview

Modbus TCP server simulating a hospital operating room control system. The player must discover the correct unit ID, map the registers, reverse the XOR-encoded state machine, and execute 6 timed transitions.

Steps

1. Discover Unit ID

Scan Modbus unit IDs (slave addresses). The server only responds to unit ID 13. Default unit ID 1 returns Modbus exceptions.

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!")

2. Map Registers

Read holding registers 0-255 on unit 13:

  • Registers 0-19: Operating room telemetry (temperature, humidity, pressure, O2, etc.)
  • Register 13: 0x4C4E ("LN" -- Lain easter egg)
  • Register 19: 0x0D13 -- this is the XOR key
  • Registers 100-105: State machine (state, encoded hint, timer, transitions, error, key)
  • Register 105: 0x0D13 -- XOR key copy (confirms reg 19)
  • Register 110: Write target (trigger register)
  • Registers 200-215: All zeros (flag area, populated after completion)

3. Understand the State Machine

  • Register 100 = current state (starts at 0)
  • Register 101 = encoded hint
  • Register 105 = XOR key (0x0D13)
  • Decode: expected_value = reg_101 XOR 0x0D13
  • Write expected_value to register 110 to advance the state
  • Each transition must happen before register 102 (timer) reaches 0

4. Execute Transitions

State Subsystem Decoded Value Source
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) special

5. Read Flag

After all 6 transitions, register 100 = 7 (complete). Read registers 200-215 and decode uint16 pairs to ASCII.

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)

Key Insights

  • XOR key 0x0D13 is stored in two places (reg 19 and reg 105) as a breadcrumb
  • The decoded values for states 0-4 match the current telemetry readings
  • State 5 uses the special value 0x1337 (hacker reference)
  • Wrong writes or timeouts reset the state machine to 0

Flag

ESPILON{m0dbu5_0p3r4t1ng_r00m}

Author

Eun0us