工控二进制逆向框架ICSREF学习笔记

栏目: 软件资讯 · 发布时间: 5年前

内容简介:2019年的安全届四大顶会之一这里把论文的一些学习笔记和文章针对的主要是

2019年的安全届四大顶会之一 NDSS 录用论文前段时间发布了,其中有一篇关于工控逆向的论文, ICSREF: A Framework for Automated Reverse Engineering of Industrial Control Systems Binaries ,论文最主要的工作就是开发了一个针对工控二进制文件的逆向框架—— ICSREF

这里把论文的一些学习笔记和 ICSREF 框架的源码审计笔记做一个简单的记录。

文章针对的主要是 codesys 套件编译出来的 PRG 文件进行的逆向分析。 codesys 是一个独立于硬件的编程平台,是用于对PLC控制器的最强大的编程 工具 之一,支持 IEC 61131-3 的五种编程语言的标准。应用非常广泛,市场上超过250家公司都使用codesys,超过20%的PLC都可以采用codesys编程。codesys本质是一套工具,包括编程软件和设备runtime等等。codesys编译的时候会生成一个 PRG 文件,这就是文章研究的基础。

环境:用codesys+树莓派搭建模拟PLC环境

安装

首先是 codesys ,翻了下实验室的一堆PLC,都没有直接用 codesys 的,不过有个施耐德的PLC用的组态软件 somachine 应该是基于 codesys 的,因为打开 somachine 的时候,状态栏出现了一个 codesys gateway 的图标,和利时的 autothink 也是。不过还是想找使用原生codesys的。

麻神说 codesys 套件有树莓派的 RTS ,可以在树莓派上搞,然后就去扒拉了一下,这里记录下。

首先是上codesys官网下 CODESYS Development System V3 CODESYS Control for Raspberry Pi MC SL

找个win机器,把两个东西装好

然后点 Update Raspberry Pi
工控二进制逆向框架ICSREF学习笔记

然后就会弹出左边的框,这个时候,树莓派的用户名密码ip输一下,然后 install 就OK了。

可以在树莓派上看一下

工控二进制逆向框架ICSREF学习笔记

确实是跑起来了。

编程

可以新建一个标准工程一个试试

工控二进制逆向框架ICSREF学习笔记

选上树莓派。

然后自然是硬件组态步骤,这里由于树莓派就比较简单了

devide->通讯设置 然后输入ip搜一下就ok了。

工控二进制逆向框架ICSREF学习笔记

然后就可以去写一个简单的梯形图

工控二进制逆向框架ICSREF学习笔记

就可以编译下发,之后 调试->登录到 即可在线操作、监控调试等等。

工控二进制逆向框架ICSREF学习笔记

当然codesys还可以编写HMI,不过这里不多赘述。

PRG文件格式

不得不说作者的逆向功底确实是深厚啊。。。根据PRG文件介绍一下其主要的结构

Header

PRG二进制文件的前 0x50 个字节构成包含各种信息的文件头。

这里列出一些重要的信息:

  • offset=0x04 : 此位置的值 +0x08 得到函数符号表的结尾
  • offset=0x20 : 此位置的值 +0x18 得到程序的入口点
  • offset=0x2c : 此位置的值 +0x18 得到程序的结尾
  • offset=0x44 : 此位置的值 +0x18 得到函数符号表的结尾

符号表结构

符号表结构的开头没有在header里面显示出来,而 ICSREF 在寻找的时候是通过可显字符匹配大于4字节的连续可显字符就作为符号表。如下图所示:

工控二进制逆向框架ICSREF学习笔记

结构基本一样, 函数名+\x00+cnt+\x00 ,实际运行时会根据后两个数据字节来计算调用相应函数所需的跳转偏移量。

I/O映射关系

PRG文件在PLC里面运行的时候,对于PLC具体的物理I/O会映射到存储器的某地址,比如在 WAGO 750-881 的PLC中,接受输入时会从 0x28CFEC00 - 0x28CFF7F8 读取,刷新输出时更新 0x28CFD800 - 0x28CFE3F8 的值。

而这个值我们可以通过解密CODESYS安装目录下的一个TRG文件可以获得。具体的解密方式是将TRG文件和一个固定的2048位块进行异或操作即可。

功能块结构(Block结构)

根据61131-3的标准,PLC的程序的组织单元,我们称作POU,对应一个block,所以存在很多函数block(功能块)。以西门子s7-300为例,就存在组织块(OB)、系统功能块(SFB)、系统功能(SFC)、系统数据块(SDB)、功能(FC)、功能块(FB)等。编程的时候是对块进行编程。

工控二进制逆向框架ICSREF学习笔记

例如codesys默认的主块就是 PLC_PRG ,当然根据需要可以添加更多的功能块(POU)。

打个比方的话,就是写一个 Python 工程,有很多类,每个类是一个文件。这里就是每个功能是一个块。而一个块里面可能包含一个或者多个子程序。

这里要区分下子程序和函数,可自行维基百科关键字 subroutine

那么在PRG二进制文件中,每一个子程序都有入口和出口程序,入口对应的二进制序列为 \x0d\xc0\xa0\xe1\x00\x58\x2d\xe9\x0c\xb0\xa0\xe1 ,出口对应的二进制序列为 \x00\xa8\x1b\xe9

入口程序翻译成汇编如下:

MOV R12, SP 
STMFD SP!, {R11, R12, LR} 
MOV R11, R12

出口程序翻译成汇编如下:

LDMDB R11, {R11, SP, PC}

通过这样就可以寻找出所有的子程序。然后因为是arm架构,所以可以直接进行反汇编。

全局变量初始化

第一个子程序紧跟在header后面,从0x50开始。这个功能块是个特殊的函数,可以视作全局的INIT函数,用于初始化 VAR_GLOBAL 类型的变量和函数。

在用61131-3系列语言编程的时候,和普通的编程语言不大一样,所有用到的变量需要在一个地方声明,当然你可以在每个POU声明局部变量。

工控二进制逆向框架ICSREF学习笔记

而每个变量都有一个类型,其中全局变量就是 VAR_GLOBAL 类型,你可以在任意的POU里面使用。

工控二进制逆向框架ICSREF学习笔记

第一个功能块后面有三个很短的子程序,再后面跟着一个子程序目的是调用 SYSDEBUG 子程序,用于对动态调试的支持。

静态链接库和用户功能块的导入

SYSDEBUG 子程序后面紧跟的子程序用于导入静态链接功能块库,和用户编写的功能块库。

静态链接库功能块和用户自定义功能块由两个结构上相邻的子程序构成:第一个用于执行主要功能,第二个用于初始化内存。

所有的子程序的倒数第二个就是我们俗称的 main 程序,也就是之前提到的在codesys编程的时候默认为 PLC_PRG 的POU。这个程序也就是扫描周期的起点了。

函数调用

由于PLC对于实时性的要求,基本上PLC程序编译器编译后都不会存在运行时解析,即像是延迟加载绑定这种是不存在的,所以也就意味都是静态调用关系,所以很容提取出函数之间的调用关系,重构出CFG图。

PRG文件中从一个子程序到另一个子程序或动态链接函数的调用间接跳转的指令如下:

STR Ri, [SP,#-4]! 
STR LR, [SP,#-4]! 
LDR Ri, =SUB_OFFSET 
LDR Ri, [Ri] 
MOV LR, PC 
MOV PC, Ri 
NOP 
LDR LR, [SP],#4 
LDR Ri, [SP],#4

所以就能提取出某一个子程序中调用的所有函数的偏移。

源码阅读

源码结构(radare2除外):

工控二进制逆向框架ICSREF学习笔记

icsref.py 就是主文件,然后核心的代码在 PRG_analyze.py 里面,

icsref.py 中的核心类 icsrefPrompt 代码使用了cmd2库,扩展自cmd库,基本用法没什么太大变化,用来进行命令行的解析和交互。通过 do_* 的函数就是实现对应的命令操作。

按顺序来走把,首先是 console 函数,

def console():
    prompt = icsrefPrompt()
    prompt.prompt = 'reversing@icsref:$ '

    # Load banner
    thisdir = os.path.split(__file__)[0]
    banner_f = os.path.join(thisdir, 'data', 'banner')
    __file__
    with open(banner_f, 'r') as f:
        lines = f.readlines()
    banner = ''
    for line in lines:
        banner += line

    sys.path.append(thisdir)
    for i in os.listdir(os.path.join(thisdir, 'modules')):
        if i.startswith('module_') and i.endswith('.py'):
            # Get name without extension
            mod_name = 'modules.' + os.path.splitext(i)[0]
            # Get module
            mod = importlib.import_module(mod_name)
            # Add the methods of mod (ONLY) to icsrefPrompt class as do_<something>
            name_func_tuples = inspect.getmembers(mod, inspect.isfunction)
            name_func_tuples = [t for t in name_func_tuples if inspect.getmodule(t[1]) == mod]
            for fun in name_func_tuples:
                setattr(icsrefPrompt, 'do_{}'.format(fun[0]), fun[1])

    # Start cmd module
    prompt.cmdloop(banner)
if __name__ == '__main__':
    console()

主要是实例化一个 icsrefPrompt 类,然后把 modules 目录下的模块全部注册进来,以 do_* 加入命令行操作中。

然后是 icsrefPrompt 类:

class icsrefPrompt(Cmd):
    """
    cmd2 prompt class for the interactive console
    """
    def __init__(self):
        Cmd.__init__(self, use_ipython=True)

    def do_load(self, filename):

    def do_analyze(self, filename):

    def do_save(self, filename):

继承自cmd2库,用于处理命令行交互。实现了三个操作 loadanalyzesave ,其他操作都在 modules 目录,由之前的 console 函数初始化时添加进来。

  • load :主要是加载 .dat 文件,这个文件是 analyze 分析的结果保存成的文件,通过 dill 库将结果反序列化存储的, dill 库是一个扩展自 pickle 的模块,这里不赘述。然后还原关键变量 self.prg ,本质是个 Program 类,后面会详细介绍这个类。
  • analyze :核心,实例化一个 Program 类来解析PRG文件,将结果保存到result目录下,存成 .dat 格式。
  • save :analyze之后,将结果换个名字保存。

接下来看看 PRG_analysis.py 文件中的关键类之一 program 类。

class Program():
    def __init__(self, path):
            """
            init function creates the Program object and does the analyses
            """
            # Program path
            self.path = path

            # Program name
            self.name = os.path.splitext(os.path.basename(self.path))[0]

            # Program hexdump
            self.hexdump = self.__read_file()
            print('DONE: Hexdump generation')   

            # Analyze program header
            # ROM:00000004: End of strings
            # ROM:00000020: Entry point (OUTRO?) + 0x18 (==24)
            self.program_start = struct.unpack('I', self.hexdump[0x20:0x20+4])[0] + 24
            # ROM:0000002C: End of OUTRO? + 0x18 (==24)
            self.program_end = struct.unpack('I', self.hexdump[0x2C:0x2C+4])[0] + 24
            # ROM:00000044: End of dynamic libs (Before SYSDBGHANDLER)
            self.dynlib_end = struct.unpack('I', self.hexdump[0x44:0x44+4])[0]

            print('DONE: Header analysis')

            # Program strings
            self.strings = self.__strings()
            print('DONE: String analysis')

            # I/O analysis from trg file
            self.__find_io()
            print('DONE: I/O analysis')

            # Function Boundaries
            self.FunctionBoundaries = self.__find_blocks()
            print('DONE: Find function boundaries')

            # Program functions
            self.Functions = []

            self.__find_functions()
            print('DONE: Function disassembly')

            # Find all static and dynamic libraries and their offsets
            # Dynamic libraries
            self.dynlibs_dict = self.__find_dynlibs()
            print('DONE: Find dynamic calls')

            # Static libraries
            self.statlibs_dict = self.__find_statlibs()
            print('DONE: Find static calls')

            # All libraries: Add dynamic and static calls
            self.libs_dict = self.dynlibs_dict.copy()
            self.libs_dict.update(self.statlibs_dict)

            # Find library calls for each function
            self.__find_libcalls()
            print('DONE: Call offsets renaming')

            # Save object instance in file
            self.__save_object()
    ....
    ....
    ....
    ....

该类的主要作用就是根据 PRG 文件的结构对其进行解析,从上面 init 函数就可以看出来,主要进行 提取header、提取io映射关系、寻找子程序、寻找子程序调用关系、寻找库 ,具体的方法在上一节基本都已经解释了。

此外 PRG_analysis.py 文件中还有另一个 Function 类,

class Function():
    def __init__(self, path, start, stop, hexdump, disasm):
        """
        Function initialization
        """
        # Path
        self.path = path
        # Function start offset
        self.start = start
        self.offset = start
        # Function name. Convention: sub_<offset>
        self.name = 'sub_{:x}'.format(self.start)
        # Function stop offset
        self.stop = stop
        # Function length in bytes
        self.length = stop - start
        # Hexdump of particular function
        self.hexdump = hexdump
        # Disassembly listing of particular function
        self.disasm = disasm
        # Create string with opcode sequences for hash matching
        op_str = ''
        for line in self.disasm:
            op = line[43:].split(' ')[0]
            # Discard data
            if len(op) < 8:
                op_str += line[43:].split(' ')[0]
        # Function opcodes SHA256 hash
        self.hash = hashlib.sha256(op_str).hexdigest()
        # Initialize list of calls from function. Gets populated later
        self.calls = {}

就是一个 function 结构体,每一个从PRG文件提取出的子程序对应一个function类,主要包括 偏移信息,反汇编代码,调用的函数 等信息。

然后是 modules 下的各个模块信息:

  • module_analytics.py 提供一个 analytics 命令,用于打印函数调用的统计,每个子程序被调用的次数,以及每个子程序调用了哪些函数。
  • module_cleanup.py 提供一个 cleanup 命令,用于清楚result下面的分析结果文件。
  • module_graphbuilder.py 提供一个 graphbuilder 命令,用于生成程序调用关系图,然后以svg格式保存在result文件夹下面。
  • module_hashmatch.py ,提供一个 hashmatch 命令,作者把很多公开的常用的功能块和子程序做了签名,形成一个库,然后该命令可以匹配库。
  • module_pidargs.py 提供一个 pidargs 命令,仅针对 PID 功能块,该命令使用 angr 重构堆栈,然后提取处 PID 的功能块的函数参数。
    该功能块如下:
    工控二进制逆向框架ICSREF学习笔记

其实也就是一个概念验证,毕竟是论文的产物不是工程化的东西,作者用该功能块来验证他能够提取出函数参数,那么对于其他函数也就是本质相同的重复性工作。


以上所述就是小编给大家介绍的《工控二进制逆向框架ICSREF学习笔记》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员面试宝典(第5版)

程序员面试宝典(第5版)

欧立奇、刘洋、段韬 / 电子工业出版社 / 2015-10 / 55.00

容提要 《程序员面试宝典(第5版)》是《程序员面试宝典》的第5 版,在保留第4 版的数据结构、面向对象、程序设计等主干的基础上,修正了前4 版近40 处错误,解释清楚一些读者提出的问题,并使用各大IT 公司及相关企业最新面试题(2014-2015)替换和补充原内容,以反映自第4 版以来两年多的时间内所发生的变化。 《程序员面试宝典(第5版)》取材于各大公司面试真题(笔试、口试、电话面试......一起来看看 《程序员面试宝典(第5版)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具