0x00 简介


本文主要是从工业控制网络必备的组件 PLC (可编程控制器)出发,阐明了一种新型后门的实现。本文主要内容是来自 BLACK HAT 2015 上柏林自由大学 scadacs 团队发表的演讲,这是他们的paper原文Internet-facing PLCs – A New Back Orifice。我会滤掉他们论文凑字数的部分,并在他们给出的核心思路上增加一些实现方面的具体技巧和资料。

不了解工控安全的哥们儿可以先去这篇文章上补一下基础知识 工控安全入门分析

下面几个部分都是一些背景和基础知识,如果已经对plc工控安全非常了解,可以直接跳到攻击描述(0x03)部分。

0x01 引言


本文中,我们研究攻击者如何通过公网plc访问到深层工业网络。

我们采取的方法是将plc变成网关(本文采用西门子系列plc相关技术和特性),这种方法在缺乏适当权限认证手段的plc上是可行的。经验丰富的攻击者拥有某plc的访问权限时,可以往上面上传或者下载代码,只要代码是由MC7字节码组成,这是plc的原生代码形式。我们研究了运行时环境中的plc,并发现可以通过上传mc7代码来实现很多网络服务。特别是,我们实现了

  • 一个针对西门子plc的 SNMP 扫描器
  • 一个功能上完全成熟的,为西门子plc编写的,SOCKS 代理

并且他们的实现完全只依靠编译为MC7字节码的STL语言代码。我们的扫描器和代理可以部署在plc中,并且不会中断plc中原有程序的运行,这可以使运维很难意识到plc已被感染。为了说明和分析深层工业网络入侵,我们开发了一个概念性证明工具,PLCinject(附上github项目地址: SCADACS/PLCinject)。根据我们的概念性证明,xxxxxxxx(这段太tmd复杂,我实在没法准确翻译,主要意思就是讲运行在plc上的恶意软件会使其原有代码扩展增加,如果我们定时观测原有代码和感染恶意代码后的程序,在统计学上这两者的运行效果有明显的差异,然而其对生产过程的影响微乎其微,除非运营者主动监控从PLC中发出的恶意访问的流量,否则很难在生产过程中发现)。此外,攻击者可以利用我们的方法,通过工业控制网络来攻击企业的业务网络。这意味着网络管理必须警惕从业务网络正面和背面发起的双向攻击。

我会在文章最后补充一下针对施耐德plc的编码。

0x02 工业控制系统介绍


自动化系统结构

上图展现了典型的使用自动化系统的公司结构。工业控制系统由这么几层构成。在顶部是企业资源规划(ERP)系统,其保存着当前可用资源和生产能力的相关数据。制造执行系统(MES)能够管理多个工厂或平台,并且从ERP系统接受任务。在MES下的系统位于工厂内部,监督、控制和数据采集(SCADA)系统控制生产线。他们提供关于目前生产状态的数据,并且他们提供干预手段。存储着有关生产过程的逻辑的设备称为可编程逻辑控制器(PLC)。人机交互界面(HMI)显示当前的进度,并且允许运营者与生产过程相互作用。

本文将着眼于针对 PLC 的攻击。

PLC 原本仅仅是为自动化控制而开发,在其开发之初,其应用场景是极其封闭的,几乎不能与工业内网外的任何第三方设备有所接触,但是近几年互联网的迅猛发展,和物联网、智能硬件的出现,开始逐渐有工业 PLC 暴露在公网之中,大家可以去seebug和shodan上搜索schneider或者siemens等厂商型号来发现公网上的plc设备。尽管如此,目前PLC的安全性是十分十分差的。首先来说,plc的固件迭代更新缓慢,虽然厂商可能进行维护和更新,但是给工业控制网络中的正在运行的线上plc更新固件,代价是异常巨大的,一次关机可能就是整个工厂的停止运行。其次,目前的plc已经有了一些比较低级的访问控制手段,但是很少有人会主动开启,因为它会降低plc的运行效率和稳定性。 因此,一般来说,如果某个plc面向公网开放,我们可以向其加载任意代码。

除了在权限控制上的严重问题,攻击者有可能利用plc作为一个进入生产网络甚至公司内网的网关。在本文中,我们分析和讨论这一威胁载体,并且,我们将证明,这种利用方式是真实可行的。出于演示的目的,我们开发一个运行在plc上的端口扫描器和一个socks代理。这个扫描器和代理使用plc的原生编程语言Statement List(STL)编写。

PLC

硬件

PLC由一个 CPU (一般带有通讯模块,如工业以太网、modbus、profinet等等,和一些服务的接口,如ftp、web、telnet等等),和其外部附加的数字量和模拟量输入输出模块共同组成(有时外部还会附加专用的通讯模块)。本文使用西门子 S7-314C-2 PN/DP

执行环境

这部分如果学过计算机组成原理会比较容易看懂,这部分主要讲plc的代码执行流程,跟后面我们的攻击方式的隐蔽性可行性和代理编写时的通信稳定性密切相关。

西门子PLC运行着实时操作系统,他初始化周期性时间监视。随后操作系统周期性执行四个步骤,如下图:

周期性执行步骤

在第一步中,CPU复制过程镜像的输出值来输出模块的状态。第二步,CPU读取输入模块的状态,并且更新过程映像的输入值。第三步,用户程序在时间片中执行1毫秒的持续时间。每个时间片被分割成三个部分,依次执行:操作系统,用户程序和通信。时间片的个数取决于当前的用户程序。默认情况下,时间应该不长于150毫秒,工程师可以配置不同的值。如果规定的时间用尽,中断例程被调用,在通常情况下CPU返回到周期的开始状态,并重新开始循环时间监视。

软件

我们使用 STEP 7 为plc进行编程,我使用的版本是v5.5。

参考这篇文章进行安装 step7 v5.5 cn 软件下载、仿真器安装、授权

注意,仿真器是需要额外安装的,在上文中有。

工程师可以使用梯形图(LAD),功能块图(FBD),结构化控制语言(SCL)和语句表(STL)来为PLC编程。与基于文本的SCL和类似汇编的STL,LAD和FBD语言是图形化的。PLC程序被分成组织块(OB),功能(FC),功能块(FB),数据块(DB),系统功能(SFC),系统功能块(SFB)和系统数据块(SDB)这几个单元。OB,FC和FB包含着实际的代码,而DB存储着数据结构,SDB存储PLC的当前配置。带有前缀M的内存地址被用于内部数据存储寻址。

编程

一个PLC程序至少由一个组织块(称为OB 1)组成,这就相当于C程序中的main函数。它将由操作系统调用。存在更多的用于特定用途的组织块,比如,OB 100。这个块在PLC启动时被调用一次,并通常用于初始化系统。

关于各种编程语言的语法,再次不再赘述,请自行查阅相关资料。

网络协议

这一部分尽管原文中针对通讯过程做了较为详细的阐述,但是我不会做太多解释,因为本文主要侧重恶意代码的编写。

但是还是要多说一句,这些嵌入式设备通常都是使用裁剪过的vxworks等系统,我个人认为,目前工业控制系统的渗透攻击和漏洞挖掘,在固件分析还没有特别深入的现状下,针对工控网络的通讯协议进行攻击是最为高效的手段。我大体翻了下wooyun上的工控漏洞,其实还是web渗透那一套,其实对于工业网络,有时候可用性甚至比保密性更加重要(比如伊朗核设施爆炸)。

西门子plc使用其自身的S7Comm协议来传输块。这是一种基于TCP/IP和基于TCP的ISO传输服务的远程过程调用(RPC)协议。包封装如下图:

包封装

协议提供了以下功能:

  • 系统状态表请求
  • 列出可用的块
  • 读写数据
  • 块信息请求
  • 上传下载块
  • 传输块到文件系统
  • 启动,关闭,内存初始化
  • 调试

被传输的块被结构化,其由头部,数据部分和尾部构成。

详细的传输过程请查看原文。这里仅给出一些已知字节的结构:

字节结构

尾部包含用于调用功能的参数信息。并不是所有头部和尾部的字节都被我们所知晓,但是我们已经确定了用来理解其内容的必要区域。

这部分主要是为了解释进行恶意代码注入的方式,其实完全可以使用step 7 进行代码注入,甚至可以说该协议的所有操作基本都可以使用 step 7 完成,但是如果我们弄懂了协议的结构内容和作用,就可以编写利用脚本,自动化攻击。

0x03 攻击描述


西门子plc提供了一个系统库,该库包含了可以建立任意TCP/UDP连接的功能。攻击者可以使用完整的TCP/UDP支持来扫描公网plc背后的本地生产网络。 其实根据我的了解,应该只有profinet上的通讯可以完成完整的TCP/UDP请求,这就要求我们使用的西门子plc的型号中,必须带有 PN,比如原文中使用的 S7-314C-2 PN/DP,除此之外还有s7-319-3 PN/DP等等。

思路概述

我们首先下载plc的OB1块,之后添加一条CALL指令调用任意一个我们可控制的函数,在我们给出的样例中,该函数叫做FC 666。之后,patche后的OB1,也就是FC 666和其他块(这里可能包含很多的块,比如我们自己编写的FC块,背景数据块,共享数据块等等),将被上传到PLC。下图展示了代码注入过程:

注入过程

在下个执行周期时,新上传的包含攻击代码的程序就会被执行,并且不会造成任何服务中断(这里我认为并不一定,在RUN模式下还是会有中断发生,在RUN-P模式下就会自动在下一个周期执行而不会发生中断)。这个过程使得攻击者能够在plc上运行任意恶意代码。我们随这篇文章发布了一款名叫PLCinject的工具,他可以自动化完成这个过程。有了这种技术,攻击者可以执行如下图所示的攻击流程:

攻击流程

在第一步中,攻击者注入一个SNMP扫描器,它会与plc上的正常代码一同运行。在完成一个针对本地网络的完整SNMP扫描(第二步)之后,攻击者可以从plc中下载扫描结果(第三步)。攻击者现在拥有了公网plc背后内网的一张缩略图。之后他溢出SNMP扫描器并注入一个socks代理(第四步)。这使得攻击者可以通过充当代理的plc到达本地生产网络的所有plc。在之后的两节中我们将阐明SNMP扫描器和SOCKS代理的实现。我们不会详细解释每个操作和系统调用的细节。对于这些的详细描述我们参考了S7-300 Instruction list S7-300 CPUs and ET 200 CPUsSiemens. (2006) System Software for S7-300/400 System and Standard Functions Volume 1/2

本文用到的几个系统调用为: SFC 51 “RDSYSST”, FB 65
“TCON”, FB 63 “TSEND” ,FB 64 “TRCV”, FB 67 “TU
SEND”, FB 68 “TURCV”, UDT 65 “TCON_PAR”.

请在上面给出的那篇Siemens. (2006) System Software for S7-300/400 System and Standard Functions Volume 1/2查找其参数和输入输出。

SNMP SCANNER

西门子plc不能作为一个TCP端口扫描器来使用,因为TCP连接函数TCON直到该函数成功建立连接之前,都无法被终止。此外,最多只能在西门子S7-300并发地运行8个TCP连接。因此,该PLC只能在不会发生8个连接同时失败的情况下作为TCP扫描器(如果8个连接同时失败,那么这8个连接函数就无法断开,那么扫描就无法继续进行了)。此限制并不适用于无状态的UDP连接。这就是我们要使用基于UDP的简单网络管理协议(SNMP)的原因。SNMP v1.0 在 RFC 1157[23]中被定义,并且它是为监视和控制网络设备而开发的。大量网络设备和大部分西门子 Simatic PLC 有默认支持SNMP。西门子plc在开启SNMP功能的情况下是非常活跃的。通过使用OID 1.3.6.1.2.1.1.1读取SNMP系统基本信息(sysDesc)对象,西门子plc将发送其产品类型,产品型号,硬件和固件版本,形如以下SNMP响应:

#!bash
Siemens, SIMATIC S7, CPU314C-2 PN/DP, 6ES7 314-6EH04-0AB0 , HW: 4, FW: V3.3.10.

该系统描述可以用来在漏洞和exp库中匹配已发现的plc。plc的固件不会经常打补丁。主要有两种原因:一方面,plc的固件升级将中断生产过程,这将造成亏损;另一方面,plc的固件补丁能够导致某些产品质量问题,这对客户来说是不能容忍的。这就是为什么找到一个拥有已知漏洞的西门子plc设备的可能性非常之高。该SNMP扫描器可以被分解为以下步骤:

  • 获得本地IP和子网
  • 计算子网的IP段
  • 建立UDP连接
  • 发送SNMP请求
  • 接受SNMP请求
  • 将响应存储到一个数据块中
  • 停止扫描并且关闭UDP连接

plc编程与使用C语言在X86系统上正常编程完全不同,如果学过verilog这种针对硬件的编程语言的话就比较容易理解。每个PLC程序周期性执行,所以他需要在每步之后将程序状态存储到状态变量中。在此我们只解释SNMP扫描步骤1到3。 下图显示第一步的一段代码片段,调用RDSYSST函数。

RDSYSST

RDSYSST函数读取内部系统状态表(SSL)来获得plc本地IP,SSL请求通常是用于诊断。

这里给出siemens官网上的一个实例:读取本地ip

14和15行将在RDSYSST功能繁忙的情况下停止该函数。 下图展示了程序如何计算本地网络的第一个ip地址和ip总量。

address

这是通过将plc的本地ip和子网掩码进行按位AND运算来完成的,这将返回本地网络的起始地址(24-30行)。现在SNMP扫描器需要知道子网的ip总量。因此,我们将子网掩码与0xFFFFFFFF进行XOR运算(35-39行)。结果即为子网的ip总量。 下图展示了如何使用STL语言建立一个UDP连接。首先我们需要调用TCON方法,并使用我们TCON_PAR_SCAN这个DB作为该函数的背景数据块。

udp

UDP协议的情况下,TCON函数不能建立连接,他只能在TCP协议下完成,因为与UDP相反,他的连接是定向的。 但是只调用一次TCON是不够的,当#connect变量在两次调用之间从0上升到1时,连接函数才开始工作。这就是为什么我们在连接函数首次出现之后编写了一个切换功能(10-11行)。这将在一个周期里,TCON首次被调用之后改变#connect的值,使之从False变为True。TCON函数在下一个周期被调用时,检测到上升沿信号,之后开始执行。下一步操作是,基于UDP协议的SNMP数据包,并且接收响应。这将通过调用TUSENDTURCV函数来完成。之后,SNMP扫描完成,所有数据被存储到可以被攻击者下载的数据块中(step 3)。

给出一个实例udp协议交换

关于 TCON 这个函数,其他参数没什么问题,关键在于CONNECT这个参数,这个参数需要一个指针实参,指向以UDT 65为模版建立的DB块。关于UDT 65的细节可以在上文中的手册中找到,但是我们要手动建好这么个数据结构真是非常蛋疼,所以我找到了一个专门的用来建立TCON这个连接参数的工具 Open Communication Wizard_V2.3.3

SOCKS5 PROXY

一旦攻击者已经发现所有的SNMP设备,包括本地plc,下一步就是要连接到他们。这可以通过使用可访问的plc作为进入内网的网关来实现。为了做到这一点,我们选择在plc上实现一个socks5代理。这有两个主要原因,首先,SOCKS协议是轻量级的,非常容易实现。另外所有应用都可以使用此类型的代理,要么应用是原生支持socks协议的,要么可以使用所谓的proxifier来为任意程序添加对socks的支持。socks5协议在RFC 1928[24]中被定义。通过代理无差错地TCP连接到目标需要以下几步:

  • 客户端通过TCP连接到SOCKS服务器,并发送自身支持的身份验证方法列表。
  • 服务器使用一个选定的身份验证方法回复。
  • 根据所选择的身份验证方法,选定相应的子协议。
  • 客户端发送带有目标IP的连接请求。
  • 服务器建立连接,并回复。所有后续数据包在客户端和目标之间通过隧道传输。
  • 客户端关闭TCP连接。

我们的实现提供了最小化的必要功能,他不支持身份验证,所以我们跳过了第三步。我们也不支持错误处理。并且,只支持连接IPV4地址。一旦客户端连接,我们期望该信息会经过以下几个步骤:

  • 客户端提供身份验证方法:可以是任意信息,常用的比如:0x05 0x05 (1 byte) (n bytes)
  • 服务端选择验证方法:0x05 0x00 (无认证)
  • 客户端想要连接目标:0x05 0x01 0x00 0x01 (4 bytes) (2 bytes)
  • 服务器验证连接: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00
  • 客户端和目标现在可以通过与服务器的连接进行通信了。

如前面提到的,plc程序周期性执行。这就是为什么我们使用一个简单的状态机来处理SOCKS协议。因此,我们将每个状态编号,并且使用一个跳转表来执行相应的代码块。如下图:

status_list

状态转换是由存储在一个数据块中的状态码的递增来实现的。 每个状态和其动作描述如下:

绑定监听: 第一次启动程序需要绑定和监听SOCKS端口1080,这是通过系统调用TCON在被动模式下实现的。我们保持在这个状态直到有人连接到这个端口。 协商: 我们等待客户端发送任何消息。这通过TRCV函数实现,该函数需要EN_R参数使之执行。见下图:

listener

认证: 第一条消息后,我们发送回复,指明客户端是无认证的。为了这个目的,我们使用TSEND系统调用。与TRCV相反,这个函数是边沿控制的,这意味着REQ参数必须在连续的调用之间从False变为True,这样来激活发送。如下图所示,我们切换标志并且在REQ的上升沿两次调用TSEND

auth

连接请求: 然后,我们期望客户端发送一个包含有目标IP和端口号的连接设置信息,该信息将为下一个状态而储存。 连接: 我们使用TCON建立到目标的连接。

连接验证: 当到目标的连接已经建立,我们向客户端发送验证信息。

代理: 现在我们只需要在客户端和目标之间打开连接隧道。所有使用TRCV从客户端收到的数据被存储到一个缓冲区中,并且TSEND函数也从中取出数据发送给客户端。同样的原则也适用于相反的方向,但是我们必须考虑到,发送消息可能需要几个周期,因此,第二个缓冲区被用于确保没有消息被混合或者丢失。TRCV的错误标志被作为连接断开的信号。当该信号发生时,我们将发送最后收到的数据,然后跳转到下一状态。

复位: 在这种状态下,我们使用TDISCON关闭所有连接,并且重置所有标志位到其初始状态。

0x04 讨论


该代理的最大传输速率大约是40kb/s。如果socks代理单独运行在plc上,速率可以高达730KB/s。所有的网络设备以100Mbit/s的以太网直连plc。最后我们在实验室中测试了所描述的攻击周期。除了常规的通信,我们验证了可以通过使用tsocks库的socks隧道实施对DOS漏洞CVE-2015-2177的利用。利用代码通过socks隧道成功执行。

我们的攻击有一定的局限性。为了确保plc总是可以应答请求,主程序执行时间需要被监视,当执行时间过长时主程序会被结束。我们上传的snmp扫描器或者代理代码,连同原有程序,不应超过最大总执行时间,150ms。注入一个扫描器或者代理不太可能触发超时,因为代理运行时的附加执行时间为1.35ms,这远远小于150ms。另外,超时可通过在注入程序执行完成后重置时间计数器来避免,这要使用系统调用RE_TRIGR。针对上文中的攻击,最简单的防护方式是保持plc脱机,或者使用VPN来代替公网访问。如果这是不可能的,应当激活西门子plc的第三级保护级别。这使得plc的读取基于口令,并且有写保护。没有正确密码的攻击者不能修改plc程序。根据我们的研究,这个功能在实际中很少用到。另一个应用保护机制是使用防火墙过滤可疑数据包,比如试图重新编程plc的恶意访问。

0x05 在施耐德设备上的实现


查阅施耐德 UNITY PRO 手册得到以下技术细节:

施耐德设备的tcp通讯是依靠TCP_OPEN库完成的,但是这个库有很大的限制。 首先Unity Pro默认的库中是没有TCP_OPEN这个功能块库的,TCP OPEN的库是需要额外订购的,订货号为TLXCDTCP50M,安装后才能使用,且只有Unity Premium才支持这个功能块库。并且我发现,可能只有 TSX ETY 1100WSTSX ETY 5103 提供了TCP_OPEN这个库。

这就是说,可能在实际环境中只有非常非常非常少的施耐德设备可以完成完整的TCP通讯。这也是这种攻击的局限性,这在总结部分也会提到。

这里只说一下可能用到的几个函数块,具体API调用请查阅unity pro手册的 EF/EFB/DFB库 -> TCP Open库 -> 高级 部分。

  • FCT_ACCEPT 接受连接请求
  • FCT_BIND 将数据通道口编号绑定到ip地址和端口
  • FCT_CLOSE 删除数据通道
  • FCT_CONNECT 建立连接
  • FCT_LISTEN 配置通道等待连接
  • FCT_RECEIVE 检索数据通道中的可用数据
  • FCT_SEND 将数据发送到指定数据通道
  • FCT_SOCKET 创建新的数据通道

0x06 总结


其实这篇文章思路虽然新颖,但是并不是非常猥琐,还是大家都可以想到的点,虽然思路简单,但是经过我亲自的编程实现,发现坑点实在是太多,原本一周时间就完成本文的复现,但是最后发现有点心有余而力不足的感脚,毕竟我作为一只web狗,研究自动化的时间还太短。

之后,个人认为,这种攻击手段的局限性非常大,据我调查,西门子PN型号的CPU,数量相比之下还算较多,但是其他厂商的cpu基本不具有完整的tcp/udp通讯功能,比如上一部分提到的施耐德。

下面是我自己写的几个文中出现的小demo,放到一个工程里了,虽然实现上不完整并且问题很大,但是还是放出来给初学者摸一摸,更是想让高手指点一下。希望大家能一起交流一下。

S7_pro1.zip

直接用这个zip归档在step7里恢复一下就可以了,不需要解压。

有任何问题或者姿势指导,请发送邮件到 ronnyschiatto#gmail.com