其他分享
首页 > 其他分享> > 工控安全入门(六)——逆向角度看Vxworks

工控安全入门(六)——逆向角度看Vxworks

作者:互联网

上一篇文章中我们对于固件进行了简单的分析,这一篇我们将会补充一些Vxworks的知识,同时继续升入研究固件内容。

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

本篇为工控安全入门(五)—— plc逆向初探的延伸。

 

什么是 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,启动也就完成了。

 

总结

本篇文章中介绍了Vxworks的任务机制,并将固件的初始化方面的函数进行了简单的分析,下一篇我们会研究Vxworks在网络方面的初始化以及CVE-2011-4859,并着手开始逆向固件的”main“函数。

标签:初始化,函数,逆向,int,工控,Vxworks,任务,bootram
来源: https://www.cnblogs.com/nongchaoer/p/11975984.html