◇ OpenCyber Academy
Technical · Chapter 9 · English

Chapter 9 — Critical infrastructure and sector security: tools and methods

Open, free learning for anyone who wants to protect people and technology. No account is required to read this text or its worked answers.

Chapter 9 — Critical infrastructure and sector security: tools and methods

Lesson map

Lesson 09 published. Map, example, EX033.

flowchart TD
  N0["Learning outcomes and connection"]
  N1["9.1 Choosing a method"]
  N2["9.2 DNP3 at the link layer"]
  N3["9.3 Validate the tool first"]
  N4["9.4 Error detection is not authentication"]
  N5["9.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
  N6 --> EX["Worked example"]
  EX --> Q["Exercise EX033"]

Logical drawing

flowchart LR
  N0["Learning outcomes and connection"]
  N1["9.1 Choosing a method"]
  N2["9.2 DNP3 at the link layer"]
  N3["9.3 Validate the tool first"]
  N4["9.4 Error detection is not authentication"]
  N5["9.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX033"]
[Learning outcomes and connection]
    |
    v
[9.1 Choosing a method]
    |
    v
[9.2 DNP3 at the link layer]
    |
    v
[9.3 Validate the tool first]
    |
    v
[9.4 Error detection is not authentication]
    |
    v
[9.6 Practical activity]
    |
    v
[Chapter synthesis]
    |
    v
[Worked example] --> [Exercise EX033]

Learning outcomes and connection

After this chapter you should be able to:

Chapters 7 and 8 used telemetry. This chapter is about the instruments themselves — how to pick them, build small ones correctly and know when they lie.

9.1 Choosing a method

Question Prefer Avoid unless scoped
What is configured? Configuration export, vendor tool read-only view Probing ports to infer configuration
Who talks to whom? Flow logs, passive capture on a SPAN port or tap Active scanning
What does a device run? Asset records, engineering-tool inventory during an approved session Fingerprinting by probing
Does a deny rule work? Firewall logs plus one scoped connect test Broad scans
Is a frame well-formed? Decoder with protocol checks (Chapter 2, this chapter) Eyeballing hex

The guide to OT security in NIST SP 800-82 Rev. 3 discusses passive monitoring and the risks of active techniques (SP 800-82 Rev. 3). Packet analysers such as Wireshark include dissectors for Modbus and DNP3; your own scripts are for the checks those tools do not do for you, and for repeatable, reviewable automation.

9.2 DNP3 at the link layer

DNP3 is widely used in electricity and water telecontrol. Each link-layer frame starts with a 10-byte header: start bytes 05 64, a length byte, a control byte, a 2-byte destination and a 2-byte source address (little-endian), and a 2-byte CRC. User data follow in blocks of up to 16 bytes, each with its own CRC. In the control byte, bit 7 (DIR) is set for frames from the master, bit 6 (PRM) for primary (initiating) frames, and the low four bits are the link function code — for example primary 0 "reset link states", primary 4 "unconfirmed user data", secondary 11 "link status". The DNP Users Group publishes an overview; the full standard is IEEE 1815 (DNP3 overview).

The CRC is CRC-16/DNP: polynomial 0x3D65, initial value 0, input and output reflected, final XOR 0xFFFF, with the catalogue check value 0xEA82 for the ASCII string 123456789 (CRC catalogue). Reflected, the polynomial is 0xA6BC, which is the constant used in a bit-by-bit implementation.

9.3 Validate the tool first

A CRC routine that is subtly wrong still produces numbers. Before using it, run a known-answer test: compute the CRC of 123456789 and compare with 0xEA82. Then a negative test: change one bit of a valid frame and confirm the check fails. Lab 9 does both — the assert line is the known-answer test — and record the Python version, because the result of any tool is evidence only if the tool is identified.

Keep the assert that checks the catalogue value in the script, and fail the job if it does not hold after a change to the CRC function. Record the Python version next to every result that will be used as evidence. Run the negative test on a frame you corrupted yourself, so you know the checker rejects bad CRCs before you trust it on a capture.

9.4 Error detection is not authentication

A CRC is computed from public parameters; anyone who can send a frame can compute a valid CRC. It detects accidental corruption, not forgery. Lab 9 shows this directly: a frame from an unknown source address (77) has a valid CRC. Telling genuine from forged needs either cryptographic authentication (DNP3 Secure Authentication, or a protected tunnel) or context — known source addresses, known paths, expected timing. Compare Chapter 15 of the technologist book, which uses HMAC (RFC 2104) for authenticity (RFC 2104).

9.5 Worked example: checking SS-3 headers (synthetic)

The Kestrel Grid master (DNP3 address 1) polls substation SS-3's outstation (address 10) through fep-01. A technician captured six link headers.

Lab 9 (save as lab09.py, run with python3 lab09.py):

# Lab 9 - check DNP3 link-layer headers and their CRC (offline, standard library only)
# CRC-16/DNP: poly 0x3D65 (reflected 0xA6BC), init 0x0000, reflected in/out, xorout 0xFFFF.
# Synthetic frames between the Kestrel Grid master (address 1) and substation SS-3 outstation (address 10).
import struct
def crc_dnp(data: bytes) -> int:
    crc = 0
    for byte in data:
        crc ^= byte
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA6BC if crc & 1 else crc >> 1
    return crc ^ 0xFFFF
assert crc_dnp(b"123456789") == 0xEA82          # catalogue check value
def header(length, ctrl, dest, src):
    h = bytes([0x05, 0x64, length, ctrl]) + struct.pack("<HH", dest, src)
    return h + struct.pack("<H", crc_dnp(h))
capture = [
    ("master->SS-3", header(0x05, 0xC0, 10, 1).hex()),     # primary function 0: reset link states
    ("SS-3->master", header(0x05, 0x0B, 1, 10).hex()),     # secondary function 11: link status
    ("master->SS-3", header(0x11, 0xC4, 10, 1).hex()),     # primary function 4: unconfirmed user data
    ("master->SS-3", "056411c40a000100" + "ffff"),          # wrong CRC
    ("unknown->SS-3", header(0x11, 0xC4, 10, 77).hex()),   # source not in station table
    ("master->SS-3", "0565" + header(0x05, 0xC0, 10, 1).hex()[4:]),  # bad start bytes
]
KNOWN_SOURCES = {1}
for label, hx in capture:
    b = bytes.fromhex(hx)
    if b[:2] != b"\x05\x64": print(f"{label:<14} {hx}  REJECT start bytes {b[:2].hex()}"); continue
    got = struct.unpack("<H", b[8:10])[0]; want = crc_dnp(b[:8])
    length, ctrl, dest, src = b[2], b[3], *struct.unpack("<HH", b[4:8])
    if got != want: print(f"{label:<14} {hx}  REJECT crc {got:04x} != {want:04x}"); continue
    direction = "from master" if ctrl & 0x80 else "from outstation"
    note = "" if src in KNOWN_SOURCES or src == 10 else "  ALERT unknown source address"
    print(f"{label:<14} {hx}  ok len={length} ctrl=0x{ctrl:02x} ({direction}, fc={ctrl & 0x0F}) dest={dest} src={src}{note}")

Expected output (Python 3.13.5):

master->SS-3   056405c00a000100b1ac  ok len=5 ctrl=0xc0 (from master, fc=0) dest=10 src=1
SS-3->master   0564050b01000a006ded  ok len=5 ctrl=0x0b (from outstation, fc=11) dest=1 src=10
master->SS-3   056411c40a0001000615  ok len=17 ctrl=0xc4 (from master, fc=4) dest=10 src=1
master->SS-3   056411c40a000100ffff  REJECT crc ffff != 1506
unknown->SS-3  056411c40a004d0073e6  ok len=17 ctrl=0xc4 (from master, fc=4) dest=10 src=77  ALERT unknown source address
master->SS-3   056505c00a000100b1ac  REJECT start bytes 0565

Three frames are valid and expected. One has CRC ffff where 1506 was expected — corruption or a crafted frame; on a noisy link, a run of CRC failures is a link-quality finding for the telecom provider. One has wrong start bytes and is rejected before anything else is read. The most important line is the one that passes the CRC: source address 77 is not in the station table. The CRC says the bytes arrived intact; only the address check says they came from somewhere unexpected.

9.6 Practical activity

Extend Lab 9 to verify the CRC of each 16-byte user-data block that follows the header. Construct a frame with 17 bytes of user data (one full block and one 1-byte block), compute both block CRCs, and corrupt one byte in the second block to show the check catches it.

Corrupt one byte in the second block and confirm that only that block's check fails. Keep the construction steps in the lab notes so a reviewer can rebuild the same frame, and note which byte you corrupted and the expected and computed CRC of that block.

9.7 Common errors and limits

Exercises — chapter 9

EX033 — Known-answer test. Objective: validate a tool. Inputs: Lab 9's crc_dnp. Steps: (1) replace 0xA6BC with 0x3D65 and run; (2) explain the result. Expected evidence: output and explanation. Rubric: 3 = the assertion fails because a right-shifting (reflected) algorithm needs the reflected polynomial; the known-answer test caught a plausible bug before any frame was judged; 2 = observes failure without explanation; 1 = attempted; 0 = none.

EX034 — Decode the control byte. Objective: read DNP3 link fields. Inputs: control bytes 0xC9, 0x44 and 0x0B. Steps: give DIR, PRM and function code for each and name the function. Expected evidence: three rows. Rubric: 3 = 0xC9: DIR 1, PRM 1, fc 9 request link status; 0x44: DIR 0, PRM 1, fc 4 unconfirmed user data from an outstation (for example an unsolicited response); 0x0B: DIR 0, PRM 0, fc 11 link status; 2 = one wrong; 1 = bits only; 0 = none.

EX035 — Forgery thought experiment. Objective: separate integrity and authenticity. Inputs: a frame with valid CRC, source address 1 and destination 10, arriving at the outstation from an address on the MPLS network that is not fep-01. Steps: explain what the CRC shows, what the address shows and what would actually establish authenticity. Expected evidence: short explanation. Rubric: 3 = CRC shows no accidental corruption; DNP3 address 1 is self-asserted; authenticity requires Secure Authentication or a cryptographic tunnel terminating at known endpoints, plus network enforcement of the path; 2 = partial; 1 = "valid CRC so genuine"; 0 = none.

EX036 — Method selection. Objective: pick the least intrusive method. Inputs: questions: (a) which DNP3 master addresses SS-3 has seen this month; (b) whether fep-01's firewall permits only the outstation; (c) the outstation's firmware version. Steps: choose a method for each and justify. Expected evidence: three choices. Rubric: 3 = (a) passive capture or fep-01 logs analysed with a decoder; (b) configuration export plus logs, then a scoped test if needed; (c) asset record or vendor tool during an approved session with the protection engineer's agreement; 2 = one intrusive choice; 1 = scanning; 0 = none.

Chapter synthesis

Pick the least intrusive method that answers the question, and validate every tool before trusting it: known-answer tests, negative tests, recorded versions. DNP3's link-layer CRC shows the discipline in miniature — implement it from published parameters, check it against the catalogue value — and also its limit: error detection proves the bytes arrived intact, never who sent them.

Continue studying — chapter 09 of 18

← Chapter 08 · Course chapter index · Chapter 10 →

Continue to the next authored lesson.

Worked answers

EX033 — Known-answer test. With 0x3D65 in the right-shifting loop, the assertion fails (AssertionError) and the program stops before judging any frame. A bit-reflected (LSB-first) implementation must use the reflected polynomial 0xA6BC; the normal form 0x3D65 belongs to MSB-first implementations. The known-answer test turned a silent, plausible bug into an immediate failure.

EX034 — Decode the control byte. 0xC9 = 1100 1001: DIR 1 (from master), PRM 1, function 9 — request link status. 0x44 = 0100 0100: DIR 0 (from outstation), PRM 1 (primary), function 4 — unconfirmed user data, as used for example by an outstation's unsolicited response. 0x0B = 0000 1011: DIR 0, PRM 0, function 11 — link status (secondary).

EX035 — Forgery thought experiment. The CRC shows only that the bytes were not accidentally corrupted. The DNP3 source address 1 is a field the sender writes; any host can claim it. Authenticity needs cryptographic protection — DNP3 Secure Authentication, or a tunnel whose endpoints are authenticated (for example TLS terminating at fep-01 and the outstation or its gateway) — plus network enforcement that only fep-01's path reaches the outstation. The unexpected network source is itself the finding.

EX036 — Method selection. (a) Passive: decode a capture at fep-01 or its logs over the month and list source addresses. (b) Configuration export of fep-01's firewall and its logs; a single scoped connect test only if the export is ambiguous. (c) Asset record; otherwise the vendor tool in read-only mode during an approved session arranged with Kestrel Grid's protection or telecontrol engineer — never by probing the outstation.