device-identification

practical

Lesson 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:

  1. 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

  2. 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.

  3. ICS-CERT advisories: cisa.gov/ics-advisories — CISA publishes advisories for ICS vulnerabilities independently of NVD, often with more detail on impact.

  4. 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.

  5. 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 ReadDeviceInformationRequest from pymodbus.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