如何使用 Python 构建 PC 通信?
PLC(Programmable Logic Controller)即可编程逻辑控制器,可以理解为一个微型计算机,广泛应用于工业控制领域中,包括楼宇智控、精密机床、汽车电子等等。
随着物联网的兴起,越来越多的传统工业设备需要和外界通信,但很多情况下,类似PLC的微控制器经常会由于自身硬件因素而无法与外界直接互联互通。PC作为一个中介桥梁,为PLC与外界的沟通打开了一扇门。
而Python作为当前最火的语言,不仅在AI、云计算等诸多方面都能看到它的身影,在工业控制中也不能少了它。本文就来分享下如何使用Python构建PC与PLC的通信,也算展示一把Python在工控领域的风采。
Snap7简介当前市场上主流的PLC通信方式为网络通信和串行通信。网络通信这块主要协议有profinet,modbus-tcp等,串行通信主要是基于RS232/485的modbus。
本次接触到的是西门子S7系列的PLC,通信方式都为网络型的,而Snap7(http://snap7.sourceforge.net/)正是一个开源的、32/64位的、多平台的以太网通讯库:
支持多硬件体系结构(i386/x86_64、ARM/ARM64、Sun Sparc、Mips);支持多系统(Windows、Linux、BSD、Solaris);支持多语言(C/C++、Phyton、Node.js、Pascal、C#、VB)。Python对其进行了封装,具体可以参见:https://github.com/gijzelaerr/python-snap7。
开发环境搭建这里主要从Windows和Linux(Ubuntu)两个平台,说说如何搭建Python环境下的Snap7开发环境。Python的安装这里就不再赘述,环境搭建主要就是Snap7和python-snap7两个库的安装。
1、安装Snap7
Windows下,需要根据Python的结构版本(32位/64位),将下载的Snap7的发布库copy到对应的Python安装根目录下即可。
如上图所示,我的Python是32bit,所以需要将Snap7中Win32目录下的文件Copy到Python的安装根目录下,如下图所示:
Linux(Ubuntu)下安装相对简单些,按如下命令即可:
$ sudo -s
$ add-apt-repository ppa:gijzelaar/snap7
$ apt-get update
$ apt-get install libsnap71 libsnap7-dev
2、安装python-snap7
Snap7的Python库安装就简单很多了,不管是Windows还是Linux,直接pip安装即可。
$ pip install python-snap7
经过上面两步,环境就算搭建好了。通过一个连接测试代码试试,判断下环境是否搭建正常。
import snap7
client = snap7.client.Client()
client.connect('192.168.0.1', 0, 1)
client.disconnect()
如果是下图提示,则环境正常(192.168.0.1的PLC不存在)。
如果是下图提示,则环境异常(snap7库安装不正确)。
读写PLC
环境搭建正常后,在正式建立通信前PLC还需做些配置工作,主要是开发自身的读写权限。具体参照下图配置:
通过上述配置,PLC可以正常通信了。
1、python-snap7读写分析
结合python-snap7的文档API和源码分析,python-sna7重要的两个方法是read_area和write_area,通过这两个方法就能读和写PLC的对应存储地址。
def read_area(self, area, dbnumber, start, size):
"""This is the main function to read data from a PLC.
With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters.
:param dbnumber: The DB number, only used when area= S7AreaDB
:param start: offset to start writing
:param size: number of units to read
"""
assert area in snap7.snap7types.areas.values()
wordlen = snap7.snap7types.S7WLByte
type_ = snap7.snap7types.wordlen_to_ctypes[wordlen]
logger.debug("reading area: %s dbnumber: %s start: %s: amount %s: "
"wordlen: %s" % (area, dbnumber, start, size, wordlen))
data = (type_ * size)()
result = self.library.Cli_ReadArea(self.pointer, area, dbnumber, start,
size, wordlen, byref(data))
check_error(result, context="client")
return bytearray(data)
@error_wrap
def write_area(self, area, dbnumber, start, data):
"""This is the main function to write data into a PLC. It's the
complementary function of Cli_ReadArea(), the parameters and their
meanings are the same. The only difference is that the data is
transferred from the buffer pointed by pUsrData into PLC.
:param dbnumber: The DB number, only used when area= S7AreaDB
:param start: offset to start writing
:param data: a bytearray containing the payload
"""
wordlen = snap7.snap7types.S7WLByte
type_ = snap7.snap7types.wordlen_to_ctypes[wordlen]
size = len(data)
logger.debug("writing area: %s dbnumber: %s start: %s: size %s: "
"type: %s" % (area, dbnumber, start, size, type_))
cdata = (type_ * len(data)).from_buffer_copy(data)
return self.library.Cli_WriteArea(self.pointer, area, dbnumber, start,
size, wordlen, byref(cdata))
从参数可见,需要提供PLC的区域地址、起始地址、读和写的数据长度。PLC能提供如下信息:
2、PLC数据存储和地址
通过阅读PLC的手册获取到如下信息:
PLC的数据存储通过Tag的形式与存储区间关联,分为输入(I)、输出(O)、位存储(M)和数据块(DB)。程序在访问对应(I/O)tag时,是通过访问CPU的Process Image Out对相应地址进行操作的。具体对应关系如下:
到这里就能明白python-snap7中定义的areas地址是什么含义了。
areas = ADict({
'PE': 0x81, #input
'PA': 0x82, #output
'MK': 0x83, #bit memory
'DB': 0x84, #DB
'CT': 0x1C, #counters
'TM': 0x1D, #Timers
})
现在离读写PLC还差最后一步,就是起始地址如何确定呢?
从上可见对于M3.4,对应的就是M(0x83),起始地址是3,对应bit位是4。
实战
经过上面的精心准备,下面就来一波实战。通过读写PLC的M10.1、MW201来具体看看如何读写PLC。
import struct
import time
import snap7
def plc_connect(ip, rack=0, slot=1):
"""
连接初始化
:param ip:
:param rack: 通常为0
:param slot: 根据plc安装,一般为0或1
:return:
"""
client = snap7.client.Client()
client.connect(ip, rack, slot)
return client
def plc_con_close(client):
"""
连接关闭
:param client:
:return:
"""
client.disconnect()
def test_mk10_1(client):
"""
测试M10.1
:return:
"""
area = snap7.snap7types.areas.MK
dbnumber = 0
amount = 1
start = 10
print(u'初始值')
mk_data = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('!c', mk_data))
print(u'置1')
client.write_area(area, dbnumber, start, b'')
print(u'当前值')
mk_cur = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('!c', mk_cur))
def test_mk_w201(client):
"""
测试MW201,数据类型为word
:param client:
:return:
"""
area = snap7.snap7types.areas.MK
dbnumber = 0
amount = 2
start = 201
print(u'初始值')
mk_data = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('!h', mk_data))
print(u'置12')
client.write_area(area, dbnumber, start, b'')
print(u'当前值')
mk_cur = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('!h', mk_cur))
time.sleep(3)
print(u'置3')
client.write_area(area, dbnumber, start, b'')
print(u'当前值')
mk_cur = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('!h', mk_cur))
if __name__ == "__main__":
client_fd = plc_connect('192.168.0.1')
test_mk10_1(client_fd)
test_mk10_1(client_fd)
plc_con_close(client_fd)
从代码可见,MW201,根据M确定area为MK,根据W确定数据amount为2Btye,根据201确定start为201,读出来的数据根据数据长度用struct进行unpack,写数据对应strcut的pack。
这里给出PLC变量类型和大小,这样对应确定读写的amount。
最后给出一段视频,Python操作PLC来个跑马灯。
声明:本文为作者投稿,原载于个人公众号chafezhou,版权归作者所有。
“征稿啦!”
CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。
如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。
使用PLC作为payloadshellcode分发系统
翻译: shan66
预估稿费:180RMB(不服你也来投稿啊!)
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
这个周末,我一直在鼓捣Modbus,并利用汇编语言开发了一个stager,它可以从PLC的保持寄存器中下载payload。由于有大量的PLC都暴露在互联网上,我情不自禁地想到,是否可以利用它们提供的处理能力和内存来存储某些payload,以便以后(从stager)下载它们。
所以,我们不妨考虑下面的场景:
1.攻击者从互联网上寻找一个具有足够的空间来存储payload的PLC。实际上,带有几十KB内存的Modbus设备是很容易找到的。
2.攻击者将payload上传到PLC的内存。
3.攻击者用dropper感染一个主机,然后利用stager与Modbus进行“交流”,从PLC中下载并执行该stage。
采用PLC保持寄存器存储Payload的优点
由于使用了第三方PLC,所以具有很好的匿名性,跟踪的难度非常大。无需将payload上传到服务器。
由于payload存放在PLC的内存中,所以加大了取证分析的难度。此外,一旦payload被取出,其内容可以被容易地覆盖(甚至stager自身就能做到这一点)。
此外,我认为Modbus Stager在某些ICS环境中也是非常有用的,因为这些环境下Modbus之外的协议会引起人们的警觉,并且WinHTTP / WinInet stager也不是最适用的。所以,在这种情况下,你只需要一个Modbus处理程序或者只是使用一个仿真器,在stager连接它时,由其提供stage即可。此外我们还发现,许多网络上都有可以远程管理的Modbus设备,所以它们也是这种stager的用武之地。
重要说明:请不要对任何第三方PLC执行这些操作。PLC寄存器上的任何写操作都可能毁坏原来的过程控制策略。
活跃在互联网上采用Modbus协议的PLC数量
为了弄清楚暴露在互联网中、使用Modbus协议的PLC的数量,我使用Censys API写了一个小脚本。如果你的网卡性能不错的话,你可以利用masscan或Zmap等工具来扫描互联网,寻找在502端口上运行Modbus协议的设备。
从以下输出可以看出,至少有5500个PLC可供利用。
在这些IP中,许多只是些蜜罐,这很容易看出来;例如,Conpot以及托管在云服务中的其他服务。就本文来说,即使蜜罐也无所谓,只要它们的内存足够大就行了。
如何将Payload上传至PLC保持寄存器
好了,为了将payload上传到PLC中,我编写了一个名为plcInjectPayload.py的python脚本。根据加载的控制策略的不同,对PLC可用内存大小的要求也有所变化,因此该脚本首先检查它们是否有足够的内存空间来存放相应的payload。为了检测内存的大小,可以发送操作ID为03(读取保持寄存器)的Modbus请求,尝试从某个地址读取特定记录(每个记录长度为16比特)。如果收到一个0x83异常,那么说明这个PLC对于我们来说是无法使用的。
要上传payload,请使用-upload选项,具体如下所示。该选项允许使用参数-addr规定起始地址,也就是说,从这个保持寄存器编号(如果未指定,则为地址0)开始加载payload。
如果payload的字节数为奇数,就需要用“0x90”来进行填充,以避免在读取时出现一些问题。在前面的示例中,大小为1536字节; 为了检查加载操作是否成功,我们可以利用选项-download从地址0处下载同样数量的字节。
很明显,该脚本不仅可以上传payload,实际上还可以上传任何类型的文件。所以,我们觉得这是一个泄露和共享信息的有趣方法。设想一下,有谁会怀疑某个公共PLC的保存寄存器会存有.docx或.zip文件呢?
需要格外注意的是,存放payload的记录可能会被PLC所改变。由于我们不清楚PLC I / O及其过程控制策略,所以需要寻找一个通常不会被修改到的内存范围。为此,我们可以将payload加载到某个范围,然后在一段时间内,payload经多次检查未发现任何变化的话,这就是我们要找的内存区域。为了达到这个目的,我们可以借助于plcInjectPayload.py以及另外几个bash指令即可。
在受控主机中读取PLC中存储的Payload
payload上传到PLC之后,还必须从受害者的计算机中读取它。为此,我建立了一个基于Modbus协议的stager;它的大小还不到500字节(我会设法让它变得更小)。其中,它的reverse_tcp和block_api代码取自Metasploit(https://github.com/rapid7/metasploit-framework/tree/master/external/source/shellcode/windows/x86/src/block)。下图展示的是block_recv_modbus.asm的asm代码,它的一部分职责是通过Modbus协议获取payload。因此,这段代码需要通过Modbus协议与PLC通信,以下相应的payload。这里的代码会利用前4个字节来了解该stage大小,并通过VirtualAlloc分配必要的内存。然后,通过不断发送“read holding”请求(功能代码03)来获取payload。根据协议规定,对于每个读请求,PLC最多可以返回250个字节(125个保持寄存器),因此,stager可以以它为单位,逐步下载payload。
实例解析
下面我们来看一个实际的例子。最近,我在www.exploit-db.com网站上发现一个用于Windows系统的键盘记录shellcode,大小只有600字节;虽然它的尺寸很小,但是对于一个只有几个MODBUS请求(记住,每个请求的最大字节数为250字节)的POC来说已经足够了。shellcode在执行后,会把按键敲击动作写入到用户的%TEMP%目录下的“log.bin”文件中。
因此,我们首先把该payload放到一个二进制文件中,并在它的前面放上其长度,这里是以小端字节表示的长度(4字节)。
现在,让我们从地址0开始将其上传到PLC:
这个stager一旦运行,就会通过3个请求下载该payload:250 + 250 + 102 = 602字节。下图详细描述了Modbus通信过程。
下图展示了Wireshark对上述通信过程的跟踪情况。进程监视器窗口表明,该stage在成功运行(检查log.bin文件就能看到保存的击键)
我已经通过Modbus仿真器和实际PLC对这个代码进行了验证,结果一切正常,但是如前所述,我认为该shellcode还可以进一步优化。为了进行第一个测试,我在python(plcModbusHandler.py)中创建了一个Modbus处理程序,用来把该payload发送给stager。
我正在设法把这个处理程序移植到Metasploit。更多详情,请观看下面的视频。
演示视频
视频加载中...
相关问答
学python对plc编程有用吗?
没有用,PLC可编程控制器,用于工业控制,属于硬件控制语言,控制数字信号,比较底层,编程也比较简单,十分容易学习。python是一种集成强大的三方库的应用级编...
python能取代plc么?
不能。PLC可编程控制器,用于工业控制,属于硬件控制语言,控制数字信号,比较底层,编程也比较简单,十分容易学习。python是一种集成强大的三方库的应用级编...
plc和python区别?
完全不同的两个编程语言,两者之后从事的行业也是风马牛不相及。python是一门前后端编程语言,主要用于全栈开发,数据分析,数据开发等等。plc基于强大的储存器...
plc十六进制怎么转ascii码?
要将PLC的十六进制转换为ASCII码,可以使用编程语言实现。首先,将PLC的十六进制数值转换为十进制数值。然后,将该十进制数值转换为对应的ASCII字符。具体转换方...
会python后学自动化难吗?
您好,学会Python后学习自动化并不难,但需要一些时间和努力。自动化需要掌握一定的编程技能、软件开发和工程方面的知识。学习自动化需要掌握以下几个方面:1...
高数难还是编程难?
当然,对于绝大多数人来说,高数仅仅是专业基础课,最起码得懂微积分的思想和一般计算方法,这对以后的绝大多数工程应用就足够了。而编程不同,编程语言本质上...
华为matebookd14电脑适合编程吗?
华为matebookd14电脑适合编程的啊。信息化时代。计算机首先带来的改变是信息化时代,信息化时代的重要特点就是传统资源的数字化,利用计算机强大的数字管理能力...
step7的编程方式?
Step7可以使用不同的编程方式实现,具体取决于您使用的编程语言和环境。以下是几种可能的编程方式:1.命令行编程:在命令行界面中编写和运行代码。可以使用文...
大学学的是自动化,现在有plc三级证书,可以从事什么工作?
你可以从事的工作太多了,说都说不完,主要是不知你可以做多久。plc有等级证好像在一个技校听过,但我可以告诉你这证屁用都没有,所有单位都不会看你这个证,外...如...
想做一套自动化设备,软件方面怎么设计编程?
自动化设备最简单的就是采用PLC+HMI触摸屏组成控制核心,这条路是工控行业入门级方案,编程都是利用设备厂商的专用软件编程的,一般都提供了相当丰富的实例来参...