When to use / when not to use
- Use: when you have industrial control (ICS/SCADA) traffic (pcap / live capture) and need to parse Modbus / DNP3 / OPC UA semantics
- Use: to reconstruct a PLC/RTU point table (coils/registers/object mapping) from traffic
- Use: for industrial control device (PLC/HMI/RTU) firmware and communication analysis (together with [[re-firmware]])
- Use: for authorized-scope ICS security testing (see step 5 boundaries)
- Don't use: when the traffic is ciphertext/has an encryption layer (first [[re-crypto-id]] → [[re-crypto-keys]] → [[re-crypto-decrypt]])
- Don't use: for non-industrial-control custom protocols (that's [[re-proto-rev]])
- Don't use: when you only need packet capture (that's [[re-netcap]])
Tool preparation
Verify all tools before use. ICS analysis red lines:Only test authorized systems, default to isolated networks ([[re-analyze/platform-tips]] highest principle) — the test environment must never connect to a production OT network (see step 5).
wireshark / tshark —— the mainstay for ICS parsing (modbus/dnp3/opcua dissectors built in)
- Linux:
apt install wireshark tshark/dnf install wireshark-cli wireshark/pacman -S wireshark-cli wireshark-qt(Arch has split it into wireshark-cli + wireshark-qt; when installing on Debian, choose "allow non-root packet capture", orsudo dpkg-reconfigure wireshark-commonthen add the user to the wireshark group) - macOS:
brew install --cask wireshark(includes the tshark CLI; orbrew install wiresharkCLI only) - Windows:
choco install wireshark(or the official installer package); use the Linux version inside WSL - Verify:
tshark --version;tshark -G fields | grep -iE 'modbus|dnp3|opcua'lists ICS field names (field names may be slightly adjusted between versions; the output of that command is authoritative)
scapy —— modbus layer parsing and construction (contrib layer)
- All platforms:
pip install scapy(Python 3, recommended) - Linux:
apt install python3-scapy(Debian-based; includes the contrib layer) - Verify:
python3 -c "from scapy.contrib.modbus import ModbusADURequest, ModbusADUResponse; print('ok')" - Note: the modbus layer is in
scapy.contrib.modbus(not a default layer, requires explicit import,ModbusADURequest/ModbusADUResponse+ModbusPDU0X*Request/Responsevarious function code classes);no version of scapy has DNP3 / OPC UA layers → for DNP3/OPC UA use Wireshark parsing or write a custom parser (see step 4) - To read pcap use
rdpcap(pure Python parsing, no libpcap needed)
python3 —— runtime environment for parsing scripts
- For installation and verification see [[re-proto-rev]] tool preparation
Operating steps
Execute in order, and record the result of each step. Prerequisites: the traffic has been captured ([[re-netcap]]) and is plaintext (for ciphertext first go through [[re-crypto-decrypt]]).
-
Protocol identification (port + characteristics):
tshark -r cap.pcap -T fields -e tcp.dstport | sort | uniq -c | sort -rn # 端口统计 tshark -r cap.pcap -q -z io,phs # 协议分层统计(dissector 命中情况)- Quick port reference: Modbus/TCP 502 (Modbus over TLS 802), DNP3 20000/TCP,UDP,SCTP (secure DNP3 dnp-sec 19999/TCP,UDP,SCTP), OPC UA binary 4840/TCP, S7comm 102, EtherNet/IP 44818, BACnet/IP 47808/UDP
- Ports serve only as an initial judgment (industrial control often uses non-standard ports); confirm with characteristics: Modbus/TCP transaction = transId(2) + protoId 0x0000(2) + len(2) + unitId(1) + function code(1); the first 2 bytes of a DNP3 link frame are fixed
05 64; the OPC UA binary header has a 3-byte message type (HEL/ACK/OPN/CLO/MSG)
-
Wireshark parsing (dissector first):
- GUI: open the pcap; standard ports automatically hit the modbus/dnp3/opcua dissectors; non-standard ports → right-click Decode As to specify the protocol (or command line
tshark -d tcp.port==5020,modbus) - Filtering and export:
tshark -r cap.pcap -Y 'modbus' -T fields -e modbus.func_code -e mbtcp.unit_id # unit id 官方字段在 mbtcp 层(modbus.unit_id 非官方) tshark -r cap.pcap -Y 'dnp3' -T fields -e dnp3.ctl -e dnp3.dst -e dnp3.src # 控制字 dnp3.ctl(子字段 dnp3.ctl.dir/prm/fcb/fcv/prifunc/secfunc);站地址在顶层 dnp3.dst/dnp3.src tshark -r cap.pcap -Y 'opcua' -T fields -e opcua.nodeid.numeric -e opcua.nodeid.string -e opcua.nodeid.guid # nodeid 无顶层字段,按类型用子字段 - When the field name is uncertain, use
tshark -G fields | grep -iE 'modbus|dnp3|opcua'as the authoritative output
- GUI: open the pcap; standard ports automatically hit the modbus/dnp3/opcua dissectors; non-standard ports → right-click Decode As to specify the protocol (or command line
-
Function Codes and Object Point Tables:
- Modbus Function Code Quick Reference: 01 Read Coils / 02 Read Discrete Inputs / 03 Read Holding Registers / 04 Read Input Registers / 05 Write Single Coil / 06 Write Single Register / 0F Write Multiple Coils / 10 Write Multiple Registers; Exception Response = 0x80 | Function Code
- Point Table Construction: Collect (unitId, function code, starting address, quantity) from the traffic, map register/coil addresses to tags (temperature, pressure, valve position, etc.) against the engineering documentation, and produce an "address → type → value → meaning" table
- Value Parsing: registers are 16-bit big-endian; for 32-bit combinations (IEEE754 floating point / int32) pay attention to word order (see Pitfall 2)
- DNP3 Object Point Table: group-variation number.Static quantities and event quantities are paired, different groups—Group 1 Binary Input ↔ Group 2 Binary Input Event;Group 3 Double-bit Binary Input (static) ↔ Group 4 Double-bit Input Event; Group 10 Binary Output Status ↔ Group 11 Binary Output Event; Group 30 Analog Input ↔ Group 32 Analog Input Event.Don't mistake static quantity groups for event groups(Group 3 is not an "event"); when reconstructing the point table, model it in static/event pairs, otherwise the event cache and variation semantics will be judged entirely reversed
- OPC UA: NodeId (NamespaceIndex:Identifier) + BrowseName is the point identifier
- Difference Method: Compare the traffic of normal operation vs. triggered action; the differing messages = key points
-
scapy parser (modbus) + custom DNP3 parsing:
from scapy.all import rdpcap from scapy.contrib.modbus import ModbusADURequest, ModbusADUResponse for p in rdpcap('cap.pcap'): if 'TCP' in p and p['TCP'].dport == 502 and len(p['TCP'].payload) >= 6: mb = ModbusADURequest(bytes(p['TCP'].payload)) # MBAP+PDU 一起解 mb.show() # transId/protoId/len/unitId + PDU(功能码与寄存器字段)- DNP3 (scapy has no layer): the link layer header structure is fixed (
05 64 | len | ctrl | dst(2) | src(2) | CRC(2), after the source address comes the mandatory header CRC), so parsing with struct is sufficient; subsequent data is chunked with a 2-byte CRC every 16 bytes; the transport layer/application layer proceed according to the group-variation format of IEEE 1815 - Construct test messages (authorization and isolation must be confirmed before sending to the testbed PLC, see Step 5):
from scapy.contrib.modbus import ModbusPDU03ReadHoldingRegistersRequest req = ModbusADURequest() / ModbusPDU03ReadHoldingRegistersRequest(startAddr=0, quantity=10) # 字段名是 camelCase startAddr(scapy 定义),不是 start_addr
- DNP3 (scapy has no layer): the link layer header structure is fixed (
-
Authorization Scope and Security Testing Boundaries:
- Only test authorized systems: owned/laboratory testbeds (PLC/RTU/HMI simulators), vendor-authorized test equipment, and targets within the scope of vulnerability disclosure;Strictly prohibitedto conduct unauthorized testing on production SCADA/DCS and critical facilities such as power grids/water plants/power plants (involves law and public safety)
- Physical/logical isolation: separate the test network from the production OT network (independent subnet/VLAN, firewall); the packet capture and analysis machine connects only to the testbed; by default follow the [[re-sandbox]] network isolation approach
- Sending abnormal messages (fuzzing, illegal function codes, unauthorized writes) is limited to the testbed, with full audit logging throughout; if uncertain, fall back to passive analysis (capture and parse only)
- Report sanitization: do not expose production environment site/personnel/network topology details; comply with local industrial control security testing regulations and retain written authorization
Cross-domain combination
- [[re-protocol]]: this skill is its industrial control branch (the entry point of the decision tree after Step 5 of the workflow)
- [[re-netcap]]: source of captured raw material (capture points/sandbox isolation)
- [[re-sandbox]]: security testing boundaries—testbed isolation, only test authorized systems ([[re-analyze/platform-tips]] highest principle)
- [[re-crypto-id]] / [[re-crypto-keys]] / [[re-crypto-decrypt]]: encryption variant pre-check (Modbus over TLS, OPC UA secure channel; DNP3 Secure Authentication is an authentication extension rather than default encryption, and whether TLS/VPN is layered on top needs separate confirmation)
- [[re-firmware]]: PLC/RTU/industrial control device firmware analysis (extraction → rootfs → emulation)
- [[re-proto-rev]]: state machine reconstruction for non-standard industrial control protocols
- [[re-hardware-io]]: capturing Modbus RTU / fieldbus over serial
Common pitfalls and traps
- Mixing up function code semantics: symptom—treating 01 read coils as 03 read registers, all value types wrong (coils 1 bit vs registers 16 bits); cause—function codes determine data type and address space, and statistics were not bucketed by function code; countermeasure—first tally by function code (
tshark -Y modbus.func_code), and separate point table spaces by function code (01/02 coil space, 03/04 register space) - Big-endian byte order: symptom—register values huge/garbled (e.g. temperature 0x0128=296 read as little-endian becomes 0x2801=10241); cause—Modbus registers default to big-endian, and multi-register combinations (32-bit/float) also have vendor-defined high/low word order layered on top; countermeasure—validate with known values (reasonable range/constants/precision), try all big/little-endian and high/low word order combinations, and once determined, fix it and write it into the parsing script
- Mixing up protocol variants: symptom—dissector misses or parsing is misaligned (Modbus RTU frames parsed as TCP, DNP3 over TLS port); cause—the same protocol has multiple carriers: Modbus RTU/ASCII (serial) vs TCP (502) vs TLS (802); DNP3 serial/TCP 20000 (TLS often reuses 20000 or dnp-sec 19999); OPC UA binary (4840) vs HTTP endpoint; countermeasure—step 1 confirm both port and signature, Wireshark Decode As /
-dforce the dissector - Treating encrypted variants as plaintext: symptom—high traffic entropy, dissector outputs only garbage or marks it as encrypted; cause—Modbus over TLS / OPC UA secure channel are encrypted by default; DNP3 Secure Authentication is an authentication/integrity extension rather than a confidentiality wrapper, and when entropy is high one should first confirm whether a separate TLS/VPN confidentiality layer exists; countermeasure—first determine via [[re-crypto-id]], extract keys from HMI/client process memory and certificates ([[re-crypto-keys]]), and run the decryption chain via [[re-crypto-decrypt]]
- Point table mapping misalignment: symptom—addresses do not match, value meanings wrong; cause—vendors have different addressing conventions (0-based vs 1-based, separate address spaces for coils/registers, mirrored addresses); countermeasure—cross-validate across multiple packets (repeated reads of the same address should be stable), combine with engineering documentation, and build tables separated by space
- S7CommPlus authentication chain (S7-1200/1500): symptom—S7 traffic version mismatch, authentication data cannot be decoded, flows captured with PLCSIM do not match the real device; cause—S7CommPlus is a layered authentication system, with large differences between V1(0x01)/V2(TLS,0x02)/V3(0x03); structure: OpenSSL standard layer (AES-128-ECB/SHA-256/HMAC) + BigInt big number layer (192-bit 6 dword encoding) + Monolith function family (1-11) + key derivation transform pipeline (TPre/T7/T12/T13/KeyD/LutG/Csum/Seed, 8 Transform types) + encrypted authentication data (AES-ECB chain + Blob metadata header + AES-CTR/HarpoHash checksum); key constants: session key 24B, challenge 20B, real PLC public key 40B vs PLCSIM 64B, authentication Blob 180 vs 216B; countermeasure—first confirm the protocol version and target (real device vs PLCSIM, the flow and key lengths differ), and restore along version-specific lines: plaintext structure → key derivation pipeline → Session Key rotation → Packet Digest