By RUGERO Tesla (@404Saint).
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.
Instead of forcing a massive architectural breakdown against a ticking clock, I decided to do something more chaotic: dogfood my own open-source security tool.
A while back, I released MEA (Modbus Exposure Analyzer), 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.
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.
What followed was a glorious, comedic masterclass in Linux infrastructure debugging, library dependency hell, and the ultimate facepalm moment. Here is exactly what happened.
The Setup: Industrial Lab vs. Passive Analyzer
The target environment is simple but effective:
-
OpenPLC running inside a Docker container, exposing Modbus TCP on port
502. -
Fuxa HMI containerized on port
1881, mapping tags to the PLC. - MEA, pulling register samples over multiple cycles to evaluate entropy and behavior.
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.
Phase 1: Entering Troubleshooting Hell
Before I could even scan the target, the homelab decided to humble me.
1. The Firewalld Zone Collision
I fired up the terminal, checked my docker instances, and was immediately greeted with a socket connection error. The Docker daemon refused to start.
Running manual daemon debugging (sudo dockerd) exposed the culprit:
failed to start daemon: Error initializing network controller: error creating default "bridge" network: ZONE_CONFLICT: 'docker0' already bound to 'trusted'
On Arch/EndeavourOS, firewalld had aggressively snatched the docker0 interface and dropped it into the trusted zone, blocking the Docker engine from binding its default bridge network.
The Fix: Strip it out, reload the firewall, and restart the daemon:
sudo firewall-cmd --permanent --zone=trusted --remove-interface=docker0
sudo firewall-cmd --reload
sudo systemctl start docker
2. Pymodbus Dependency Hell
With Docker up, openplc_target and fuxa_hmi were finally showing as Up. I pulled down a fresh clone of MEA, ran python3 mea.py, entered 127.0.0.1, and... CRASH.
Connection unexpectedly closed 0.000 seconds into read of unbounded read bytes
This led down a rabbit hole of modern Python package refactoring. Pymodbus has been aggressively rewriting its API syntax across minor version releases:
- In older versions, pointing to a specific Modbus Slave/Unit ID required the argument
unit=1. - Then it shifted to
slave=1. - In the latest v3.11+/v4.0 iterations, it shifted yet again to
device_id=1.
Furthermore, passing unmapped positional arguments to modern Pymodbus clients causes immediate runtime type errors. The parameters must be explicitly keyworded. The internal library logic was solid, but it was crashing against local database size configurations.
3. The Ultimate Facepalm
After stabilizing the library parameters, the connection was still getting violently dropped (ConnectionResetError: [Errno 104] Connection reset by peer).
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?
I opened up the browser, loaded the OpenPLC Web UI at http://127.0.0.1:8080, and saw it:
The PLC runtime program wasn't even running. 💀
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.
I clicked "Start PLC", flipped back to the terminal, and finally ran the code.
Phase 2: The Telemetry Data
With the runtime program finally processing logic, MEA executed flawlessly across all 5 collection windows, gathering 50 holding registers per sample.
Here is the exact raw JSON output generated by the scan:
{
"target": "127.0.0.1",
"ip_info": {
"ip": "127.0.0.1",
"type": "private",
"hostname": "localhost",
"org": null
},
"exposure": {
"exposure_level": "Low",
"reasons": [
"Private network"
]
},
"behavior": {
"classification": "Static Device",
"confidence": "High",
"entropy": {
"entropy": 0.0,
"unique_values": 1
},
"change_rate": 0.0
},
"risk": {
"overall_risk": "Medium",
"risk_score": 2,
"reasoning": [
"No register changes detected"
]
}
}
Phase 3: The Verdict
The tool worked exactly as intended, and it called me out completely.
-
Zero Entropy (
entropy: 0.0): MEA calculated a mathematical entropy score of absolute zero. Over multiple read cycles, every single holding register returned identical, unmoving bits. - High Confidence Classification: Because there was no data fluctuation or real-world sensor drift, the tool flagged the asset as a Static Device with high confidence.
- The Simulation Fingerprint: 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.
Lessons Learned
Building security tools is one thing; testing them against your own infrastructure is where the reality check happens.
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.
MEA Source Code: github.com/404saint/mea
Top comments (0)