啥都没有,就当你啥都没看见吧
前段时间一时兴起,去做了几道HTB的硬件题,其中好几题要我和设备进行”交互“的题目让我眼前一亮:欸,为什么不自己搞一个玩玩呢?
可惜由于个人的水平限制,我只对工控协议中的Modbus协议进行过细致的了解,所以就只做了Modbus TCP的交互靶机…
Scene I:Modbus服务器,启动!
我不可能实际搞一个真的Modbus设备过来是吧,咱也没这个实力,所以就简单找了一个叫PyModbus
的库去进行模拟:Welcome to PyModbus’s documentation! — PyModbus 4.0.0dev4 documentation
好消息是这个库看起来功能挺全的,坏消息是…从官方文档到官方样例都非常的…混乱…看教学视频和论坛的问答并没有让我对这个库有进一步的认识,因此我整个人是昏的,尤其是这几天我还感冒了,只能说ccb太坏了(
最后实在是没办法了,所以转头去问了D老师(DeepSeek),它也是给我抛了一个看起来很不错的代码:
1 | from pymodbus.server import StartSerialServer, StartTcpServer |
看起来很美好吧?但是实际运行就是一个又一个的Bug:这里多参数了,那里又类型不对,这个类还已经找不到了…总之还是一片混乱,但是相比官方文档已经好上不知道多少了,感觉好一些Python库的官方文档就是各种混乱,不敢想象前人是怎么吃得下官方文档这坨的(以至于我觉得看源码实现都比看文档好上不知道多少倍)
稍微提一些踩到的坑:(使用的是截至本文编写日期pyModbus库的最新稳定版3.9.2)
- 设备ID不是通过
ModbusSlaveContext
的unit
参数进行传参的,此时这个类已经没有这个参数了,设备ID是在第24行的字典进行传入的,其ID为字典的key - 个人学艺不精:Modbus设备ID只能是1~255,不能是2字节
- 启动TCP服务器的时候,使用的Framer不是代码里的
pymodbus.transaction.ModbusSocketFramer
(没记错的话好像位置也不在这里),而应该是pymodbus.FramerType.SOCKET
- 解析读取寄存器的返回数据的时候,如果使用
decoder.decode_16bit_uint()
,会将这个直接解析成UTF-8,所以最好还是使用decoder.decode_string(size=string_size)
好一些
我不清楚有没有更多的了,总而言之最后的成品如下:
1 | from pymodbus.server import StartSerialServer, StartTcpServer |
后面的sleep是为了让我用mbpoll进行实际的查看,看看是不是真的能直接通过Modbus查看对应地址的数据,最后确认是OK的
只能说还得是D老师,这些注释一下就解决了我70%的问题,我现在也简单理解了这个库的使用了
OK,临时小测,代码里面的
ModbusSlaveContext
中有4个参数:di
,ir
,co
,hr
,请问它们分别代表什么?各位大可去搜官方文档,反正是依托细碎,我最后是看源码注释才搞懂其实就是
Discrete
,Input Registers
,Coils
,Holding Registers
四种数据类型,非常傻逼有没有
Scene II:服务器数据的预处理
既然我们已经基本搞定了Modbus这一部分了,那么我们就可以开始进行一个题的出了,那么要不从一个最简单的题目来吧:让做题者直接和Modbus服务器进行通信?
那么如果是这样,我们的要求也就能分成这两部分了:
- 启动Modbus服务器并持续开放监听,同时其端口要暴露给Docker
- 启动服务器后创建一个Client进行数据的预处理,比如将flag塞进特定地址的寄存器中
看起来很简单,但是问题就在于Modbus服务器的StartTcpServer
会进行堵塞,因此两个任务不能在一个脚本内同时执行,如果使用线程的话可能会导致Python脚本执行完毕后线程也被关闭,就像前面的代码一样
上面这一段我瞎说的,我不是很会线程这方面的知识,非常欢迎大伙指出问题并纠正
除此之外,我个人常使用的是xinetd进行多线程管理,但是xinetd是连接的时候才执行,因此不是很符合预处理的情况,因此最后决定是抛弃了xinetd,将server和预处理的client分开,其中server在后台持续运行,client执行后就直接关闭,代码如下:
1 | # server.py |
1 | # Dockerfile |
1 |
|
肯定是写的不够好的,但是能动就行,还要啥自行车啊
Scene III:交互拓展和端口转发
目前我们只是进行了一些简单的数据的预处理,但是假如我们设置一些更复杂的操作呢?比如说配合梯形图进行操作?那我们需要一个验证端吧?但是因为CTFd好像只能用FRP穿透1个端口,因此最后是考虑将验证端和预处理放在同一个脚本中,不过到时候可能还得给一个说明书之类的东西
为了保证验证端输出的数据不会影响到正常的Modbus通信(比如做题者直接使用mbpoll和靶机进行通信),因此靶机设置的是不会进行引导式输出的,你只需要往里面传入数据,然后验证端会自动判断你的输入是Modbus数据还是检验设备寄存器/线圈状态的请求,我这里是设置了一个status
的命令,你如果只传入这个字符串就会返回当前的设备状态,而如果符合条件则会返回flag
当然,如果是Modbus数据,验证端就会将数据转发给服务器,并将响应数据再转发回请求侧,至于分辨的话就很简单的用了长度+特定位置的值进行判断,一个简单的样例代码如下(当然大部分是AI搓的就是了):
1 | import socket |
Scene IV:开始瞎78出题!
现在该有的基础设施都已经OK了,接下来就是自己瞎设计PLC梯形图然后搓代码了!(实际上出这种题最复杂的点就是设计梯形图和根据梯形图完成逻辑实现,只能说当时还是太天真了)
这里献个丑,放一下本人给学校内部靶场供的题目(此处仅提供client.py
代码,其它附件如有需要请自行联系我,不过质量肯定不高所以我觉得就没必要联系我了www)
1 | import socket |
为了防止泄题,本人对代码的部分数据进行了改动,不过基础逻辑大致相同,应该还是能正常运行的
至于Repo我就不放了,现在想想也没啥大不了的是吧,各位感兴趣的可以根据自己的想法去搓,我个人很多东西没有考虑进去,所以也只是抛砖引玉
- 本文作者: 9C±Void
- 本文链接: https://cauliweak9.github.io/2025/05/03/modbus-docker/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!