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:
Understand Modbus function codes and their security implications
Implement protocol-level filtering (Layer 7 defense)
Use whitelist and blacklist approaches for protocol security
Configure per-device function code policies
Respond to protocol-based attacks using Blue Team CLI
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:
Function code filter blocks FC 08
RBAC logs engineer’s session attempting unauthorized operation
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:
Config: Set secure baseline (block dangerous function codes)
Runtime: Respond to incidents (emergency lockdown, temporary access)
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.pyBlue Team CLI:
tools/blue_team.pyConfig:
config/modbus_filtering.yml
Next steps¶
After completing this challenge:
Challenge 6: Implement dual authorisation for critical operations
Challenge 3: Review logging and monitoring (may already be satisfied by ICSLogger)
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.