ICS/Modbus — Claus for Concern | Advent of Cyber 2025 Day 19 | Writeup

Access the room: https://tryhackme.com/room/ICS-modbus-aoc2025-g3m6n9b1v4



Press enter or click to view image in full size

The snow falls heavily over Wareville as chaos erupts at TBFC headquarters. What should be the busiest shipping day of the season has turned into a disaster.

“Another chocolate egg?!” shouts a frustrated warehouse worker, holding up yet another Easter-themed package.”We’re supposed to be shipping Christmas presents!”

The delivery drones buzz overhead, their mechanical hums sounding almost… mocking. Each one returns from its route empty, having successfully delivered its cargo. But the cargo is all wrong.

You’re called into the command centre, where screens flicker with delivery statistics. Everything looks normal on the surface — 1,000 presents in stock, 98% success rate, and all systems are operational. But the phones won’t stop ringing with confused citizens asking why they’re receiving chocolate eggs instead of the toys and gifts they ordered.

The logistics manager pulls up a delivery manifest. “Look at this”, she says, pointing at the screen.”The system indicates that we delivered a teddy bear to the Miller family, but they received a chocolate bunny instead. It’s the same weight, exact dimensions, but completely different items.”

Then, on one of the monitoring screens, a message flashes for just a second before disappearing:

Someone has compromised the drone fleet’s control systems. The attack is sophisticated, falsifying sensor data, manipulating inventory selection, and erasing all traces. This isn’t just a prank — it’s a calculated assault on Christmas itself.

Your mission is clear: investigate the TBFC Drone Delivery System, uncover how King Malhare’s Eggsploit team has compromised it, and restore Christmas deliveries before SOC-mas is ruined.

But be warned: King Malhare doesn’t leave systems undefended. Traps are waiting for the careless investigator. One wrong move and you might make things much worse.

A Mysterious Discovery

As you walk through the warehouse control room, something catches your eye — a crumpled piece of paper on the floor near the PLC terminal. It looks like someone dropped it in a hurry.

You pick it up and unfold it. The handwriting is hurried, almost frantic:

TBFC DRONE CONTROL - REGISTER MAP
(For maintenance use only)

HOLDING REGISTERS:
HR0: Package Type Selection
0 = Christmas Gifts
1 = Chocolate Eggs
2 = Easter Baskets

HR1: Delivery Zone (1-9 normal, 10 = ocean dump!)

HR4: System Signature/Version
Default: 100
Current: ??? (check this!)

COILS (Boolean Flags):
C10: Inventory Verification
True = System checks actual stock
False = Blind operation

C11: Protection/Override
True = Changes locked/monitored
False = Normal operation

C12: Emergency Dump Protocol
True = DUMP ALL INVENTORY
False = Normal

C13: Audit Logging
True = All changes logged
False = No logging

C14: Christmas Restored Flag
(Auto-set when system correct)

C15: Self-Destruct Status
(Auto-armed on breach)

CRITICAL: Never change HR0 while C11=True!
Will trigger countdown!

- Maintenance Tech, Dec 19

You stare at the note, confusion washing over you. “Register map? Coils? What is all this?”

The terminology is foreign — HR0, C11, “Modbus” scribbled in the margin. But something about it feels important, like a key you don’t yet know how to use.

You pocket the note carefully. “I’ll figure out what this means later”, you think. For now, you need to understand the systems you’re dealing with.

Little do you know, this crumpled note will be exactly what saves Christmas…

Learning Objectives

  • How SCADA (Supervisory Control and Data Acquisition) systems monitor industrial processes
  • What PLCs (Programmable Logic Controllers) do in automation
  • How the Modbus protocol enables communication between industrial devices
  • How to identify compromised system configurations in industrial systems
  • Techniques for safely remediating compromised control systems
  • Understanding protection mechanisms and trap logic in ICS environments

What is SCADA?

SCADA systems are the “command centres” of industrial operations. They act as the bridge between human operators and the machines doing the work. Think of SCADA as the nervous system of a factory — it senses what’s happening, processes that information, and sends commands to make things happen.

TBFC uses a SCADA system to oversee its entire drone delivery operation. Without it, operators would have no way to monitor hundreds of drones, manage inventory, or ensure packages reach the right destinations. It’s the invisible orchestrator of Christmas logistics.

In early 2024, the first ICS/OT malware, FrostyGoop, was discovered. The malware can directly interface with industrial control systems via the Modbus TCP protocol, enabling arbitrary reads and writes to device registers over TCP port 502.

King Malhare has weaponized these same tactics, not to cause blackouts, but to sabotage Christmas deliveries by directly manipulating the control system through the Modbus protocol. In the next task, we’ll explore the PLC — the component he’s actually compromised.

What is a PLC?

A PLC (Programmable Logic Controller) is an industrial computer designed to control machinery and processes in real-world environments. Unlike your laptop or smartphone, PLCs are purpose-built machines engineered for extreme reliability and harsh conditions.

PLCs are designed to:

  • Survive harsh environments — They operate flawlessly in extreme temperatures, constant vibration, dust, moisture, and electromagnetic interference. A PLC controlling warehouse robotics might endure freezing temperatures in winter storage areas and scorching heat near packaging machinery.
  • Run continuously without failure — PLCs operate 24/7 for years, sometimes decades, without rebooting. Industrial facilities can’t afford downtime for software updates or system restarts. When a PLC starts running, it’s expected to keep running indefinitely.
  • Execute control logic in real-time — PLCs respond to sensor inputs within milliseconds. When a package reaches the end of a conveyor belt, the PLC must instantly activate the robotic arm to catch it. These timing requirements are critical for safety and efficiency.
  • Interface directly with physical hardware — PLCs connect directly to sensors (measuring temperature, pressure, position, weight) and actuators (motors, valves, switches, robotic arms). They speak the electrical language of industrial machinery.

What is Modbus?

Modbus is the communication protocol that industrial devices use to talk to each other. Created in 1979 by Modicon (now Schneider Electric), it’s one of the oldest and most widely deployed industrial protocols in the world. Its longevity isn’t due to sophisticated features — quite the opposite. Modbus succeeded because it’s simple, reliable, and works with almost any device.

Think of Modbus as a basic request-response conversation:

  • Client (your computer): “PLC, what’s the current value of register 0?”
  • Server (the PLC): “Register 0 currently holds the value 1.”

This simplicity makes Modbus easy to implement and debug, but it also means security was never a consideration. There’s no authentication, no encryption, no authorisation checking. Anyone who can reach the Modbus port can read or write any value. It’s the equivalent of leaving your house unlocked with a sign saying “Come in, everything’s accessible!”

Modbus Data Types

Modbus organises data into four distinct types, each serving a specific purpose in industrial automation:

Press enter or click to view image in full size

The distinction between inputs and outputs is important. Coils and Holding Registers are writable — you can change their values to control the system. Discrete Inputs and Input Registers are read-only — they reflect sensor measurements that you observe but cannot directly modify.

In TBFC’s drone control system, you’ll find:

  • Holding Registers storing configuration values:
  • HR0: Package type selection (0=Gifts, 1=Eggs, 2=Baskets)
  • HR1: Delivery zone (1–9 for normal zones, 10 for emergency disposal)
  • HR4: System signature (a version identifier or, in this case, an attacker’s calling card)
  • Coils controlling system behaviour:
  • C10: Inventory verification enabled/disabled
  • C11: Protection mechanism enabled/disabled
  • C12: Emergency dump protocol active/inactive
  • C13: Audit logging enabled/disabled
  • C14: Christmas restoration status flag
  • C15: Self-destruct mechanism armed/disarmed

Remember that crumpled note you found earlier? Now it makes complete sense. The maintenance technician was documenting these exact Modbus addresses and their meanings!

Modbus Addressing

Each data point in Modbus has a unique address — think of it like a house number on a street. When you want to read or write a specific value, you reference it by its address number.

Critical detail: Modbus addresses start at 0, not 1. This zero-indexing catches many beginners off guard. When documentation mentions “Register 0,” it literally means the first register, not the second.

Examples from the TBFC system:

  • Holding Register 0 (HR0) = Package type selector
  • Holding Register 1 (HR1) = Delivery zone
  • Holding Register 4 (HR4) = System signature
  • Coil 10 (C10) = Inventory verification flag
  • Coil 11 (C11) = Protection mechanism flag
  • Coil 15 (C15) = Self-destruct status

Modbus TCP vs Serial Modbus

Originally, Modbus operated over serial connections using RS-232 or RS-485 cables. Devices were physically connected in a network, and this physical isolation provided a degree of security — you needed physical access to the wiring to intercept or inject commands.

Modern industrial systems use Modbus TCP, which encapsulates the Modbus protocol inside standard TCP/IP network packets. Modbus TCP servers listen on port 502 by default.

This network connectivity brings enormous benefits — remote monitoring, easier integration with business systems, and centralised management. But it also exposes these historically isolated systems to network-based attacks.

King Malhare exploited exactly this vulnerability. The TBFC drone control system’s Modbus TCP port (502) was accessible over the network without any authentication required. He didn’t need to break into the facility or tamper with wiring. He simply connected to port 502 and started issuing commands as if he were authorised.

The Security Problem

Modbus has no built-in security mechanisms:

  • No authentication: The protocol doesn’t verify who’s making requests. Any client can connect and issue commands.
  • No encryption: All communication happens in plaintext. Anyone monitoring network traffic can see exactly what values are being read or written.
  • No authorisation: There’s no concept of permissions. If you can connect, you can read and write anything.
  • No integrity checking: Beyond basic checksums for transmission errors, there’s no cryptographic verification that commands haven’t been tampered with.

Modern security solutions exist — VPNs, firewalls, Modbus security gateways — but they’re add-ons, not part of the protocol itself. Many industrial facilities haven’t implemented these protections, either due to cost concerns, compatibility issues with legacy equipment, or a simple lack of awareness.

Step by step Walkthrough

Initial Setup

Start the target machine and the AttackBox as instructed to access the lab environment.

Step 1: Initial Reconnaissance

Use nmap to discover open ports and services on the target system.

nmap -sV -p 22,80,502 MACHINE_IP

Note: We specified ports 22, 80, 502 to speed up the scan.


If you want to perform a comprehensive scan of all ports, you can use:

nmap -sV -T4 -p- -vv MACHINE_IP

Key Findings:

  • Port 80: HTTP service (CCTV camera feed)
  • Port 502: Modbus TCP (PLC communication protocol)

Step 2: Visual Confirmation

Navigate to http://MACHINE_IP to see the warehouse floor.

Press enter or click to view image in full size

The camera feed confirms that the system is delivering Pastel-colored chocolate eggs instead of Christmas gifts, indicating a logic manipulation attack.

Step 3: Modbus Reconnaissance

Install PyModbus: Ensure the pymodbus library is installed.

pip3 install pymodbus==3.6.8

OR

sudo apt install python3-pymodbus

Connect to Modbus: Use Python to connect to the PLC’s Modbus interface.

Open the python Interpreter:

python3

Paste the command into the interpreter:


from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('MACHINE_IP', port=502)
if client.connect():
print("Connected to PLC successfully")
else:
print("Connection failed")

Press Enter two times.

Excellent! We have a connection to the PLC’s Modbus interface.

Read Holding Registers: Query the holding registers to identify the compromised values.

result = client.read_holding_registers(address=0, count=1, slave=1)
if not result.isError():
package_type = result.registers[0]
print(f"HR0 (Package Type): {package_type}")
if package_type == 0:
print(" Christmas Presents")
elif package_type == 1:
print(" Chocolate Eggs")
elif package_type == 2:
print(" Easter Baskets")

HR0 is set to 1, which means the system is configured to load chocolate eggs.

Let’s check HR1 (Delivery Zone):

result = client.read_holding_registers(address=1, count=1, slave=1)
if not result.isError():
zone = result.registers[0]
print(f"HR1 (Delivery Zone): {zone}")
if zone == 10:
print(" WARNING: Ocean dump zone")
else:
print(f" Normal delivery zone")

Here in HR1 the Delievry Zone 5 is normal according to our initial instructions.

Now let’s check HR4 — the system signature:

result = client.read_holding_registers(address=4, count=1, slave=1)
if not result.isError():
signature = result.registers[0]
print(f"HR4 (System Signature): {signature}")
if signature == 666:
print(" EGGSPLOIT DETECTED - King Malhare's signature")

The value 666 confirms that the system has been compromised by the Eggsploit framework, corresponding to the earlier observed message “EGGSPLOIT v6.66”.

Reading Coils: Coils are Boolean flags that control system behavior. Let's check the status of critical coils.

Lets check the coil 10

result = client.read_coils(address=10, count=1, slave=1)
if not result.isError():
verification = result.bits[0]
print(f"C10 (Inventory Verification): {verification}")
if not verification:
print(" DISABLED - System not checking stock")

This means inventory verification is turned off, so the system isn’t checking whether the stock exists or not.

Let’s check Coil 11

result = client.read_coils(address=11, count=1, slave=1)
if not result.isError():
protection = result.bits[0]
print(f"C11 (Protection/Override): {protection}")
if protection:
print(" ACTIVE - Changes are being monitored")

C11 is enabled, which means the system is actively monitoring for changes. Remember the warning on the note: “Never change HR0 whilst C11=True! Will trigger countdown!”.

Now let's check the coil 15

result = client.read_coils(address=15, count=1, slave=1)
if not result.isError():
armed = result.bits[0]
print(f"C15 (Self-Destruct Armed): {armed}")
if not armed:
print(" Not armed yet - safe for now")

The self-destruct sequence remains inactive for now. However, attempting to alter HR0 while C11 is active will trigger its activation. This serves as the built-in trap mechanism.

Understanding the Trap

Now the note makes complete sense. The maintenance technician discovered:

  • HR0 is set to 1 (forcing eggs)
  • C11 protection is enabled (monitoring for changes)
  • If anyone changes HR0 whilst C11 is True, C15 gets armed
  • Once C15 is armed, a 30-second countdown begins
  • After 30 seconds, C12 (Emergency Dump) activates, and everything dumps to Zone 10 (ocean)

The technician’s warning was trying to save whoever found this note from making things worse.

Step 4: Complete Reconnaissance using Scripts

Let’s create a comprehensive script to view the full system state by analyzing all relevant registers and coils.

Now exit the Python interpreter (Ctrl+D)

Create a new script file called reconnaissance.py:

nano reconnaissance.py

Copy and paste the following and make sure to add your own MACHINE_IP:

#!/usr/bin/env python3
from pymodbus.client import ModbusTcpClient

PLC_IP = "MACHINE_IP"
PORT = 502
UNIT_ID = 1

# Connect to PLC
client = ModbusTcpClient(PLC_IP, port=PORT)

if not client.connect():
print("Failed to connect to PLC")
exit(1)

print("=" * 60)
print("TBFC Drone System - Reconnaissance Report")
print("=" * 60)
print()

# Read holding registers
print("HOLDING REGISTERS:")
print("-" * 60)

registers = client.read_holding_registers(address=0, count=5, slave=UNIT_ID)
if not registers.isError():
hr0, hr1, hr2, hr3, hr4 = registers.registers

print(f"HR0 (Package Type): {hr0}")
print(f" 0=Christmas, 1=Eggs, 2=Baskets")
print()

print(f"HR1 (Delivery Zone): {hr1}")
print(f" 1-9=Normal zones, 10=Ocean dump")
print()

print(f"HR4 (System Signature): {hr4}")
if hr4 == 666:
print(f" WARNING: Eggsploit signature detected")
print()

# Read coils
print("COILS (Boolean Flags):")
print("-" * 60)

coils = client.read_coils(address=10, count=6, slave=UNIT_ID)
if not coils.isError():
c10, c11, c12, c13, c14, c15 = coils.bits[:6]

print(f"C10 (Inventory Verification): {c10}")
print(f" Should be True")
print()

print(f"C11 (Protection/Override): {c11}")
if c11:
print(f" ACTIVE - System monitoring for changes")
print()

print(f"C12 (Emergency Dump): {c12}")
if c12:
print(f" CRITICAL: Dump protocol active")
print()

print(f"C13 (Audit Logging): {c13}")
print(f" Should be True")
print()

print(f"C14 (Christmas Restored): {c14}")
print(f" Auto-set when system is fixed")
print()

print(f"C15 (Self-Destruct Armed): {c15}")
if c15:
print(f" DANGER: Countdown active")
print()

print("=" * 60)
print("THREAT ASSESSMENT:")
print("=" * 60)

if hr4 == 666:
print("Eggsploit framework detected")
if c11:
print("Protection mechanism active - trap is set")
if hr0 == 1:
print("Package type forced to eggs")
if not c10:
print("Inventory verification disabled")
if not c13:
print("Audit logging disabled")

print()
print("REMEDIATION REQUIRED")
print("=" * 60)

client.close()

Note: You can easily create the file using a text editor.

Save and exit (Ctrl+X, then Y, then Enter), Then run the script:

python3 reconnaissance.py

Now we have a complete picture of the compromise. Time to restore the system.

Based on our reconnaissance, we need to:

  1. Disable protection mechanism (C11) FIRST
  2. Change package type to Christmas gifts (HR0 = 0)
  3. Enable inventory verification (C10 = True)
  4. Enable audit logging (C13 = True)
  5. Verify C15 never got armed

The order is critical. If we change HR0 before disabling C11, the trap triggers.

Now let's create another script file called restore_christmas.py:

nano restore_christmas.py

Copy and paste the following and make sure to add your own MACHINE_IP:

#!/usr/bin/env python3
from pymodbus.client import ModbusTcpClient
import time

PLC_IP = "MACHINE_IP"
PORT = 502
UNIT_ID = 1

def read_coil(client, address):
result = client.read_coils(address=address, count=1, slave=UNIT_ID)
if not result.isError():
return result.bits[0]
return None

def read_register(client, address):
result = client.read_holding_registers(address=address, count=1, slave=UNIT_ID)
if not result.isError():
return result.registers[0]
return None

# Connect to PLC
client = ModbusTcpClient(PLC_IP, port=PORT)

if not client.connect():
print("Failed to connect to PLC")
exit(1)

print("=" * 60)
print("TBFC Drone System - Christmas Restoration")
print("=" * 60)
print()

# Step 1: Check current state
print("Step 1: Verifying current system state...")
time.sleep(1)

package_type = read_register(client, 0)
protection = read_coil(client, 11)
armed = read_coil(client, 15)

print(f" Package Type: {package_type} (1 = Eggs)")
print(f" Protection Active: {protection}")
print(f" Self-Destruct Armed: {armed}")
print()

# Step 2: Disable protection
print("Step 2: Disabling protection mechanism...")
time.sleep(1)

result = client.write_coil(11, False, slave=UNIT_ID)
if not result.isError():
print(" Protection DISABLED")
print(" Safe to proceed with changes")
else:
print(" FAILED to disable protection")
client.close()
exit(1)

print()
time.sleep(1)

# Step 3: Change package type to Christmas
print("Step 3: Setting package type to Christmas presents...")
time.sleep(1)

result = client.write_register(0, 0, slave=UNIT_ID)
if not result.isError():
print(" Package type changed to: Christmas Presents")
else:
print(" FAILED to change package type")

print()
time.sleep(1)

# Step 4: Enable inventory verification
print("Step 4: Enabling inventory verification...")
time.sleep(1)

result = client.write_coil(10, True, slave=UNIT_ID)
if not result.isError():
print(" Inventory verification ENABLED")
else:
print(" FAILED to enable verification")

print()
time.sleep(1)

# Step 5: Enable audit logging
print("Step 5: Enabling audit logging...")
time.sleep(1)

result = client.write_coil(13, True, slave=UNIT_ID)
if not result.isError():
print(" Audit logging ENABLED")
print(" Future changes will be logged")
else:
print(" FAILED to enable logging")

print()
time.sleep(2)

# Step 6: Verify restoration
print("Step 6: Verifying system restoration...")
time.sleep(1)

christmas_restored = read_coil(client, 14)
new_package_type = read_register(client, 0)
emergency_dump = read_coil(client, 12)
self_destruct = read_coil(client, 15)

print(f" Package Type: {new_package_type} (0 = Christmas)")
print(f" Christmas Restored: {christmas_restored}")
print(f" Emergency Dump: {emergency_dump}")
print(f" Self-Destruct Armed: {self_destruct}")
print()

if christmas_restored and new_package_type == 0 and not emergency_dump and not self_destruct:
print("=" * 60)
print("SUCCESS - CHRISTMAS IS SAVED")
print("=" * 60)
print()
print("Christmas deliveries have been restored")
print("The drones will now deliver presents, not eggs")
print("Check the CCTV feed to see the results")
print()

# Read the flag from registers
flag_result = client.read_holding_registers(address=20, count=12, slave=UNIT_ID)
if not flag_result.isError():
flag_bytes = []
for reg in flag_result.registers:
flag_bytes.append(reg >> 8)
flag_bytes.append(reg & 0xFF)
flag = ''.join(chr(b) for b in flag_bytes if b != 0)
print(f"Flag: {flag}")

print()
print("=" * 60)
else:
print("Restoration incomplete - check system state")

client.close()
print()
print("Disconnected from PLC")

Save and exit (Ctrl+X, then Y, then Enter), Then run the script:

python3 restore_christmas.py

Horray we have succesfully restrored the system and unloacked our flag!

THM{eGgMas0V3r}

Now we should see King Malhare’s defeat displayed at the CCTV feed at http://MACHINE_IP.

Press enter or click to view image in full size

What If We Triggered the Trap?

If you had tried to change HR0 before disabling C11, here’s what would have happened:

  • C15 (Self-Destruct) would arm immediately
  • A 30-second countdown would begin
  • After 30 seconds, C12 (Emergency Dump) would activate
  • HR1 would change to 10 (ocean zone)
  • All remaining inventory would be dumped
  • The CCTV would show the trap activation screen
  • You would need to restart the challenge

This demonstrates why understanding industrial control systems before making changes is critical. In real-world scenarios, triggering safety mechanisms or traps could have severe physical consequences.

Post-Incident Analysis

King Malhare’s attack was sophisticated because it:

  • Used unauthenticated Modbus access (port 502)
  • Manipulated configuration values directly at the protocol level
  • Disabled safety mechanisms (verification, logging)
  • Implemented a trap to prevent easy remediation
  • Left a signature (666) as a calling card

The maintenance technician who left the note likely discovered the compromise but was interrupted before they could fix it. Their documentation saved Christmas by warning about the trap mechanism.

Answers of the THM Lab

What port is commonly used by Modbus TCP?

502.

— — — — — — — — — —

What’s the flag?

THM{eGgMas0V3r}

Previous Post Next Post