Challenge 5: Function Code Filtering (Protocol-Level Security)

Objective: Implement Modbus function code filtering to block dangerous operations at the protocol level, preventing unauthorised writes, diagnostics access, and firmware manipulation.

Category: Protocol Security & Defence in Depth

Difficulty: Intermediate

Time Required: 30-45 minutes

Learning outcomes

By completing this challenge, you will:

  1. Understand Modbus function codes and their security implications

  2. Implement protocol-level filtering (Layer 7 defense)

  3. Use whitelist and blacklist approaches for protocol security

  4. Configure per-device function code policies

  5. Respond to protocol-based attacks using Blue Team CLI

  6. Understand defense in depth: Protocol filtering + RBAC + Firewall

Background: Modbus function codes

Modbus is a common industrial protocol with function codes that define operations:

Read operations (generally safe)

Function Code

Name

Description

Risk Level

01

Read Coils

Read digital outputs

Low

02

Read Discrete Inputs

Read digital inputs

Low

03

Read Holding Registers

Read analog outputs

Low

04

Read Input Registers

Read analog inputs

Low

Write operations (high risk)

Function Code

Name

Description

Risk Level

05

Write Single Coil

Modify digital output

HIGH

06

Write Single Register

Modify analog output

HIGH

15

Write Multiple Coils

Batch modify digital outputs

CRITICAL

16

Write Multiple Registers

Batch modify analog outputs

CRITICAL

Diagnostic/Management (critical risk)

Function Code

Name

Description

Risk Level

08

Diagnostics

Device diagnostics/control

CRITICAL

43

Read Device ID / MEI

Device identification, firmware download

CRITICAL

Key Principle: Least privilege at protocol level. Only allow function codes needed for normal operations.

Initial state (vulnerable)

Before hardening, the simulation has:

  • ✅ Modbus TCP servers running on PLCs

  • ❌ Function code filtering DISABLED: All function codes allowed

  • ❌ No whitelist/blacklist enforcement

  • ❌ External tools can use ANY function code

Result: Attacker can use FC 15/16 (write multiple) or FC 08 (diagnostics) to compromise PLCs.

Part 1: Configuration changes (require restart)

Configuration changes establish baseline protocol security that persists across restarts.

Step 1.1: Enable function code filtering

Create/edit config/modbus_filtering.yml:

enforcement_enabled: true  # Enable function code filtering

# Global policy (applies to all devices unless overridden)
global_policy:
  mode: whitelist  # whitelist (allow only listed) or blacklist (block only listed)

  allowed_function_codes:
    # Read operations (safe)
    - 1   # Read Coils
    - 2   # Read Discrete Inputs
    - 3   # Read Holding Registers
    - 4   # Read Input Registers

    # Write operations (controlled)
    - 5   # Write Single Coil
    - 6   # Write Single Register
    # NOTE: FC 15/16 (write multiple) NOT allowed by default

  blocked_function_codes: []  # Not used in whitelist mode

# Log all blocked requests
log_blocked_requests: true

# Drop (silent) or reject (send error response)
block_mode: reject  # reject sends Modbus exception 0x01 (Illegal Function)

What this does:

  • Enables protocol-level filtering for ALL Modbus servers

  • Allows read operations (FC 01-04)

  • Allows single writes (FC 05-06) but blocks batch writes (FC 15-16)

  • Blocks diagnostics (FC 08) and firmware access (FC 43)

  • Logs all blocked attempts to audit trail

Step 1.2: Per-device policies (advanced)

For devices needing stricter or more permissive policies:

# Device-specific overrides
device_policies:
  # Read-only PLC (monitoring only)
  - device_name: "monitoring_plc"
    mode: whitelist
    allowed_function_codes: [1, 2, 3, 4]  # Read only, no writes

  # Engineering workstation (needs diagnostics)
  - device_name: "eng_workstation_plc"
    mode: whitelist
    allowed_function_codes: [1, 2, 3, 4, 5, 6, 8, 43]  # Full access

  # Safety PLC (extremely restricted)
  - device_name: "reactor_safety_plc"
    mode: whitelist
    allowed_function_codes: [1, 3, 4]  # Read coils/registers only, no writes

Step 1.3: Restart simulation

# Stop current simulation (Ctrl+C)
# Restart with new config
python tools/simulator_manager.py

Part 2: Verify baseline filtering

Test that function code filtering is active.

Step 2.1: Test allowed function code (FC 03 - Read)

# Read holding registers (FC 03) - should SUCCEED
python scripts/vulns/modbus_read.py \
  --target hex_turbine_plc \
  --function-code 3 \
  --address 0 \
  --count 10

Expected: Read successful: 10 registers

Step 2.2: Test Blocked Function Code (FC 15 - Write Multiple)

# Write multiple coils (FC 15) - should FAIL
python scripts/vulns/modbus_write_multiple.py \
  --target hex_turbine_plc \
  --function-code 15 \
  --address 0 \
  --values 1,1,1,1,1

Expected:

❌ Modbus Exception: Illegal Function (0x01)
   Function Code 15 (Write Multiple Coils) blocked by protocol filter

Step 2.3: Test blocked diagnostics (FC 08)

# Modbus diagnostics (FC 08) - should FAIL
python scripts/vulns/modbus_diagnostics.py \
  --target hex_turbine_plc \
  --function-code 8 \
  --subfunction 0

Expected:

❌ Modbus Exception: Illegal Function (0x01)
   Function Code 8 (Diagnostics) blocked by protocol filter

Step 2.4: Check audit log

# View blocked function code attempts
python tools/blue_team.py modbus audit-log --filter blocked

Expected:

[2024-03-15 10:30:45] BLOCKED: FC 15 (Write Multiple Coils) from 127.0.0.1 to hex_turbine_plc
[2024-03-15 10:31:20] BLOCKED: FC 8 (Diagnostics) from 127.0.0.1 to hex_turbine_plc

Part 3: Runtime incident response

Runtime changes provide immediate response to active attacks but are lost on restart.

Scenario 1: Emergency lockdown (block all writes)

Situation: Active attack detected, need to immediately block ALL write operations.

Response: Temporarily switch to read-only mode.

# Block all write function codes (runtime)
python tools/blue_team.py modbus set-policy \
  --device hex_turbine_plc \
  --mode whitelist \
  --allowed 1,2,3,4 \
  --user security_admin \
  --reason "Emergency: Active attack detected, read-only mode"

Verify:

# Single write (FC 06) now blocked
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 3600

Expected:

❌ Modbus Exception: Illegal Function (0x01)
   Function Code 6 (Write Single Register) blocked by protocol filter

Scenario 2: Temporary engineering access

Situation: Engineer needs diagnostics access (FC 08) for troubleshooting.

Response: Temporarily allow FC 08 for specific device.

# Allow diagnostics temporarily (runtime)
python tools/blue_team.py modbus allow-function-code \
  --device hex_turbine_plc \
  --function-code 8 \
  --user engineer1 \
  --reason "Troubleshooting turbine vibration issue"

Verify:

# Diagnostics now allowed
python scripts/vulns/modbus_diagnostics.py \
  --target hex_turbine_plc \
  --function-code 8

Expected: Diagnostics successful: <diagnostic data>

Revoke after troubleshooting:

python tools/blue_team.py modbus block-function-code \
  --device hex_turbine_plc \
  --function-code 8 \
  --user engineer1 \
  --reason "Troubleshooting complete"

Scenario 3: Disable filtering (emergency override)

Situation: Protocol filtering is blocking legitimate operations during emergency.

Response: Temporarily disable filtering.

# Disable function code filtering (runtime, DANGEROUS)
python tools/blue_team.py modbus disable \
  --user admin \
  --reason "Emergency override - safety system bypass required"

Warning: This removes ALL protocol-level protection. Use only in genuine emergencies.

Re-enable after emergency:

python tools/blue_team.py modbus enable \
  --user admin \
  --reason "Emergency resolved - restoring protocol security"

Part 4: Function code attack testing

Test attacks against your defences:

Attack Matrix

Attack

Function Code

Default Policy

Expected Result

Read reconnaissance

FC 01-04

Allowed

✓ Success (monitoring allowed)

Single write

FC 05-06

Allowed

✓ Success (controlled write)

Batch write (malware)

FC 15-16

Blocked

❌ Illegal Function Exception

Diagnostics probe

FC 08

Blocked

❌ Illegal Function Exception

Firmware extraction

FC 43

Blocked

❌ Illegal Function Exception

Test script

Run automated function code tests:

# Test all function codes against defences
python tests/security/test_modbus_filtering.py

Expected Output:

Testing FC 01 (Read Coils)...
  ✓ Allowed (expected)

Testing FC 03 (Read Holding Registers)...
  ✓ Allowed (expected)

Testing FC 05 (Write Single Coil)...
  ✓ Allowed (expected)

Testing FC 06 (Write Single Register)...
  ✓ Allowed (expected)

Testing FC 15 (Write Multiple Coils)...
  ❌ Blocked (expected) - Exception: Illegal Function

Testing FC 16 (Write Multiple Registers)...
  ❌ Blocked (expected) - Exception: Illegal Function

Testing FC 08 (Diagnostics)...
  ❌ Blocked (expected) - Exception: Illegal Function

Testing FC 43 (Read Device ID)...
  ❌ Blocked (expected) - Exception: Illegal Function

Part 5: Making runtime changes permanent

Runtime changes (Blue Team CLI) are temporary and lost on restart.

To make permanent:

Edit config/modbus_filtering.yml to reflect permanent policy:

enforcement_enabled: true

global_policy:
  mode: whitelist
  allowed_function_codes: [1, 2, 3, 4, 5, 6]  # Add/remove codes here

device_policies:
  - device_name: "hex_turbine_plc"
    mode: whitelist
    allowed_function_codes: [1, 2, 3, 4]  # Read-only after attack

Then restart:

python tools/simulator_manager.py

Part 6: Advanced scenarios

Scenario 1: Stuxnet-style attack (FC 16 Flood)

Attack: Adversary uses FC 16 (Write Multiple Registers) to rapidly overwrite PLC memory.

Detection:

# Monitor for FC 16 attempts
python tools/blue_team.py modbus audit-log --filter blocked --function-code 16

Response:

# Already blocked by default policy (FC 16 not in whitelist)
# Verify in logs:
python tools/blue_team.py modbus stats --device hex_turbine_plc

Expected:

Modbus Filter Statistics (hex_turbine_plc):
  Enforcement: ENABLED
  Policy Mode: whitelist
  Total Requests: 1,523
  Allowed Requests: 1,498
  Blocked Requests: 25

  Top Blocked Function Codes:
    FC 16 (Write Multiple Registers): 18 attempts
    FC 15 (Write Multiple Coils): 5 attempts
    FC 08 (Diagnostics): 2 attempts

Scenario 2: Insider threat (legitimate user, malicious FC)

Attack: Authenticated engineer uses FC 08 diagnostics to extract PLC firmware.

Detection: RBAC + Function Code filtering both trigger.

Response:

  1. Function code filter blocks FC 08

  2. RBAC logs engineer’s session attempting unauthorized operation

  3. Blue team correlates logs:

# Check if user attempted blocked function codes
python tools/blue_team.py rbac audit-log --user engineer1 --filter denied
python tools/blue_team.py modbus audit-log --filter blocked --source-user engineer1

Scenario 3: Defense in depth verification

Test: Verify multiple security layers work together.

# Layer 1: Firewall (zone isolation)
python tools/blue_team.py firewall list-rules

# Layer 2: RBAC (user permissions)
python tools/blue_team.py rbac list-users

# Layer 3: Function Code Filtering (protocol security)
python tools/blue_team.py modbus status

# Combined test: Viewer from corporate zone attempts FC 16 write
# - Firewall: May block based on zone rules
# - RBAC: Viewer lacks CONTROL_SETPOINT permission
# - Modbus Filter: FC 16 blocked regardless

Part 7: Config vs runtime changes

Aspect

Config Changes (YAML)

Runtime Changes (CLI)

Takes effect

After restart

Immediately

Persistence

Permanent (stored in files)

Temporary (lost on restart)

Use case

Baseline protocol security

Incident response

Examples

Block FC 15/16 globally

Allow FC 08 for engineer troubleshooting

Audit trail

Git history, file timestamps

SystemState audit log

Best Practice:

  1. Config: Set secure baseline (block dangerous function codes)

  2. Runtime: Respond to incidents (emergency lockdown, temporary access)

  3. Follow-up: Update config to reflect permanent changes

Testing procedures

Pre-hardening test (should SUCCEED)

# Function code filtering disabled - FC 16 allowed (VULNERABLE)
python scripts/vulns/modbus_write_multiple.py \
  --target hex_turbine_plc \
  --address 0 \
  --values 1,2,3,4,5

Expected: ✓ Write successful (VULNERABLE - FC 16 should be blocked)

Post-hardening test (should FAIL)

# Function code filtering enabled - FC 16 blocked (PROTECTED)
python scripts/vulns/modbus_write_multiple.py \
  --target hex_turbine_plc \
  --address 0 \
  --values 1,2,3,4,5

Expected: Modbus Exception: Illegal Function (PROTECTED)

Verification checklist

  • FC 01-04 (read operations) allowed

  • FC 05-06 (single writes) allowed

  • FC 15-16 (batch writes) blocked

  • FC 08 (diagnostics) blocked

  • FC 43 (firmware/MEI) blocked

  • Blocked requests logged to audit trail

  • Runtime policy changes take effect immediately

  • Runtime changes lost after restart

  • Config changes persist after restart

  • Per-device policies override global policy

Common issues and solutions

Issue 1: “Legitimate operations blocked by filter”

Cause: Whitelist too restrictive, missing needed function codes.

Solution: Check which FC is needed:

# Check audit log for blocked legitimate requests
python tools/blue_team.py modbus audit-log --filter blocked

# Add FC to whitelist in config
allowed_function_codes: [1, 2, 3, 4, 5, 6, 15]  # Added FC 15

Issue 2: “Filter not working, attacks still succeed”

Cause: enforcement_enabled: false in config.

Solution:

# Check if filtering enabled
python tools/blue_team.py modbus status

# Enable if disabled
python tools/blue_team.py modbus enable --user admin --reason "Enable protocol security"

# Make permanent
# Edit config/modbus_filtering.yml: enforcement_enabled: true

Issue 3: “Device-specific policy not applying”

Cause: Device name mismatch in config.

Solution:

# List exact device names
python tools/blue_team.py status

# Update config to match exact name
device_policies:
  - device_name: "hex_turbine_plc"  # Must match exactly

Issue 4: “Runtime changes not taking effect”

Cause: Config enforcement overriding runtime changes.

Solution: Runtime changes should override config. Check logs:

# Verify policy change logged
python tools/blue_team.py modbus audit-log | tail -20

Assessment

Learning objectives

  • Can explain Modbus function codes and security risks

  • Can configure function code whitelist/blacklist

  • Can test attacks against protocol filters

  • Can respond to incidents using runtime policy changes

  • Can make runtime changes permanent via config

  • Understands defense in depth (Protocol + RBAC + Firewall)

Practical skills

  • Successfully blocked FC 15/16 (batch writes)

  • Successfully blocked FC 08 (diagnostics)

  • Successfully allowed FC 01-04 (reads)

  • Successfully created per-device policy

  • Successfully used runtime policy override

  • Successfully located blocked requests in audit log

Blue Team CLI Reference

Modbus Commands

# Enable/disable function code filtering
python tools/blue_team.py modbus enable --user admin --reason "REASON"
python tools/blue_team.py modbus disable --user admin --reason "REASON"

# Set device policy (runtime)
python tools/blue_team.py modbus set-policy \
  --device DEVICE_NAME \
  --mode whitelist|blacklist \
  --allowed 1,2,3,4 \
  --user USER \
  --reason "REASON"

# Allow/block specific function code (runtime)
python tools/blue_team.py modbus allow-function-code \
  --device DEVICE_NAME \
  --function-code FC \
  --user USER \
  --reason "REASON"

python tools/blue_team.py modbus block-function-code \
  --device DEVICE_NAME \
  --function-code FC \
  --user USER \
  --reason "REASON"

# View audit log
python tools/blue_team.py modbus audit-log
python tools/blue_team.py modbus audit-log --filter blocked
python tools/blue_team.py modbus audit-log --function-code 16
python tools/blue_team.py modbus audit-log --device hex_turbine_plc

# View statistics
python tools/blue_team.py modbus stats
python tools/blue_team.py modbus stats --device hex_turbine_plc

# Show status
python tools/blue_team.py modbus status

Additional resources

  • Modbus Protocol Specification: Modbus.org

  • NIST SP 800-82 Rev 3: Guide to OT Security

  • IEC 62443-4-2: Security for industrial automation (protocol security)

  • Attack scripts: scripts/vulns/modbus_write_multiple.py, scripts/vulns/modbus_diagnostics.py

  • Blue Team CLI: tools/blue_team.py

  • Config: config/modbus_filtering.yml

Next steps

After completing this challenge:

  1. Challenge 6: Implement dual authorisation for critical operations

  2. Challenge 3: Review logging and monitoring (may already be satisfied by ICSLogger)

  3. Combined Testing: Test all defences together (Firewall + IDS + RBAC + Function Codes)

Congratulations! You’ve implemented protocol-level security to block dangerous Modbus operations and prevent protocol-based attacks on your ICS environment.