工控安全PLC固件逆向二

一、逆向工程

逆向工程是网络安全行业里面一项很重要的技术。

逆向工程

首先,我们解释下逆向工程是什么。逆向是一个相对正向而言的解释,相对正向来说,对一个程序来讲,正向就是开发的过程,从0到1。

就是在一个软件诞生的整个生命周期中的一个过程, 也就是按照需求通过编码把需求实现,产生一个程序,当然这个程序可能是web,也可能是一个应用软件或者APP。

在正向开发之后,这个程序经过编译,程序被封装成了只有计算机才能识别的0和1这种字节码二进制文件,如果此时我们想对该开发的程序进行渗透或者去分析运行轨迹,又或者说去对该程序做一、定的修改时,那么就需要对该程序进行逆向分析(当然不建议对其他人的软件做修改哦)。

那么说到这我们更通俗的来表达一下,正向就像工厂生产一个产品,而逆向了就像你小时候败家的样子,总喜欢把一些玩具或者电子电器拆开研究一下他里面有啥,他是怎么运行的,当然绝大多数情况下,你一定挨了不少骂,说不定还逃脱不了一顿打,但是用我常讲的话来说攻击即防御,先正向后逆向,你得明白正向的过程,你才可能站在逆向的角度去考虑程序的设计结构,或者我们说得有逆向思维,又或者讲为反推。

逆向工程内容

逆向工程包含Web逆向、软件逆向、安卓逆向、移动APP逆向,他们的差别是什么?

web逆向是因为动态语言开发运行后不像静态语言会把源代码通过http请求给传输给客户端,客户端往往只能看到web程序的运行结果,当然这个阶段还会伴随着js加密,ajax同异步数据传输等情况,那对这些加密的过程进行解密的过程就属于web逆向。

软件逆向是四者中间更大的一个词汇,软件逆向里面可以包含移动逆向、苹果IOS逆向以及安卓APK文件的逆向。即后者与前者属于包含关系。

软件逆向工程是指从程序系统出发,通过运用解密、反汇编、系统分析、程序理解等多种计算机网络安全技术,对软件的结构、流程、算法、代码等进行逆向的拆解分析,从而推出软件产品的源代码、设计原理、结构、算法、处理过程、运行方法及相关文档等的过程。

整个过程被称为软件逆向工程,过程中采用的技术被称之为软件逆向工程技术。

逆向常用的软件

OllyDbg:主用于调试程序,无法调试内核,UI功能强大。

SoftICE:工作在ring0态的调试器,常用于调试驱动程序,功能强大的命令行工具。

WinDbg:介于上两者之间的调试器,可视化图形界面,主要通过命令来进行调试。

IDA Pro:反汇编软件,用于静态反汇编,附带有较弱的动态调试功能。

UltraEdit:16进制编辑器,可直接修改可执行文件,也可用于常见语言的变成工作。

dex2jar:一款用于Android平台程序逆向分析的工具,从名字中可以看出,通过它,可以将Android的APK包中的可执行程序dex文件转换成jar包

逆向工程属于软件安全中的硬功夫,值得深入研究,但学习者通常也会觉得枯燥,难度大。

从业者在学习过程中,需要积累对目标程序足够多的知识。如果没有这些基础知识的支撑,那距离实际应用会差非常远。

二、PLC逆向

之前我们学习了包括modbus、S7comm、DNP3等等工控领域的常用协议,从这篇开始,我们一步步开始,学习如何逆向真实的plc固件。

用到的固件为GitHub - ameng929/NOE77101_Firmware

目前网上几篇对于该固件的分析都是以2018工控安全题目解题为主,并没有相应的知识和说明,这次我们不会做题目方面的说明,而是重点关注如何从零开始逆向固件。

将会涉及到vxworks操作系统、PowerPC CPU架构的汇编语言、Ghrida的反编译问题,网上提供的资料较少,可能对于很多人来说是全新的知识。由于篇幅所限,也为了不让大家烦躁,我们暂且不对这两个新玩意儿进行长篇大论,我会将知识点穿插讲解。另外,建议大家使用windows系统,mac版的ida无法对PowerPC的固件进行函数识别,使用了好几个版本测试都存在该问题,原因暂时不明。

固件提取

这一步其实和路由器的固件逆向非常相似,厂商都会在官网提供固件的升级包,我们通过下载安装来更新设备的软件版本。不过路由器的版本更新相对于plc就频繁得多了,plc升级在大多数厂家的认识中是一件“麻烦”事(假如说一天一更新那工厂还干不干活了……),所以plc的固件是相对稳定的,我们选用的固件虽然看上去很“古老”,但如果考虑到我们现在不是进行漏洞挖掘,而是进行基础学习的话,就很合适了。

这里用到的工具就是大家熟知的binwalk,不过有所不同的是对于路由器来说,我们其实是“知根知底”的,因为架构和操作系统都还在我们的知识范围,而plc的操作系统和cpu就不那么友好了。

如图为binwalk操作某路由器固件,可以看到信息很多,包括文件系统等等一堆。

工控安全PLC固件逆向二_第1张图片

如图为操作plc固件,基本可以说是啥信息都没有。

工控安全PLC固件逆向二_第2张图片

直接binwalk -e NOE77101.bin提取即可,对分离出来的文件再一次binwalk就可以看到很多有用的信息了,包括固件采用的操作系统、内核版本、符号表等等信息。

工控安全PLC固件逆向二_第3张图片

至此我们成功提取除了固件,要注意的是,这里操作系统是vxworks,cpu则是big endian的PowerPC,所以接下来分析工作并不是一帆风顺的,还需要经历诸多磨炼。

手动修复与自动修复

手动修复

我们将固件拖入ida准备开始分析。

工控安全PLC固件逆向二_第4张图片

首先要修改处理器选项,因为ida并不能识别出来我们处理器的架构,我们需要将Processor type手动设置为PowerPC的big endian。

工控安全PLC固件逆向二_第5张图片

接着ida还会让我们指定RAM和ROM的起始地址,在我们不知道的情况下,只能直接点击ok,剩下的选项都直接默认即可。其实这里已经有成熟的工具可以用来自动化识别了,但是考虑到我们是第一次实践,还是认认真真的尝试着走一遍。

进入后发现ida的函数少得可怜(mac版的就干脆一个也没有,笔者这里是在windows上保存了idb后在mac上操作的),而且一般的ida不支持对PowerPC的反编译,就很难受。当然,再难受也得硬着头皮看,先找到第一个函数。

工控安全PLC固件逆向二_第6张图片

看不懂?没事,我们一点点扣。首先这里大量使用到了r1处理器,在PPC(后文中PowerPC都会简称为PPC)中共有32个通用寄存器,虽然叫通用,但有一些寄存器和x86一样被“固化”了,比如r1就是栈寄存器,其余“固化”的还有:

  • r0,函数序言(function prologs)阶段使用,一般不需要我们关心
  • r2, toc指针,名字很高大上,其实就是系统调用时,标识系统调用号。
  • r3,返回值
  • r4-r10,参数,返回值较为特殊(比如乘法导致一个寄存器放不下的时候)时,r4也可以存放返回值
  • r11,用在指针的调用和当作一些语言的环境指针。
  • r12 ,用在异常处理和glink(动态连接器)代码。
  • r13 ,保留作为系统线程ID。
  • r14-r31,本地变量

还有好多好多寄存器,我们贯彻用到哪说哪的原则,这里就不再提了,需要我们再说。

lis r1,1
addi r1,r1,0

这两条指令可就有意思了,lis是加载立即数(load Immediate)的意思,它将16位的立即数传送到r1,然后再左移16位,在本条指令中就是把1放到了寄存器的第17位上,也就是0x10000了。

这里就冒出来问题了,我们很显然一般用的都是32位数(不管是编程还是寄存器长度,一般都是32位)啊,PPC的指令长度为4个字节,也就是32位,除去指令码和寄存器外,根本就放不下32位的数,难不成这里的1就是16位的1?

当然不是啦,这里就要看第二条指令了,addi的意思就是让将r1的低十六位+0的值给r1的低十六位,这样就是lis操作高十六位,addi再补上低十六位,成功实现了32位立即数的传送。

下面关于r3的操作同理,接着又是addi操作,将r1的值减了0x10,然后是b指令,b指令就是call了,调用一个函数的意思。还记得r1是干什么的吗?没错,r1是栈寄存器啊,这里开始将栈赋值,然后又对一个栈地址进行了减法操作意味着什么?

没错,一开始进行的实际上是栈的初始化,我们把它叫做initStack,接着的减法实际上就是在栈上开辟了空间(栈是高地址向低地址增长的),这里就应该是整个固件的初始化函数的一部分。

工控安全PLC固件逆向二_第7张图片

再查看官方手册,我们发现,初始化的栈的地址也就是System Image的地址,所以我们ida最开始选择的RAM、ROM的地址就应该是0x10000。我们重新用ida打开该程序,设置RAM、ROM,就可以发现识别的函数多了很多了。

这一步之后我们需要符号进行还原,毕竟谁也不愿意看着一堆sub_xxx来分析吧?这里我们就要用到binwalk告诉我们的符号表地址了(忘了吗?是0x301e74),我们用010editor打开固件,16进制形式分析,跳转到对应地址。

工控安全PLC固件逆向二_第8张图片

我们之前已经知道了这是vxworks5的固件,此类固件的符号表比较特殊,16字节为一组,分别表示的是符号字符串地址、符号所在地址、特殊标识(比如0x0500就是函数的意思)、0填充位,所以我们就可以按照这个规则来进行符号的修复。

脚本如下:(大家自行按照自己的固件地址对Start和end进行修改即可)

# coding:utf-8
from idaapi import *
import time

eaStart = 0x31eec4 
eaEnd = 0x348114
ea = eaStart
while ea < eaEnd:
    offset = 0
    MakeStr(Dword(ea - offset), BADADDR)
    sName = GetString(Dword(ea - offset), -1, ASCSTR_C)
    print sName
    if sName:
        eaFunc = Dword(ea - offset + 4)
        MakeName(eaFunc,sName)
        MakeCode(eaFunc)
        MakeFunction(eaFunc,BADADDR)
        ea = ea + 16
print"ok"

修复完后ida的function栏可以说是相当友好了。

自动修复

上面我们对固件进行了繁琐的分析最终才完成了对于函数表的修复,实际上,已经有大神开发了一款插件可以对Vxworks的固件进行自动的修复和分析,这就是平安的银河安全实验室开发的vxhunter,大家可以去github自行下载(感谢大佬们的奉献)。

GitHub - PAGalaxyLab/vxhunter: ToolSet for VxWorks Based Embedded Device Analyses

之前我们手动分析用的是ida,自动部分我们就用Ghidra来进行(还有一个最大的好处是Ghidra支持对PowerPC的反汇编,所以之后的分析我们都采用Ghidra来进行)。

按照readme安装vxhunter,然后打开windows选项卡,选择scriptManager,运行脚本即可。

工控安全PLC固件逆向二_第9张图片

耐心等待一段时间,即可完成。

工控安全PLC固件逆向二_第10张图片

是不是非常简单?下一步就可以开始进行分析了。

小试牛刀

首先我们跳转到之前简单分析的initStack(要注意之前是0x4c,但是我们把基址设置为0x10000后地址应该是0x1004c)。

工控安全PLC固件逆向二_第11张图片

可以看到在initStack操作后,进行了跳转,目标是usrInit,还记得参数的传递规则吗?没错,这里的r3就是为了传递参数的,可以看到,和我们之前说到的一样,还是使用lis和addi的搭配进行32位整数的传递。

当然其实再往上看我们还可以发现r4寄存器的身影,因为PowerPC不像x86的stdcall,能通过push来判断哪些是函数的参数,所以我们只能是把r3~r10的身影都锁定,宁可错杀,不可放过。(我们也可以通过查看后续函数的调用来猜测,不过也是猜测,还是多注意为好)。

工控安全PLC固件逆向二_第12张图片

这里可能有些人一看就慌神了,“我靠,这都不认识,怎么看啊?”,不慌不慌,其实这里的调用十分有规律,我们拆开看看。

00010018 3c 80 04 00     lis        r4,0x400
0001001c 38 84 00 00     addi       r4,r4,0x0
00010020 7c 90 8b a6     mtspr      IC_CSR,r4
00010024 7c 98 8b a6     mtspr      DC_CST,r4

可以看到,实际上是进行了三组同样的操作,只不过是r4的值不一样罢了,我们选择一组来进行分析。

首先打头的还是lis和addi的组合,将r4的值设置为0x4000000,然后是mtspr指令,指令格式如下:

mtspr      spc_reg,reg

spc_reg是特殊寄存器的意思,指令将reg寄存器的内容传个一个特殊寄存器,所以这条指令的意思就是将0x4000000赋给IC_CSR寄存器。可以看到,这个r4就是个中间商,那么和之后的参数传递应该是没有关系的。

看到这里有些同学可能就会说了:“你不是说Ghidra有反汇编功能吗?那你还费劲让我们看这个?”,哎,别急别急,我们就来看看Ghidra给我们的反汇编是什么样的。

工控安全PLC固件逆向二_第13张图片

它就分析出了三条,instructionSynchronize()实际上对应的指令是isync,也就是指令同步的意思,并不关键,TLBInvalidateAll()实际上对应的指令是tlbia,也就是快表的相关操作(快表不知道的去翻大二专业课《操作系统》吧,文章内实在没地方讲了),而最后就是函数调用了。

有没有发现?这反编译显然是把我们的mtspr给落下了,直接将r4当成了参数,导致usrInit有了俩参数!其实我们在刚才的探索中很明显看到r4在这绝对没有起到参数传递的功能。这就说明了对于PowerPC的反编译,ida干不了,但Ghrida干得也不是很好,所以在之后的分析中我们要时刻留意,绝对不能只看反编译结果。

接着我们调到usrInit函数来看看,还是先看汇编部分:

工控安全PLC固件逆向二_第14张图片

这部分是大家熟悉的函数序言,我们简单分析一下。

stwu r1,local_18(r1)

意思就是将r1的内容送到r1+local_18的地址中,r1我们说过是栈顶指针。

mfspr r0,LR

这句话在ida上会被翻译为mflr r0,就是讲LR的值给了r0,LR寄存器是记录函数返回地址的寄存器

stw r31,local_4(r1)

和第一句格式相同,是将r31的内容送到r1+local4的地址中

stw r0,local_res4(r1)

这里local_res4是个正数,也就是说将r0送到了一个栈基址往上的地方,在想到r0内容是函数返回地址,是不是就清晰了?实际上就是相当于x86中call指令将函数返回地址保存在栈上的操作。

or r31,r1,r1

or指令的操作是将第二个操作数与第三个操作数or后保存进第一个数中。那就有人要问了,r1和r1进行了or之后不还是r1吗?好问题,实际上这种格式就是PPC的mov指令,写成x86就是mov r31,r1,为什么这样大费周折呢?这里我猜测是为了提高效率。

说到这是不是大家已经脑补出了函数序言的基本行为?实际上和x86并没有什么本质上的区别。

继续来看该函数:

工控安全PLC固件逆向二_第15张图片

又是一堆函数,那我们就先看看第一个吧,它以param1作为参数,而param1就是r3,前面分析了是0。

工控安全PLC固件逆向二_第16张图片

首先是bzero操作,参数是开始地址和结束地址,将中间部分都置为0,之后又这段地址将用来保存数据。

sysStartType是系统的启动类型,包括有bootram启动和rom启动,压缩式和非压缩式等等,这里受篇幅所限,先不细讲了。

intVecBaseSet是非常非常非常重要的一个函数,不知道大家看到intVec有没有想起来在《Windows调试艺术》中我们说过有个东西叫中断向量表(Interrupt Vector Table),其实就是那玩意,这个函数就是用来设置起始地址的,这里的起始地址固定为0。

返回到usrInit我们又看见下面还一个名字中带Vec的啊,那正好再来看看这个伙计:

工控安全PLC固件逆向二_第17张图片

是不是又觉得有点难了?别慌,慢慢来,首先是var2变成了地址,而后又大量使用var2的数组形式,那就让我们瞅瞅地址指向了啥。

工控安全PLC固件逆向二_第18张图片

哦,var2[0]是个数,var2[1]是个地址,指向的是个函数,var2[2]是个地址,指向的是个结构体。

往后走首先是if检验excExhandle(也就是var2[2])是不是为NULL,不是继续,显然这里不是,那就继续。

接着是个非常怪异的循环,涉及到指针、地址、结构体、结构体指针的变换,可能会很难,我尽量说的简单一些。

首先找迭代变量,这里很显然是*var1,而var1是啥?var1是var2+5,var2加5(这里的加5是地址加5,不是真的+5),看看图中地址就知道了,它跑到了handle,也就是说var1在验证handle是否为空。

var2同样在迭代,不过它是每次+3,在看图,也就是到了0x200那个地方,下面又是一个相同的结构。

最终是以*var2、var2[2]为参数调用var[1],也就是excExcHandle(data,excExHandle),在迭代不断地进行该函数,完成Interrupt Vector Table的初始化工作。

三、Vxworks

由于涉及到操作系统的内容,建议大家在阅读本篇前有一定操作系统知识的基础,或者是阅读我的《Windows调试艺术》的文章简单了解诸如线程、中断、驱动等的知识。

什么是 Vxworks

Vxworks操作系统是由美国Wind River System 公司开发的一套实时操作系统,Vxworks比起linux,有更好的实时性和可裁切性,公司可以根据自己的需求去定制化Vxworks,我们逆向的固件就是施耐德在Vxworks上进行的二次开发。在国防安全、工业化方面Vxworks占据了半壁江山,甚至连爱国者导弹都与Vxworks有关,可见其在工控领域的地位,但是,由于Vxworks目前几乎是没有任何的”纯新手“入门书籍,所以学习起来就较为困难,但是如果想要深入学习或者进行二次开发的话,可以参考官方的相关手册。

Vxworks的任务

Vxworks作为RTOS(real time operating system)有一套独特的任务体制和调度方案来保证其实时性,在对固件进行进一步研究之前,我们有必要把这一部分内容搞清楚,保证后续工作的顺利。

在Vxworks中有四种任务队列:

  • active队列,所有的任务都在这个队列中,也叫做活动队列,当我们在shell中运行i命令显示全部任务时
  • tick队列,Vxworks中有tsakDelay函数,该函数的目的是为了让某个任务推迟执行,也就是暂时不可抢占cpu的任务,这些任务就保存在tick队列中,也叫做定时队列
  • ready队列,已经做好所有准备、等待cpu的任务,也叫做就绪队列
  • work队列,一种特殊的环形队列,也叫做内核延时队列

在Vxworks中,有着usrAppInit的函数(取决于某个宏,我们这里就先认为默认有了),实际上就相当于我们平常理解的main函数了,我们可以在这”胡作非为“,但是我们作为一个多任务系统,不可能就一个main函数一个主任务打天下,所以还需要几个函数来进行任务的相关操作。

Vxworks的任务有256个优先级(0~255),0为最高优先级,对于应用层的程序,一般使用100到250的优先级,驱动类的程序使用51到99。和其他操作系统一样,每个任务都用一种数据结构表示,也就是TCB(在之前的《Windows调试艺术》中我们详细介绍过该结构,虽然操作系统不同,但是设计思想是一致的)。

要注意,在Vxworks5.x版本中,并没有严格区分内核态和用户态(使用KernelState全局变量来标识是否为内核,但栈还是一个栈,并没有从本质上区分),也就是说TCB还是暴露在用户视野下的,所以一旦存在堆栈溢出的情况,会非常致命,而在6.x之后Vxworks正式区分了用户与内核,极大改善了安全问题。

int taskSpawn(
    char *name,
  int priority,
  int options,
  int stackSize,
  FUNCPTR entryPtr,
  int agr1,
  int agr2,
  ...
  int agr10
)

该函数用来创建一个新的任务,返回的是任务的”身份证“,同时也是个内存地址,指向该任务的TCB,也被称为tid

  • name,执行任务的名字
  • priority,优先级,也就是上面提到的那256个
  • options,控制任务的某些行为,比如VX_DSP_TASK意思是要使用DSP处理器来支持该任务
  • stackSize,也就是该任务要使用的栈的大小
  • entryPtr,任务要执行的函数的指针,一般称为入口函数
  • args,入口函数所需要的参数,如果多于10个还可以使用指针进行结构体或数组传输的方式
int taskCreate(
...
)

该函数参数与taskSpawn完全一致,返回的同样是tid,但是它创建的任务并没有做好运行的准备(也就是说没有进入ready队列),需要使用taskActivate来唤醒它。

STATUS taskDelete(int tid)

该函数用来删除一个任务,但是该函数非常非常危险,一般是不使用的。举个栗子,假设我们有一个扳手,现在有一个任务在用,后面还有几个任务在排队等待使用,但是突然你把人家delete了,就相当于连人带扳手都没了,但后面几个任务就傻眼了,成了无限等待了。

当然,关于的任务的函数还有很多很多,这里只是简单地说明,之后碰到了我们再去看。

Vxworks的启动

之前我们用到了sysStartType(系统启动类型)这个参数,但是由于篇幅所限没有详细说明,这里就先来看一下。

Vxworks简单来说有两种不同的启动方式:

  • bootram启动,类似我们pc的BIOS,先有个小的操作系统,这个操作系统再去引导真正要用的操作系统运行。这个小的操作系统就是bootram,它存放在ROM或者是Flash中,它运行后会通过串口或是网口将Vxworks下载到RAM在进行启动工作。
  • ROM启动,Vxworks映像直接保存在ROM中,直接启动即可。

bootram启动(对比Vxworks相关函数)

因为ROM启动和bootram实际上在后续的部分完全一致,所以这里我们挑选更为复杂的bootram启动来做讲解。

当上电时,系统会自动跳转到ROM或是Flash的bootram引导程序,有趣的是,bootram和Vxworks的命名方式非常相似。一个是bootConfig.c,一个是usrConfig.c,而程序中像是usrInit、usrRoot等的函数名完全一致,当然了,功能上也有类似之处,下面说的usrInit、ursRoot没有特殊说明均为bootram的。

对于bootram来说,又可以按照是否进行了压缩分为bootram.bin和bootram_uncmp.bin等等,因为大体思路相同,这里我们就以bootram.bin为例进行说明。

  • romInit,初始化工作。比如初始化内存寄存器、初始化寄存器、初始化栈(这个栈是bootram用到的栈,和我们后来的Vxworks没有关系)、禁止中断等等
  • romStart,复制工作。它将非压缩(这里非压缩的就是romInit和romstart)部分复制到ram的低地址(定义为RAM_LOW_ADRS)部分,将压缩的部分复制到ram的高地址(定义为RAM_HIGH_ADRS)部分并进行解压;对于冷启动(之前文章中提到过,会重置数据)来说,它还会将ram的数据清空;最后再跳转到usrInit。
  • usrInit,和我们之前分析的Vxworks的usrInit有类似之处
void usrInit(int startType)
{
    while (trapValue1 != 0x12348765 || trapValue2 != 0x5a5ac3c3 )
    {
        ;
    }           
    cacheLibInit (0x02 , 0x02 );
    bzero (edata, end - edata);
    sysStartType = startType;
    intVecBaseSet ((FUNCPTR *) ((char *) 0x0) );
    excVecInit ();
    sysHwInit ();
    usrKernelInit ();
    cacheEnable (INSTRUCTION_CACHE);
    kernelInit ((FUNCPTR) usrRoot, (24000) ,(char *) (end) ,sysMemTop (), (5000) , 0x0 );
}

我们这里就把固件的usrInit也拿出来,放一块对比着学习。

工控安全PLC固件逆向二_第19张图片

首先是做了个死循环,检查两个变量的值,很显然值就是两个地址,实际上就是检查romStart的复制工作是不是成功了。而Vxworks显然不需要再对RAM的内存空间进行检查了,所以没有这一步。

接着调用cacheLibInit,可以看到两个usrInit都有这个函数,xxxLibInit可以视作一类函数,功能是初始化xxx的库函数,这里就是初始化cache(就是缓存)的库函数,至于参数则是初始化中一些选项,不用在意。

bzero (edata, end – edata)上一次也说了,将一段地址的内容赋为0,这里实际上就是将BSS段清0

intVecBaseSet、excVecInit 上篇文章中详细分析了代码,就是布置中断向量表。

sysHwInit ()这个函数用来初始化设备,直观来说就是将各种外设进行简单的初始化,同时让他们保持“沉默”,我们都知道CPU通过中断来响应外设,但由于现在我们还没完全建立起中断体系(只是简单地建立了interrupt vector),所以设备一旦产生中断,那就会出现没有中断处理函数的尴尬情况,进而导致系统出错,所以需要让设备保持“沉默”。

往后走是rootram的cacheEnable和Vxworks的usrCacheEnable,其实类似xxxEnable的函数都是“使能”的意思,就是数字电路中的使能端,只有使能了,这个东西才可以用。

最后就是最最最关键的usrKernelInit了,我们先来看看Vxworks的,如下图:

工控安全PLC固件逆向二_第20张图片

上来的xxxLibInit我们说过了是初始化函数库的,之后是qInit(包括workQInit),全称是queue init,也就是队列的初始化,详细的我们上面已经讲过了。

void kernelInit

(

    FUNCPTR rootRtn,            /* 用户启动例程 */

    unsigned  rootMemSize,             /*给 TCB 和初始任务栈分配的内存 */

    char *       pMemPoolStart,      /* 内存池的起始地址 */

    char *       pMemPoolEnd,         /* 内存池的结束地址 */

    unsigned  intStackSize,    /* 中断栈大小 */

    int              lockOutLevel    /* 关中断级别 (1-7) */

)

主要就是创建并执行了一个任务,同时设置了该任务的TCB(thread control block,保存线程的相关信息)、栈、内存池等等。这里创建的任务就是usrRoot,分配的内存池起始地址为(sysMemTop – end)/16,即内存空间的十六分之一用来存储,中断级别为0即禁止任何形式的中断。

而bootram的usrKernelInit函数基本一致,只不过将kernelInit放到了外面。

  • usrRoot,不知道大家有没有注意到,从usrInit到usrRoot,是以任务创建的方式进行的,也就是说,没有返回值,在进一步说,从这一步开始,上下文就变了,之前可以说是活在”石器时代“,一堆函数所做的只不过是在”石器时代“进行操作罢了,而现在正式进入”文明时代“了。

工控安全PLC固件逆向二_第21张图片

如图为反编译的Vxworks的usrRoot。

工控安全PLC固件逆向二_第22张图片

首先开始是usrKernelCoreInit,具体如上图所示,主要作用是对一些功能进行初始化,sem开头的代表信号量,wd则是看门狗(watch dog,简单来说就是监测系统有没有严重到无法恢复的错误,有的话将系统重启)的意思,msgQ则是消息队列(关于消息的内容在《Windows调试艺术》中也见过了,实质相同),taskHook则是和hook相关的内容。

接着调用memInit来初始化系统内存堆,这里开始我们就可以使用malloc和free函数了。往后到了非常重要的一个环节,也就是sysClkInit函数,它是用来对时钟进行初始化的,而时钟就肯定要涉及到时钟的中断处理(我们在上面说了 sysHwInit等一系列并没有真正完成硬件设备的中断处理注册工作,现在我们的时钟还不能正常的工作),我们将升入去研究它。

工控安全PLC固件逆向二_第23张图片

首先是sysClkConnect函数,它以usrClock作为参数,很可疑,有没有可能usrClock就是我们要找的中断处理例程呢?我们深入去看。

工控安全PLC固件逆向二_第24张图片

可以看到,usrClock这个函数只是被放在内存的某个位置,似乎和中断没挂钩,反倒是出现了sysHwInit2,不得不让人和上面的sysHwInit产生联系,我们一步步深入该函数。

工控安全PLC固件逆向二_第25张图片

最终我们发现了intConnect,它将ppc860Int注册为时钟中断处理例程,这里实际上和我找到的Vxworks的源码并不相同,在源码中intConnect会将sysClkInt注册为中断处理例程,然后在sysClkInt去调用usrClock,最后再去执行usrClock的tickAnnounce函数进行具体的任务调度,这里不知道是不是施耐德的固件对于具体需要进行了调整,还是说又是Ghidra的一个分析bug。

回到usrRoot,之后调用usrIosCoreInit,进入函数发现iosInit,这是Vxworks的io子系统,之后我们会具体讲解,这里只看看它初始化了啥。

工控安全PLC固件逆向二_第26张图片

iosInit的参数有三个,第一个是支持的最大驱动数,第二个函数是系统最多同时打开的文件数,第三个则是一个特殊的文件,所有写入它的内容都会无效(Linux也有类似的文件)。

继续深入就到了usrSerialInit。

 工控安全PLC固件逆向二_第27张图片

这个函数有些难理解,为了方便大家查看,我将一些变量进行了重命名。

首先是tyname为0,也就是为空了,然后将tyname(空的)和/tyCo/连接起来,实际上就是tyCo,然后调用了一个未解析出来的函数,实际上就是将ix的值变为字符串,再将其拼接到tyname上,再加上ix<1的循环,也就是说此时就有了/tyCo/1和/tyCo/0两个字符串,这个名字实际上就代表了串口,也就是说该plc有两个串口。

对于这两个设备,首先使用ttyDevCreate来创建设备,这里听着不太通顺,实际上是Vxworks的一个特点,虽然你实际上已经有这个串口设备了,但是对于系统来说,并不知道,需要你基于它调用xxxDevCreate来”注册“,关于这个的详细说明会在后面的文章中涉及。注册后会判断ix是否为0,也就是对于/tyCo/0在进行操作,调用ioctl函数来对该串口设备进行操作。

ioctl和linux的ioctl相似,都是因为传统的open、read、write等基本操作都是将设备抽象成文件来进行(linux崇尚万物皆文件嘛)的,对于设备独有的操作(比如光驱的弹出等等)就无法完成了,所以有了该函数。

ioctl(int fd,int function,int arg)

fd即为open设备后返回的文件标识符(Vxworks中经常叫做consoleFd,别和控制台搞混了……),function则是要进行的操作,arg是操作需要的参数,大家可以在ioLib.h中找到,如下图所示。

工控安全PLC固件逆向二_第28张图片

最后跳出循环,注意,此时consoleFd为/tyCo/1的fd,利用ioGlobalStdSet标准对输入、标准输出、错误输出进行重定向,此时,我们的printf之类的函数就可以用了。

再跳回到usrRoot,剩下的初始化的函数我们就不看了(有兴趣的可以自己看看),只关心一下usrNetworkInit,为啥呢?因为这有个漏洞,也就是很有名的CVE-2011-4859,这个版本的固件还未修复,我们在下一篇文章会详细分析这部分的内容。

最终由usrRoot调用usrAppInit,我们总算是来到了所谓的”main“。

同样的,对比bootram系统,初始化方面相同,而这些完成之后,bootram也就开始将Vxworks加载到内存中,开始将工作托付给Vxworks,启动也就完成了。

可以看到,plc固件的逆向涉及到了非常非常多的新知识,由于篇幅所限,这次我们仅仅是完成了最基本的工作,接下来我们会一点点深入,研究Vxworks在网络方面的初始化以及CVE-2011-4859,并着手开始逆向固件的”main“函数,直到彻底吃透该固件。

你可能感兴趣的:(安全)