device-identification
practicalLesson 4 — Device Identification and Target Profiling
Function Code 43 — The Attacker's Favorite
FC43 (0x2B) is called "Encapsulated Interface Transport." It is a general-purpose container for sub-protocols. The sub-protocol relevant to us is MEI type 0x0E: Read Device Identification. This is defined in the Modbus Application Protocol specification v1.1b3, Annex E.
A single FC43 request with MEI type 0x0E and read code 0x01 (streaming read) can return all basic device information in one round-trip. No authentication. No session establishment. Just send the request.
The device identification objects:
| Object ID | Category | Name | Typical Content |
|---|---|---|---|
| 0x01 | Basic | VendorName | "Schneider Electric" |
| 0x02 | Basic | ProductCode | "BMX P34 2020" |
| 0x03 | Basic | MajorMinorRevision | "V3.40" |
| 0x04 | Regular | VendorURL | "www.schneider-electric.com" |
| 0x05 | Regular | ProductName | "Modicon M340" |
| 0x06 | Regular | ModelName | Sometimes different from ProductCode |
| 0x07 | Regular | UserApplicationName | Sometimes operator-defined asset label |
| 0x80–0xFF | Extended | Vendor-specific | Varies; can include serial number, production date |
"Basic" objects (0x01–0x03) are available via read code 0x01. "Regular" objects require read code 0x02. "Extended" objects require read code 0x03. Start with 0x01 — it tells you enough to identify the device.
Sending FC43 with pymodbus
pymodbus implements the Read Device Identification request through the ReadDeviceInformationRequest class:
from pymodbus.client import ModbusTcpClient
from pymodbus.mei_message import ReadDeviceInformationRequest
client = ModbusTcpClient("127.0.0.1", port=502)
client.connect()
# Read code 0x01 = basic stream (VendorName, ProductCode, Revision)
request = ReadDeviceInformationRequest(
read_code=0x01,
object_id=0x00,
slave=1
)
response = client.execute(request)
if not response.isError():
for obj_id, obj_value in response.information.items():
try:
decoded = obj_value.decode('utf-8')
except Exception:
decoded = obj_value.hex()
print(f"Object 0x{obj_id:02X}: {decoded}")
client.close()
Expected output on a Schneider M340:
Object 0x01: Schneider Electric
Object 0x02: BMX P34 2020
Object 0x03: V3.40
Reading regular objects (read code 0x02):
request = ReadDeviceInformationRequest(read_code=0x02, object_id=0x00, slave=1)
response = client.execute(request)
Reading extended objects (read code 0x03):
request = ReadDeviceInformationRequest(read_code=0x03, object_id=0x80, slave=1)
response = client.execute(request)
Fallback if FC43 is not supported: The device returns FC 0xAB (0x2B | 0x80) with exception code 0x01 (illegal function). In that case, device identification must come from other sources: Shodan banner, nmap service detection, or register values at known vendor-specific addresses.
Building a Complete Target Profile
Device identification is one input. A complete recon profile for a Modbus target includes:
Layer 1 — Network
IP: 203.0.113.47
Port: 502/tcp (confirmed open)
RTT: 45ms (geographic hint — likely France from latency + Shodan org)
Reverse DNS: plc01.station-nord.example.fr
Organization: ExampleISP (from BGP/WHOIS)
First seen on Shodan: 2023-08-12
Layer 2 — Device Identity (from FC43)
VendorName: Schneider Electric
ProductCode: BMX P34 2020
Revision: V2.10
ProductName: Modicon M340
Layer 3 — Unit ID Map (from unit ID scan)
Unit 1: responds to FC03, FC01, FC43
Unit 2: responds to FC03 only (downstream RTU via serial gateway)
Unit 3: responds to FC03 only
Layer 4 — Register Map (from block scan)
Unit 1, Holding registers:
0–99: mapped (live values, changing — sensor data)
100–199: mapped (static — configuration/setpoints)
200–299: exception 0x02 (unmapped)
1000–1099: mapped (static)
Unit 2, Holding registers:
0–9: mapped (all static)
Layer 5 — Vulnerability Assessment
ProductCode: BMX P34 2020, Revision: V2.10
CVE-2022-45788: Schneider Electric Modicon M340 — Pre-auth file read via FTP
CVE-2021-22698: Schneider Modicon — Unauthenticated reboot via malformed Modbus request
Firmware V2.10 is EOL — no patches available for BMX P34 2020 after V3.20
This is the profile you hand to the client in a pentest report, or the profile you use to assess risk when performing defensive research.
Vendor-Specific Recon
Different vendors expose different information. Know what to look for:
Schneider Electric (Modicon M340, M580, Momentum, Quantum)
- FC43 is well-implemented; vendor name always "Schneider Electric"
- Product code directly maps to part numbers in the product catalog
- Firmware version format: V[major].[minor] (e.g., V3.40)
- M580 also runs a web server on port 80 — grab the firmware version from the HTTP headers and /tmp/log/ endpoints
- Known vulnerable firmware: M340 < V3.40 for several CVEs; check Schneider's security notification portal
Siemens S7 (in Modbus compatibility mode)
- FC43 may return minimal information or timeout
- Primary protocol is S7comm (port 102); Modbus support is an add-on
- Nmap s7-info NSE script is more informative than modbus-discover for Siemens
- Register map in Modbus mode maps to Siemens data blocks (DBs) — requires knowledge of the PLC program
Wago 750-352 and 750-881 (fieldbus couplers) - FC43 returns "WAGO" vendor name - Also run a web server on port 80 with a default credential of "admin/wago" on older firmware - Modbus register map is auto-generated from the I/O module configuration - Register 0x1000+ area often contains diagnostic data
Allen-Bradley / Rockwell (Micro800, CompactLogix in Modbus mode) - Many Rockwell PLCs support Modbus only as a secondary protocol via add-on modules - FC43 support varies; often returns nothing or a generic string - Primary protocol is EtherNet/IP (port 44818) — much more information available there
Generic/unknown devices When FC43 returns nothing useful: 1. Check Shodan for historical banners of the same IP 2. Fetch the HTTP server on port 80 if present (many PLCs have embedded web servers) 3. Check port 443 (HTTPS), 102 (S7comm), 44818 (EtherNet/IP), 1089–1091 (EtherNet/IP object model) 4. Look for vendor-specific register addresses: many vendors document a "device type" or "serial number" register in their Modbus mapping documentation
Automating Profile Generation
#!/usr/bin/env python3
"""Generate a Modbus target profile. Authorized use only."""
import json
from pymodbus.client import ModbusTcpClient
from pymodbus.mei_message import ReadDeviceInformationRequest
def get_device_id(client, uid):
"""Query FC43 MEI device identification. Returns dict of decoded objects."""
info = {}
labels = {
0x01: "VendorName",
0x02: "ProductCode",
0x03: "MajorMinorRevision",
0x04: "VendorURL",
0x05: "ProductName",
0x06: "ModelName",
0x07: "UserApplicationName",
}
for read_code in [0x01, 0x02]:
try:
req = ReadDeviceInformationRequest(
read_code=read_code, object_id=0x00, slave=uid
)
resp = client.execute(req)
if not resp.isError():
for obj_id, val in resp.information.items():
label = labels.get(obj_id, f"Object_0x{obj_id:02X}")
try:
info[label] = val.decode("utf-8").strip("\x00")
except Exception:
info[label] = val.hex()
except Exception:
pass
return info
def scan_unit_ids(client):
responding = []
for uid in range(1, 248):
r = client.read_holding_registers(address=0, count=1, slave=uid)
if not r.isError():
responding.append(uid)
return responding
def scan_register_ranges(client, uid, step=100, max_addr=5000):
ranges = []
for addr in range(0, max_addr, step):
r = client.read_holding_registers(address=addr, count=step, slave=uid)
if not r.isError():
ranges.append({"start": addr, "end": addr + step - 1, "values": r.registers})
return ranges
def build_profile(target, port=502):
profile = {"target": target, "port": port, "units": {}}
client = ModbusTcpClient(target, port=port, timeout=2)
if not client.connect():
profile["error"] = "connection_failed"
return profile
print(f"[*] Scanning unit IDs...")
units = scan_unit_ids(client)
profile["responding_unit_ids"] = units
for uid in units:
print(f"[*] Profiling unit {uid}...")
unit_data = {}
unit_data["device_id"] = get_device_id(client, uid)
unit_data["register_ranges"] = scan_register_ranges(client, uid)
profile["units"][uid] = unit_data
client.close()
return profile
if __name__ == "__main__":
import sys
target = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
profile = build_profile(target)
print(json.dumps(profile, indent=2))
Run against the challenge container:
python3 profile.py 127.0.0.1 > target_profile.json
cat target_profile.json
From Profile to CVE
Once you have VendorName + ProductCode + MajorMinorRevision, the vulnerability research workflow:
-
Vendor security portal: Schneider:
se.com/en/work/support/cybersecurity/security-notifications.jsp. Siemens:cert.siemens.com. Rockwell:rockwellautomation.com/en-us/support/advisory.html -
NVD / MITRE CVE: Search
cpe:2.3:o:schneider-electric:modicon_m340_firmware:*in NVD to find all CVEs for the product line, then filter by version. -
ICS-CERT advisories:
cisa.gov/ics-advisories— CISA publishes advisories for ICS vulnerabilities independently of NVD, often with more detail on impact. -
Exploit databases: Some Modbus CVEs have public PoC code in ExploitDB or GitHub. A firmware version from FC43 can lead directly to a working exploit.
-
Vendor changelogs: Release notes for firmware updates often document security fixes without explicit CVE assignment ("fixed an issue where malformed packets caused the device to restart" = unacknowledged DoS).
Key Takeaways
- FC43 MEI type 0x0E returns vendor, model, and firmware version in one unauthenticated request
- Read codes 0x01 (basic), 0x02 (regular), 0x03 (extended) progressively reveal more information
- Use
ReadDeviceInformationRequestfrompymodbus.mei_message - A complete target profile combines: network data, FC43 response, unit ID map, and register map
- Cross-reference firmware version against vendor security portals, NVD, and ICS-CERT
- Vendor-specific register addresses (web servers on port 80, serial numbers in specific holding registers) supplement FC43 data when FC43 is unsupported