前言
在上一篇文章中,我们通过模拟器环境实现了S7-300的启停实验。本次文章,我们将详细介绍S7comm协议的S7Comm Header和Job 和 Ack_Data机制并抓包分析其请求和相应报文。
S7Comm
S7Comm(S7 Communication)是西门子专有的协议,是西门子 S7 通讯协议簇里的一种。
S7 协议的 TCP/IP 实现依赖于面向块的 ISO 传输服务。S7 协议被封装在 TPKT 和 ISO-COTP 协议中,这使得 PDU(协议数据单元)能够通过 TCP 传送。
它用于 PLC 编程,在 PLC 之间交换数据,从 SCADA(监控和数据采集)系统访问 PLC 数据以及诊断目的。
S7Comm 以太网协议基于 OSI 模型,从 wireshark 协议分级可以看出排列。
S7Comm 数据作为 COTP 数据包的有效载荷,第一个字节总是 0×32 作为协议标识符。
S7Comm 协议包含三部分:
- Header
- Parameter
- Data
根据实现的功能不同,S7 comm 协议的结构会有所不同。
S7Comm Header
S7Comm 的头,定义了该包的类型、参数长度、数据长度等。
S7Comm Header的格式为:
- 0 (unsigned integer, 1 byte): Protocol Id,协议 ID,通常为 0×32;
- 1 (unsigned integer, 1 byte): ROSCTR,PDU type,PDU 的类型,一般有以下值:
- 0×01 - JOB(Request: job with acknowledgement):作业请求。由主设备发送的请求(例如,读/写存储器,读/写块,启动/停止设备,设置通信);
- 0×02 - ACK(acknowledgement without additional field):确认响应,没有数据的简单确认(未遇到过由 S7 300/400 设备发送得);
- 0×03 - ACK_DATA(Response: acknowledgement with additional field):确认数据响应,这个一般都是响应JOB的请求;
- 0×07 - USERDATA:原始协议的扩展,参数字段包含请求/响应 ID(用于编程/调试,读取 SZL,安全功能,时间设置,循环读取…)。
- 2~3 (unsigned integer, 2 bytes): Redundancy Identification (Reserved),冗余数据,通常为 0×0000;
- 4~5 (unsigned integer, 2 bytes): Protocol Data Unit Reference,it’s increased by request event。协议数据单元参考,通过请求事件增加;
- 6~7 (unsigned integer, 2 bytes): Parameter length,the total length (bytes) of parameter part。参数的总长度;
- 8~9 (unsigned integer, 2 bytes): Data length,数据长度。如果读取 PLC 内部数据,此处为 0×0000;对于其他功能,则为 Data 部分的数据长度;
其中最重要的字段就是 ROSCTR,它决定了后续参数的结构
在响应数据包中,还有可能存在错误信息,其错误信息结构为:
- 10 (unsigned integer, 1 bytes): Error class,错误类型:
- 11 (unsigned integer, 1 bytes): Error code,错误代码;流量包分析
下载 wireshark 官网公开的pcap包 https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=s7comm_downloading_block_db1.pcap
COTP Connection Packet
- COTP 连接请求包
- COTP 请求确认包
COTP Fuction Packet
- 数据传输包
S7Comm Header
其中最重要的字段就是 ROSCTR,它决定了后续参数的结构
在响应数据包中,还有可能存在错误信息
可见图中的错误类型就是 No error
Job 和 Ack_Data
S7Comm 中 Job(作业请求) 和 Ack_Data(确认数据响应) 中的 Parameter 项的第一个字段是 function(功能码),其类型为 Unsigned integer,大小为 1 byte,决定了其余字段的结构、消息的目的。
建立通信(Setup communication 0xF0)
建立通信在每个会话开始时被发送,然后可以交换任何其他消息。它用于协商 ACK 队列的大小和最大 PDU 长度,双方声明它们的支持值。ACK 队列的长度决定了可以同时启动而不需要确认的并行作业的数量。PDU 和队列长度字段都是大端。
当 PDU 类型为 Job 时,具体的 Parameter 结构,如下:
- 1 (Unsigned integer, 1 byte): Parameter part: Reserved byte in communication setup pdu,保留字节;
- 2 (Unsigned integer, 2 bytes): Max AmQ (parallel jobs with ack) calling;
- 3 (Unsigned integer, 2 bytes): Max AmQ (parallel jobs with ack) called;
- 4 (Unsigned integer, 2 bytes): Parameter part: Negotiate PDU length。协商 PDU 长度。
读取值(Read Var 0x04)
数据读写操作通过指定变量的存储区域,地址(偏移量)及其大小或类型来执行。
当 PDU 类型为 Job 时,item的结构如下:
- 0 (Unsigned integer, 1 byte): Variable specification,确定项目结构的主要类型,通常为 0×12,代表变量规范;
- 1 (Unsigned integer, 1 byte): Length of following address specification,本 Item 其余部分的长度;
- 2 (Unsigned integer, 1 byte): Syntax Ids of variable specification,确定寻址模式和其余项目结构的格式;
- 3(Unsigned integer, 1 byte): Transport sizes in item data,确定变量的类型和长度:
- 4~5 (Unsigned integer ,2 byte): Request data length,请求的数据长度;
- 6~7 (Unsigned integer, 2 byte): DB number,DB 模块的编号,如果访问的不是 DB 区域,此处为 0×0000;
- 8 (Unsigned integer, 1 byte):: Area,区域类型:
- 9~11(Unsigned integer, 3 byte): Address,地址。
PDU 类型为 Ack_Data 时,Data 结构如下:
- 0 (Unsigned integer, 1 byte): Return code,返回代码:
- 1 (Unsigned integer, 1 byte): Transport size,数据的传输尺寸:
- 2~3 (Unsigned integer, 2 bytes): Length,数据的长度;
- 4~4 length (?): Data,数据;
- ? (Unsigned integer, 1 byte): Fill byte,填充字节。
写入值(Write Var 0x05)
Write Var 中 Parameter 的结构跟读取值(Read Var0x04)一样,但是 Write Var 还需写入值,所以 Write Var 比 Read Var 多了一个 Data 项。
Data 的结构为:
- 0 (Unsigned integer, 1 byte): Return code,返回代码,这里是未定义,所以为 Reserved(0×00);
- 1 (unsigned integer, 1 byte): Transport size,确定变量的类型和长度:
- 2-3 (unsigned integer, 2 bytes): Length,写入值的数据长度;
- 4 (1 byte): Data,写入的值;
- 5 (unsigned integer, 1 byte): Fill byte,填充字节,如果数据的长度不足 Length 的话,则填充;
PDU 类型为 Ack_Data 时,Parameter 也只有 function、item count 两个字段。而 Data 中也只有一个 Return code 字段,其结构如下:
- 0 (Unsigned integer, 1 byte): Return code,返回代码:
下载
下载Step7 发送块数据给 PLC.在西门子设备上,程序代码和(大部分)程序数据存储在块中,这些块有自己的头和编码格式。
在西门子设备中有8种不同类型的功能块,这些块在上/下载请求中用特殊的ASCII文件名寻址。这个文件名的结构如下:
- 1 (1 byte): File identifier(ASCII),文件标识符。其有_ (Complete Module)、$ (Module header for up-loading)两种文件标识符;
- 2 (2 bytes): Block type,块类型。
- 3 (5 bytes): Block number,块编号;
- 4 (1 byte): Destination filesystem(ASCII),目标的文件系统。其有三种文件系统:
- P (Passive (copied, but not chained) module):被动文件系统
- A (Active embedded module):主动文件系统
- B (Active as well as passive module):既主既被文件系统
下载有3种不同的功能类型:
- 请求下载(Request download 0x1A)
当 PDU 类型为 Job 时,Request download 0x1A 没有 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 无意义,一般为0x00000000;
- 4 (1 byte): filename length,文件名长度;
- 5 (? bytes): filename, default is 9 byte,文件名,长度一般为9个字节;
- 1 (1 byte): File identifier(ASCII),文件标识符。其有_ (Complete Module)、$ (Module header for up-loading)两种文件标识符;
- 2 (2 bytes): Block type,块类型。
- 3 (5 bytes): Block number,块编号;
- 4 (1 byte): Destination filesystem(ASCII),目标的文件系统。其有P(Passive (copied, but not chained) module)、A (Active embedded module)、B (Active as well as passive module)三种文件系统;
- 6 (1 byte): Length part 2 in bytes,参数的第二部分长度,也就是接下来的字段长度;
- 7 (1 byte): Unknown char(ASCII);
- 8 (6 bytes): Length load memory in bytes(ASCII);
- 9 (6 bytes): Length of MC7 code in bytes(ASCII)。 PDU 类型为 Ack_Data 时,Request download 0x1A 的 Parameter 中只有一个 function。
PDU 类型为 Ack_Data 时,Request download 0x1A 的 Parameter 中只有一个 function。
- 下载块(Download block 0x1B)
下载是 Step7 发送块数据给 PLC。当 PDU 类型为 Job 时,Download block 0x1B 也没有 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 无意义,一般为0x00000000;
- 4 (1 byte): filename length,文件名长度;
- 5 (? bytes): filename, default is 9 byte,文件名,长度一般为9个字节;
- 1 (1 byte): File identifier(ASCII),文件标识符。其有_ (Complete Module)、$ (Module header for up-loading)两种文件标识符;
- 2 (2 bytes): Block type,块类型。
- 3 (5 bytes): Block number,块编号;
- 4 (1 byte): Destination filesystem(ASCII),目标的文件系统。其有P(Passive (copied, but not chained) module)、A (Active embedded module)、B (Active as well as passive module)三种文件系统;
Download block 0x1B 的 Parameter 与 Request download 0x1A 的 Parameter 的第一部分相同
那 PDU 类型为 Ack_Data 时,Download block 0x1B 有 Parameter 和 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 1 (Unsigned integer, 2 bytes): Length,数据长度;
- 2 (Unsigned integer, 2 bytes): Unknown byte(s) in blockcontrol,未知字节;
- 3 (Label,data_length-4 bytes): Data,数据;
- 下载结束(Download ended 0x1C)
当 PDU 类型为 Job 时,Download ended 0x1C 也没有 Data,其 Parameter 的结构,如下:
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 无意义,一般为 0x00000000;
- 4 (1 byte): filename length,文件名长度;
- 5 (? bytes): filename, default is 9 byte,文件名,长度一般为9个字节;
- 1 (1 byte): File identifier(ASCII),文件标识符。其有_ (Complete Module)、$ (Module header for up-loading)两种文件标识符;
- 2 (2 bytes): Block type,块类型。
- 3 (5 bytes): Block number,块编号;
- 4 (1 byte): Destination filesystem(ASCII),目标的文件系统。其有P(Passive (copied, but not chained) module)、A (Active embedded module)、B (Active as well as passive module)三种文件系统;
PDU 类型为 Ack_Data 时,Download ended 0x1C 的 Parameter 中只有一个 function。
上传
上传是 PLC 发送块数据给 Step7
在上传过程中,先是 Step7 向 PLC 发送一个开始上传的 Job,PLC 收到后则回复一个 Ack_Data,并告诉 Step7 块的长度、上传会话 ID。然后 PLC 继续上传块数据到 Step7,直到 Step7 收到所有字节。最后,Step7 发送结束上传的作业请求来关闭上传会话。
上传有3种不同的功能类型
- 开始上传(Start upload 0x1D)
当 PDU 类型为 Job 时,Start upload 0x1D 没有 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 上传的会话ID,此时为0x00000000;
- 4 (1 byte): filename length,文件名长度;
- 5 (? bytes): filename, default is 9 byte,文件名,长度一般为9个字节;
- 1 (1 byte): File identifier(ASCII),文件标识符。其有_ (Complete Module)、$ (Module header for up-loading)两种文件标识符;
- 2 (2 bytes): Block type,块类型。
- 3 (5 bytes): Block number,块编号;
- 4 (1 byte): Destination filesystem(ASCII),目标的文件系统。其有P(Passive (copied, but not chained) module)、A (Active embedded module)、B (Active as well as passive module)三种文件系统;
那 PDU 类型为 Ack_Data 时,Start upload 0x1D 的 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 上传的会话ID,告诉Step7上传会话ID;
- 4 (Unsigned integer, 1 byte): Blocklengthstring Length;
- 5 (Character string): Blocklength,块的长度;
- 上传(Upload 0x1E)
当 PDU 类型为 Job 时,Upload 0x1E 也没有 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): for all unknown bytes in blockcontrol;
- 3 (4 bytes): 上传的会话ID,告诉Step7上传会话ID;
PDU 类型为 Ack_Data 时,Upload 0x1E 有 Parameter 和 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 1 (Unsigned integer, 2 bytes): Length,数据长度;
- 2 (Unsigned integer, 2 bytes): Unknown byte(s) in blockcontrol,未知字节;
- 3 (Label,data_length-4 bytes): Data,数据;
- 上传结束(End upload 0x1F)
上传结束的过程,即为所有数据上传完成后,Step7 发送结束上传的作业请求,PLC 收到后就关闭会话,然后返回一个响应。
当 PDU 类型为 Job 时,End upload 0x1F 也没有 Data,其 Parameter 的结构,如下
- 1 (1 byte): Function Status,功能码状态;
- 2 (2 bytes): Error code,错误代码:
- 3 (4 bytes): 上传的会话ID,告诉 Step7 上传会话 ID; 那 PDU 类型为 Ack_Data 时,End upload 0x1F 的 Parameter 中只有一个 function。
程序调用服务(PI service 0x28)
程序调用是用于在 PLC 执行修改执行/内存状态的日常工作。这些命令可以用于启动或停止 PLC 控制程序、激活或删除程序块。
当 PDU 类型为 Job 时,PI service 0x28 没有 Data,只有 Parameter,那 Parameter 的结构,如下:
- 1 (7 bytes): Unknown;
- 2 (Unsigned integer, 2 bytes): Parameter block length;
- 3 (?bytes): Parameter block,参数;
- 4 (Unsigned integer, 1 byte):String length,PI service的字符串长度;
- 5 (Character string, ASCII):PI (program invocation) Service name,程序调用服务名
Parameter 包含两个主要部分:
- 服务名称
- 参数:取决于方法类型,可以将它们看作是它的参数
服务名称及其相关参数的示例:
- _INSE:激活设备上下载的块,参数是块的名称(比如:OB 1)。
- _DELE:从设备的文件系统中删除一个块,该参数也是该块的名称。
- P_PROGRAM:设置设备的运行状态(启动、停止、复位)。
- _GARB:压缩 PLC 内存。
- _MODU:将 ram 复制到 ROM,参数包含文件系统标识符(A/E/P)。
如果服务调用的参数是块的话,那么 Parameter block 的结构如下:
- 1 (1 byte): Number of block;
- 2 (1 byte): Unknown,默认为 0x00;
- 3 (? bytes): filename,文件名:
- 1 (2 bytes, ASCII): Block type,块类型。
- 2 (5 bytes, ASCII): Block number,块编号;
- 3 (1 byte, ASCII): Destination filesystem(ASCII),目标的文件系统。其有 P(Passive (copied, but not chained) module)、A (Active embedded module)、B (Active as well as passive module)三种文件系统;
PLC STOP 0x29
PLC STOP 基本上跟程序调用服务(PI service 0x28)一致,唯一的区别就是它没有 Parameter block,而它的 PI service 为 P_PROGRAM。
流量包分析
下载 wireshark 官网公开的pcap包 https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=s7comm_downloading_block_db1.pcap
建立通信(Setup communication 0xF0)
- 请求
- 响应
其协商结果为:ACK 队列的大小为 1;最大 PDU 长度为 240。
请求下载(Request download 0x1A)
- 请求
- 响应
下载块(Download block 0x1B)
- 请求
- 响应
下载结束(Download ended 0x1C)
- 请求
- 响应
程序调用服务(PI service 0x28)
- 请求
- 响应
然后再下载这个流量包 https://github.com/ITI/ICS-Security-Tools/blob/master/pcaps/s7/snap7_s300_everything.pcapng
开始上传(Start upload 0x1D)
- 请求
- 响应
上传(Upload 0x1E)
- 请求
- 响应
上传结束(End upload 0x1F)
- 请求
- 响应
然后再下载这个流量包 https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=get&target=s7comm_varservice_libnodavedemo.pcap
读取值(Read Var 0x04)
- 读值操作的作业请求
- 响应
写入值(Write Var 0x05)
- 向地址为 0×000020 的 Flags(M)写入 0×0103 的作业请求
- 向地址为 0×000020 的 Flags(M)写入 0×0103 的确认响应
总结
本文,我们分析了S7comm协议的S7Comm Header、Job 和 Ack_Data机制,并通过wireshark抓包分析其请求和相应报文。下一篇文章,我们将继续学习S7comm协议Userdata 协议拓展部分。
本文作者 r0fus0d