Making a Script to Communicate under Modbus Protocol
Now that the semester is over, I would like to talk about one part of one of my assignments: making a script to communicate with devices through Modbus.
During the second semester of my senior year I took the module “Protection of Critical Infrastructures”, which, as the name suggests, revolves around the different legislation concerning industries, sectors, and resources considered critical for the regular functioning of society, as well as common threats, APTs, and methods aimed towards pentesting in industrial systems.
Each country defines what is and what is not a critical infrastructure; however, some examples of key CIs (I will now abbreviate them as such) are the power grid, water plants, nuclear power plants, transport networks, etc. In many of these industries, the technology used does not just communicate over regular protocols like HTTPS (for example, because it is too complex for the device to handle). Think of a lever in an assembly line: how do we communicate with the device that hosts the lever so that it switches ON/OFF? And how do we check its current state, in the first place? Or how do we check the current temperature of the device that hosts the lever to make sure there is no risk of overheat? In many cases, the Modbus protocol is used to establish this communication.
The second assignment for this university module I mentioned had the goal of getting familiar with Modbus through different exercises, from analysing and finding flaws in Modbus traffic generated by Metasploit modules, to making a script using the Python library “pwntools” in order to communicate with an emulated OT environment. To achieve the latter, I would have to get familiar with and handcraft packets to send according to the Modbus Protocol Standard, as well as be careful with the environment in order not to cause any major disruptions that would require a restarting of the system. Since Modbus was the main goal, the script was going to focus on monitoring coils and holding registers.
The Environment
The environment was held in a container where a simple interface allowed to see the current state of two devices that communicated using Modbus. Limited and incomplete information of the environment was available to us (and understandably so, because we would have to do reconnaisance in order to progress), like only knowing the device ID.
With this little information, we would have to first find or develop ways to enumerate information, test behaviour, analyse traffic, etc.
Two scenarios were given to us:
- Control the ventilation system of a room with multiple air entrances.
- Monitor the filling and refilling of a glass (it may sound simplistic to think of a glass, but substitute “glass” for “water dam” and the implications become a biiiiiiiiiiiiiit more noticeable).
Scenario number 1 was not too complicated, and it allowed me to develop my own functions to read from or write a value to a coil and reading many contiguous ones or changing the values of all of them (which proved useful in the next case).
modbus_manager probing the coils within a specific range in the classroom after having turned one of them on.
Scenario number 2, however, was hell let loose. We were basically given nothing. No device ID, no error replies from the device if the requests were not processed well or were targeted to the wrong area, no mapping of the memory addresses… nothing. Moreover, this section needed careful enumeration and reconnaisance activities because, if by any chance the glass overflowed by too much, it would break and the whole system would have to be restarted (a big no-no). Therefore, I developed functions that would test the behaviour of different memory addresses and began mapping out the layout and trying to find the relevant coils, holding registers, etc. This way, several findings came to light, such as that the holding register that held the current liquid level was ID 27, or that, when overflown, it would change its value to 1337, which also happened to be the ID for the engine’s coil, and a close value, ID 1341, was the ID for the lever.
Finally, with all the information gathered, I wrote a function that would closely monitor the status of the glass in order to fill and refill only when certain thresholds were reached and while gracefully ending any task shall the monitorization abruptly stop (thank you, lovely “finally” blocks in Python).
The glass overflowing and breaking apart. When a threshold past 100 was reached, it would break and holding register 27 would be set to 1337.
modbus_manager waiting for the glass to be empty again.
The Final Tool: modbus_manager.py
One of the first things I thought with this assignment was: “Okay, I want to make one of those cool CLI tools I use many times on Linux”, so I would need a library to parse arguments for me.
Some time before beginning to work on this assignment, I had been checking out my Test Harness software I had developed in my Software Quality Assurance module in China at the UNNC. One of the things I did during that assignment was accidentally reinventing the wheel(!) and creating a very rudimentary flags system for a CLI-like part of the test harness. Needless to say, it was quite rigid and not easy to expand. Thus, this time I decided not to reinvent the wheel and, instead, leverage already existing Python libraries, like “argparse”, specifically designed for this purpose.
This code required lots of testing (in my case, especially while managing the bytes to send and to receive) and reading the documentation to understand the Modbus protocol, but once I had the basic functions ready (read, write, etc.), it became as smooth as butter to make larger functions that built on top of the already existing ones (oh, reusability! =D ).
Closing Words
Undoubtedly, there is plenty of room for improvement. For instance, I would like to research for ways to locate into a different file all the ArgumentParser configuration and subparsers, improve the overall logging system using ContextManagers, refactor the code to use explicit arguments or python dataclasses instead of SimpleNamespaces, or make the project more object-oriented and separate the functions into methods in classes for Coil, HoldingRegister, Enumeration, etc. Nonetheless, the purpose of this task was getting familiar with the Modbus Protocol, its elements, exceptions, etc. and the pwntools library, and I believe I did a nice job achieving these basics.
You can check my full project here (很棒, 相信我😎), and I will try to upload soon a translated version of my report too.
Regards,
Saul FG