工控安全入门(六)——逆向角度看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“函數。
總結
以上是生活随笔為你收集整理的工控安全入门(六)——逆向角度看Vxworks的全部內容,希望文章能夠幫你解決所遇到的問題。