逆向工程是网络安全行业里面一项很重要的技术。
逆向工程
首先,我们解释下逆向工程是什么。逆向是一个相对正向而言的解释,相对正向来说,对一个程序来讲,正向就是开发的过程,从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包
逆向工程属于软件安全中的硬功夫,值得深入研究,但学习者通常也会觉得枯燥,难度大。
从业者在学习过程中,需要积累对目标程序足够多的知识。如果没有这些基础知识的支撑,那距离实际应用会差非常远。
之前我们学习了包括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固件,基本可以说是啥信息都没有。
直接binwalk -e NOE77101.bin提取即可,对分离出来的文件再一次binwalk就可以看到很多有用的信息了,包括固件采用的操作系统、内核版本、符号表等等信息。
至此我们成功提取除了固件,要注意的是,这里操作系统是vxworks,cpu则是big endian的PowerPC,所以接下来分析工作并不是一帆风顺的,还需要经历诸多磨炼。
手动修复与自动修复
手动修复
我们将固件拖入ida准备开始分析。
首先要修改处理器选项,因为ida并不能识别出来我们处理器的架构,我们需要将Processor type手动设置为PowerPC的big endian。
接着ida还会让我们指定RAM和ROM的起始地址,在我们不知道的情况下,只能直接点击ok,剩下的选项都直接默认即可。其实这里已经有成熟的工具可以用来自动化识别了,但是考虑到我们是第一次实践,还是认认真真的尝试着走一遍。
进入后发现ida的函数少得可怜(mac版的就干脆一个也没有,笔者这里是在windows上保存了idb后在mac上操作的),而且一般的ida不支持对PowerPC的反编译,就很难受。当然,再难受也得硬着头皮看,先找到第一个函数。
看不懂?没事,我们一点点扣。首先这里大量使用到了r1处理器,在PPC(后文中PowerPC都会简称为PPC)中共有32个通用寄存器,虽然叫通用,但有一些寄存器和x86一样被“固化”了,比如r1就是栈寄存器,其余“固化”的还有:
还有好多好多寄存器,我们贯彻用到哪说哪的原则,这里就不再提了,需要我们再说。
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,接着的减法实际上就是在栈上开辟了空间(栈是高地址向低地址增长的),这里就应该是整个固件的初始化函数的一部分。
再查看官方手册,我们发现,初始化的栈的地址也就是System Image的地址,所以我们ida最开始选择的RAM、ROM的地址就应该是0x10000。我们重新用ida打开该程序,设置RAM、ROM,就可以发现识别的函数多了很多了。
这一步之后我们需要符号进行还原,毕竟谁也不愿意看着一堆sub_xxx来分析吧?这里我们就要用到binwalk告诉我们的符号表地址了(忘了吗?是0x301e74),我们用010editor打开固件,16进制形式分析,跳转到对应地址。
我们之前已经知道了这是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,运行脚本即可。
耐心等待一段时间,即可完成。
是不是非常简单?下一步就可以开始进行分析了。
小试牛刀
首先我们跳转到之前简单分析的initStack(要注意之前是0x4c,但是我们把基址设置为0x10000后地址应该是0x1004c)。
可以看到在initStack操作后,进行了跳转,目标是usrInit,还记得参数的传递规则吗?没错,这里的r3就是为了传递参数的,可以看到,和我们之前说到的一样,还是使用lis和addi的搭配进行32位整数的传递。
当然其实再往上看我们还可以发现r4寄存器的身影,因为PowerPC不像x86的stdcall,能通过push来判断哪些是函数的参数,所以我们只能是把r3~r10的身影都锁定,宁可错杀,不可放过。(我们也可以通过查看后续函数的调用来猜测,不过也是猜测,还是多注意为好)。
这里可能有些人一看就慌神了,“我靠,这都不认识,怎么看啊?”,不慌不慌,其实这里的调用十分有规律,我们拆开看看。
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给我们的反汇编是什么样的。
它就分析出了三条,instructionSynchronize()实际上对应的指令是isync,也就是指令同步的意思,并不关键,TLBInvalidateAll()实际上对应的指令是tlbia,也就是快表的相关操作(快表不知道的去翻大二专业课《操作系统》吧,文章内实在没地方讲了),而最后就是函数调用了。
有没有发现?这反编译显然是把我们的mtspr给落下了,直接将r4当成了参数,导致usrInit有了俩参数!其实我们在刚才的探索中很明显看到r4在这绝对没有起到参数传递的功能。这就说明了对于PowerPC的反编译,ida干不了,但Ghrida干得也不是很好,所以在之后的分析中我们要时刻留意,绝对不能只看反编译结果。
接着我们调到usrInit函数来看看,还是先看汇编部分:
这部分是大家熟悉的函数序言,我们简单分析一下。
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并没有什么本质上的区别。
继续来看该函数:
又是一堆函数,那我们就先看看第一个吧,它以param1作为参数,而param1就是r3,前面分析了是0。
首先是bzero操作,参数是开始地址和结束地址,将中间部分都置为0,之后又这段地址将用来保存数据。
sysStartType是系统的启动类型,包括有bootram启动和rom启动,压缩式和非压缩式等等,这里受篇幅所限,先不细讲了。
intVecBaseSet是非常非常非常重要的一个函数,不知道大家看到intVec有没有想起来在《Windows调试艺术》中我们说过有个东西叫中断向量表(Interrupt Vector Table),其实就是那玩意,这个函数就是用来设置起始地址的,这里的起始地址固定为0。
返回到usrInit我们又看见下面还一个名字中带Vec的啊,那正好再来看看这个伙计:
是不是又觉得有点难了?别慌,慢慢来,首先是var2变成了地址,而后又大量使用var2的数组形式,那就让我们瞅瞅地址指向了啥。
哦,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的初始化工作。
由于涉及到操作系统的内容,建议大家在阅读本篇前有一定操作系统知识的基础,或者是阅读我的《Windows调试艺术》的文章简单了解诸如线程、中断、驱动等的知识。
什么是 Vxworks
Vxworks操作系统是由美国Wind River System 公司开发的一套实时操作系统,Vxworks比起linux,有更好的实时性和可裁切性,公司可以根据自己的需求去定制化Vxworks,我们逆向的固件就是施耐德在Vxworks上进行的二次开发。在国防安全、工业化方面Vxworks占据了半壁江山,甚至连爱国者导弹都与Vxworks有关,可见其在工控领域的地位,但是,由于Vxworks目前几乎是没有任何的”纯新手“入门书籍,所以学习起来就较为困难,但是如果想要深入学习或者进行二次开发的话,可以参考官方的相关手册。
Vxworks的任务
Vxworks作为RTOS(real time operating system)有一套独特的任务体制和调度方案来保证其实时性,在对固件进行进一步研究之前,我们有必要把这一部分内容搞清楚,保证后续工作的顺利。
在Vxworks中有四种任务队列:
在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
int taskCreate(
...
)
该函数参数与taskSpawn完全一致,返回的同样是tid,但是它创建的任务并没有做好运行的准备(也就是说没有进入ready队列),需要使用taskActivate来唤醒它。
STATUS taskDelete(int tid)
该函数用来删除一个任务,但是该函数非常非常危险,一般是不使用的。举个栗子,假设我们有一个扳手,现在有一个任务在用,后面还有几个任务在排队等待使用,但是突然你把人家delete了,就相当于连人带扳手都没了,但后面几个任务就傻眼了,成了无限等待了。
当然,关于的任务的函数还有很多很多,这里只是简单地说明,之后碰到了我们再去看。
Vxworks的启动
之前我们用到了sysStartType(系统启动类型)这个参数,但是由于篇幅所限没有详细说明,这里就先来看一下。
Vxworks简单来说有两种不同的启动方式:
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为例进行说明。
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也拿出来,放一块对比着学习。
首先是做了个死循环,检查两个变量的值,很显然值就是两个地址,实际上就是检查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的,如下图:
上来的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放到了外面。
如图为反编译的Vxworks的usrRoot。
首先开始是usrKernelCoreInit,具体如上图所示,主要作用是对一些功能进行初始化,sem开头的代表信号量,wd则是看门狗(watch dog,简单来说就是监测系统有没有严重到无法恢复的错误,有的话将系统重启)的意思,msgQ则是消息队列(关于消息的内容在《Windows调试艺术》中也见过了,实质相同),taskHook则是和hook相关的内容。
接着调用memInit来初始化系统内存堆,这里开始我们就可以使用malloc和free函数了。往后到了非常重要的一个环节,也就是sysClkInit函数,它是用来对时钟进行初始化的,而时钟就肯定要涉及到时钟的中断处理(我们在上面说了 sysHwInit等一系列并没有真正完成硬件设备的中断处理注册工作,现在我们的时钟还不能正常的工作),我们将升入去研究它。
首先是sysClkConnect函数,它以usrClock作为参数,很可疑,有没有可能usrClock就是我们要找的中断处理例程呢?我们深入去看。
可以看到,usrClock这个函数只是被放在内存的某个位置,似乎和中断没挂钩,反倒是出现了sysHwInit2,不得不让人和上面的sysHwInit产生联系,我们一步步深入该函数。
最终我们发现了intConnect,它将ppc860Int注册为时钟中断处理例程,这里实际上和我找到的Vxworks的源码并不相同,在源码中intConnect会将sysClkInt注册为中断处理例程,然后在sysClkInt去调用usrClock,最后再去执行usrClock的tickAnnounce函数进行具体的任务调度,这里不知道是不是施耐德的固件对于具体需要进行了调整,还是说又是Ghidra的一个分析bug。
回到usrRoot,之后调用usrIosCoreInit,进入函数发现iosInit,这是Vxworks的io子系统,之后我们会具体讲解,这里只看看它初始化了啥。
iosInit的参数有三个,第一个是支持的最大驱动数,第二个函数是系统最多同时打开的文件数,第三个则是一个特殊的文件,所有写入它的内容都会无效(Linux也有类似的文件)。
继续深入就到了usrSerialInit。
这个函数有些难理解,为了方便大家查看,我将一些变量进行了重命名。
首先是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中找到,如下图所示。
最后跳出循环,注意,此时consoleFd为/tyCo/1的fd,利用ioGlobalStdSet标准对输入、标准输出、错误输出进行重定向,此时,我们的printf之类的函数就可以用了。
再跳回到usrRoot,剩下的初始化的函数我们就不看了(有兴趣的可以自己看看),只关心一下usrNetworkInit,为啥呢?因为这有个漏洞,也就是很有名的CVE-2011-4859,这个版本的固件还未修复,我们在下一篇文章会详细分析这部分的内容。
最终由usrRoot调用usrAppInit,我们总算是来到了所谓的”main“。
同样的,对比bootram系统,初始化方面相同,而这些完成之后,bootram也就开始将Vxworks加载到内存中,开始将工作托付给Vxworks,启动也就完成了。
可以看到,plc固件的逆向涉及到了非常非常多的新知识,由于篇幅所限,这次我们仅仅是完成了最基本的工作,接下来我们会一点点深入,研究Vxworks在网络方面的初始化以及CVE-2011-4859,并着手开始逆向固件的”main“函数,直到彻底吃透该固件。