内容简介: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
然后就会弹出左边的框,这个时候,树莓派的用户名密码ip输一下,然后 install
就OK了。
可以在树莓派上看一下
确实是跑起来了。
编程
可以新建一个标准工程一个试试
选上树莓派。
然后自然是硬件组态步骤,这里由于树莓派就比较简单了
点 devide->通讯设置
然后输入ip搜一下就ok了。
然后就可以去写一个简单的梯形图
就可以编译下发,之后 调试->登录到
即可在线操作、监控调试等等。
当然codesys还可以编写HMI,不过这里不多赘述。
PRG文件格式
不得不说作者的逆向功底确实是深厚啊。。。根据PRG文件介绍一下其主要的结构
Header
PRG二进制文件的前 0x50
个字节构成包含各种信息的文件头。
这里列出一些重要的信息:
-
offset=0x04
: 此位置的值+0x08
得到函数符号表的结尾 -
offset=0x20
: 此位置的值+0x18
得到程序的入口点 -
offset=0x2c
: 此位置的值+0x18
得到程序的结尾 -
offset=0x44
: 此位置的值+0x18
得到函数符号表的结尾
符号表结构
符号表结构的开头没有在header里面显示出来,而 ICSREF
在寻找的时候是通过可显字符匹配大于4字节的连续可显字符就作为符号表。如下图所示:
结构基本一样, 函数名+\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)等。编程的时候是对块进行编程。
例如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声明局部变量。
而每个变量都有一个类型,其中全局变量就是 VAR_GLOBAL
类型,你可以在任意的POU里面使用。
第一个功能块后面有三个很短的子程序,再后面跟着一个子程序目的是调用 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.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库,用于处理命令行交互。实现了三个操作 load
, analyze
, save
,其他操作都在 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学习笔记》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员面试宝典(第5版)
欧立奇、刘洋、段韬 / 电子工业出版社 / 2015-10 / 55.00
容提要 《程序员面试宝典(第5版)》是《程序员面试宝典》的第5 版,在保留第4 版的数据结构、面向对象、程序设计等主干的基础上,修正了前4 版近40 处错误,解释清楚一些读者提出的问题,并使用各大IT 公司及相关企业最新面试题(2014-2015)替换和补充原内容,以反映自第4 版以来两年多的时间内所发生的变化。 《程序员面试宝典(第5版)》取材于各大公司面试真题(笔试、口试、电话面试......一起来看看 《程序员面试宝典(第5版)》 这本书的介绍吧!