工控安全入门(六)——逆向角度看Vxworks
上一篇文章中我们对于固件进行了简单的分析,这一篇我们将会补充一些Vxworks的知识,同时继续升入研究固件内容。
由于涉及到操作系统的内容,建议大家在阅读本篇前有一定操作系统知识的基础,或者是阅读我的《Windows调试艺术》的文章简单了解诸如线程、中断、驱动等的知识。
本篇为工控安全入门(五)—— plc逆向初探的延伸。
什么是 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也拿出来,放一块对比着学习
首先是做了个死循环,检查两个变量的值,很显然值就是两个地址,实际上就是检查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放到了外面。
- usrRoot,不知道大家有没有注意到,从usrInit到usrRoot,是以任务创建的方式进行的,也就是说,没有返回值,在进一步说,从这一步开始,上下文就变了,之前可以说是活在”石器时代“,一堆函数所做的只不过是在”石器时代“进行操作罢了,而现在正式进入”文明时代“了。
如图为反编译的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,启动也就完成了。
总结
本篇文章中介绍了Vxworks的任务机制,并将固件的初始化方面的函数进行了简单的分析,下一篇我们会研究Vxworks在网络方面的初始化以及CVE-2011-4859,并着手开始逆向固件的”main“函数。