本文系原創,轉載請說明出處
Please Subscribe Wechat Official Account:信安科研人,獲取更多的原創安全資訊
原始碼:
原文:
作者及團隊:
紐約大學的momalab團隊,自2019年起,這個團隊一共產出四個工作,每個工作都發表在體系結構和網路安全的頂會上:
[1] A. Keliris and M. Maniatakos. 「ICSREF: A Framework for Automated Reverse Engineering of Industrial Control Systems Binaries」. In: Network and Distributed System Security Symposium (NDSS). 2019
Github:
[2] D. Tychalas and M. Maniatakos. 「IFFSET: in-field fuzzing of industrial control systems using system emulation」. In: IEEE Design, Automation & Test in Europe Conference & Exhibition (DATE). 2020, pp. 662–665
Video:
Github:
[3] D. Tychalas, H. Benkraouda, and M. Maniatakos. 「ICSFuzz: Manipulating I/Os and Repurposing Binary Code to Enable Instrumented Fuzzing in ICS Control Applications」. In: USENIX Security. 2021
Video:
Github:
[4] Rajput, P. H. N., Doumanidis, C., & Maniatakos, M. 「ICSPatch: Automated Vulnerability Localization and Non-Intrusive Hotpatching in Industrial Control Systems using Data Dependence Graphs.」. In: USENIX Security. 2023
Github:
模糊測試是一種自動化
•考慮到PLC控制應用程式的二進位制檔案是使用專有編譯器從高階PLC程式語言編譯而來的,是否會引入安全漏洞?
•PLC二控制應用程式的進位制檔案由專有的runtime載入,作為PLC作業系統的一個程序執行。考慮到這些runtime是從常規的C/ c++原始碼編譯的,runtime有多易受攻擊?
•考慮到PLC控制應用程式的二進位制檔案的執行受實時約束和大量使用GPIO,Fuzzing可以用來發現潛在的漏洞嗎?
注:下文說的runtime都是指codesys runtime
runtime是什麼:是一個自包含elf的二進位制檔案,駐留在linux系統的/usr/bin中,通過包裝器程序部署,作為OS初始化的一部分,包含在etc/init.d的引導指令碼中。
包裝器程序(Wrapper Process)是一種執行在作業系統上的程序,它的主要作用是將外部請求傳遞給內部程序或服務,並將其響應返回給請求方。包裝器程序通常用於隔離和保護內部程序或服務,從而提高系統的安全性和穩定性。
PLC的Runtime模組通常包括以下幾個功能:
掃描迴圈(Scan Cycle):PLC的掃描迴圈是指PLC控制器在一個迴圈中完成對所有輸入、輸出和程式的掃描所需的時間。在每個迴圈中,PLC會對輸入訊號進行取樣、判斷和處理,然後根據程式的要求輸出相應的控制訊號。
任務排程(Task Scheduling):任務排程是指PLC控制器在掃描迴圈中對程式的執行順序和優先順序進行管理和控制。PLC的任務排程機制通常採用優先順序、時間片和中斷等方式,以實現程式的同步和非同步執行。
資料通訊(Data Communication):資料通訊是指PLC控制器與其他裝置之間進行資料交換和通訊的過程。PLC的資料通訊機制通常包括資料採集、資料儲存、資料處理和資料輸出等過程,以實現與其他裝置的資料交換和通訊。
錯誤處理(Error Handling):錯誤處理是指PLC控制器在執行過程中遇到異常情況時,如輸入訊號異常、程式執行錯誤等,能夠及時進行處理和報警。PLC的錯誤處理機制通常包括錯誤診斷、錯誤恢復和錯誤報警等功能。
控制應用程式如何載入與執行:
控制應用程式載入過程開始於runtime的初始化期間,通過一個file-open的系統呼叫到應用程式的二進位制檔案所在的寫死資料夾位置。
然後runtime開始在記憶體中複製控制應用程式程式碼和資料。
在控制應用程式檔案完成記憶體載入之後,開始執行,由一組基於pthread API的自定義函數處理。
控制應用程式二進位制程式碼被推到一個新範例化的執行緒的堆疊中,強制在所有主程序執行緒的堆疊段上啟用執行特權(這個操作可以是任意程式碼執行的主要推動因素)。
除了控制應用程式的載入/執行之外,runtime還會生成另外兩個與控制應用程式本身相關的執行緒,一個是KBUS_CYCLE,它充當控制應用程式和KBUS之間的中介,另一個是VISU,它根據嵌入到原始碼中的資訊提供控制過程的視覺化。
下表列出了在runtime程序執行的時候,與之互動關係最活躍的幾個函數:
比較重要的一個函數
KBUS: 是一個輕量級的程序間通訊系統,用於在runtime執行緒之間傳遞資料,更重要的是,它處理從GPIO埠到控制應用程式本身的控制應用程式資料。
控制應用程式的格式:控制應用程式二進位制檔案與傳統的計算機程式二進位制檔案相似,由標頭檔案、主程式、資料段和靜態和動態連結的庫組成。
ICSREF裡面給出了這個二進位制檔案的具體格式
控制二進位制檔案包含:控制二進位制檔案還包含從庫和使用者定義的F/FB呼叫函數或函數塊(F/FB)。這兩者都是靜態連結的,並以兩個連續子例程的格式包含在二進位制檔案中。第一個包含表示F/FB功能的指令,第二個初始化它的本地記憶體。接下來,將PLC (PLC_PRG)的主要功能封裝到下一個子程式中。這個子例程是最有趣的元件,因為它包含了控制邏輯。控制二進位制檔案中的動態連結函數通過位於最後一個程式碼子例程之後的符號表解析。符號表包含兩個位元組的資料,runtime使用它們計算呼叫相應函數所需的跳轉偏移量。
梯形圖(LD):(圖形化)這種語言類似於電路,取代了硬接線繼電器控制系統。
功能框圖(FBD):(圖形)FBD也是基於連線功能塊(FB)的接線圖。
結構化文字(ST):(基於文字)該語言是最接近高階計算機程式語言的語言,基於Pascal。
下圖視覺化了三種語言的差距:
(1)作者使用差異工具(例如vbindiff)進行的初始自動分析表明,不同的語言會產生不同的二進位制檔案。
(2)進一步的研究發現,差異的主要來源是編譯器為不同的PLC語言插入了數量可變的無操作指令(NOPs)。
NOP有很多種的變體,其中大多數使用的為典型指令(例如mov r0, r0)。
NOP指令在嵌入式系統中的主要作用是用於調整程式執行的時間,或者在一些情況下起到預留位置的作用。下面是一些常見的用途:
調整程式執行時間:在某些情況下,需要調整程式的執行時間,以便使不同的硬體部件能夠協調工作。NOP指令可以用來延遲程式執行的時間,以便確保硬體能夠在正確的時間進行操作。
預留位置:在一些情況下,程式需要保留一些空白的指令,以便在將來填充其他指令。在這種情況下,NOP指令可以充當預留位置,佔據一個指令位置,使程式保持完整性。
偵錯:NOP指令還可以用於偵錯嵌入式系統中的程式。在偵錯時,可以在程式中插入NOP指令,以便在特定的時間點停止程式執行,並允許開發人員檢查程式狀態和變數值。
總之,NOP指令在嵌入式系統中的作用是非常靈活的,它可以在程式的執行中起到不同的作用,具體取決於系統的需求和開發人員的意圖。
作者通過將C/C++和PLC程式設計環境中的脆弱函數相比較,以確定PLC控制應用程式程式語言是否具有記憶體安全問題。
(1)作者發現PLC程式也允許通過指標操作記憶體。
表2顯示分析的函數列表中的第二列中列出的標準程式語言(如C++)中的字串操作都存在潛在的安全漏洞。
而Codesys標準庫包括一個函數陣列,包括SysStrCpy (SysLibStr庫),Concat(標準庫),以及SysMemCpy, SysMemMove, SysMemSet,和SysMemCmp,都是Codesys SysLibMem庫的一部分。
(2)其中可能存在漏洞的函數如:SysMemCpy和SysMemMove不比較源緩衝區和目標緩衝區的大小,因此容易導致潛在的崩潰導致緩衝區溢位。
1 測試用例執行導致的系統崩潰無法反饋
2 輸入無法通過常規的方式(如stdio)傳遞到PLC控制應用程式中
3 輸入交付難同步
4 難插樁以獲取覆蓋率資訊
控制應用程式的二進位制檔案不遵循傳統的二進位制執行檔案,有其自己的執行機制,這種特點導致:
1、不能呼叫execve這種直接的系統呼叫來執行二進位制檔案
2、測試用例執行錯誤將無反饋
3、輸入(如一些訊號值)不能通過傳統的方式傳到控制應用程式中
4、由於控制二進位制檔案執行須遵循PLC的Scan Cycle,fuzzing的輸入傳遞不容易同步
5、無法將插樁應用於二進位制檔案
fuzzing可以大致分為兩個部分:
輸入生成和執行控制。執行控制部分與PLC系統通訊,並執行輸入的測試用例,同時接收PLC系統發來的正常和異常的訊號;輸入生成產生大量的變異的輸入至執行起的二進位制檔案,以讓系統產生崩潰狀態。
(1)非同步和同步控制應用程式
控制應用程式按照執行過程可以分為非同步和同步兩類:
首先,什麼是同步什麼是非同步?
同步操作是指程式執行任務時,會等待當前任務完成之後再進行下一個任務,這種方式需要一直等待任務完成,才能繼續執行下一個任務,因此在等待任務完成的過程中,程式的執行會阻塞。
非同步操作是指程式執行任務時,不需要等待當前任務完成,可以在任務執行的同時,執行其他任務。程式會在執行任務時立即返回,繼續執行其他任務,當任務完成後,會通過回撥函數或者其他機制通知程式任務已經完成,程式會再次執行相關程式碼。
舉例來說,同步操作就像你需要等待紅綠燈變綠才能通過路口一樣,而非同步操作就像你可以在等待紅綠燈的過程中,做其他的事情一樣。
下圖的左邊是同步,右邊是非同步
對於同步控制應用程式:
同步型別的控制應用程式二進位制遵循掃描週期模型,其週期性地檢查預定的記憶體對映地址來更新輸入,根據接收的值執行操作並寫入到相關的輸出地址。
可以從下圖中觀察到在一個processing cycle內,也就是上問題提到的掃描週期,PLC的控制應用程式從I/O Memory中讀取輸入值,經過符合控制應用程式的計算,最後將輸出值寫入I/O Memory中,更新輸出。
對於非同步控制應用程式:
典型的有羅克韋爾的ControlLogix L5控制器,與傳統的同步掃描架構的PLC不同,此類PLC採用與大多數現代計算機作業系統類似的方法如在每個任務之間切割處理時間來處理多個任務
具體可以看這篇:
總之,在管理員使用非同步的方式執行控制應用程式,當控制應用程式接收外部的訊號,將會更新輸入或者結束執行。
(2)可利用點
同步控制應用程式需要等待控制應用程式執行完畢才可以執行下一次,這對fuzzing技術來說是個效率災難,因為fuzzing需要將大量的測試用例輸入到PLC中儘可能的多次執行,以提高fuzzing的效率(這或許是傳統計算機軟體fuzzing和PLCfuzzing的不同點之一)。而非同步的控制應用程式不需要等待,等執行的非同步控制應用程式執行完了會返回一個結束訊號給系統,告訴系統我這裡執行完畢了,那麼這就意味著對fuzzing來說,可以用非同步的方式同時測試多個控制應用程式,巨幅提高測試效率。
(3)作者的方法
控制fuzzer(模糊測試工具)將測試用例傳送到PLC應用程式的輸入過程,以滿足指定時間尺度內的PLC任務執行週期。
為什麼不用非同步的方法?作者提到:非同步程式是PLC程式的特殊情況,佔可用二進位制檔案的一小部分,限制了這種方法的適用性。
(4)如何檢測到程式執行異常產生的終止
傳統的OS中,一個非同步的程式執行發生錯誤會呼叫SIGSEGV訊號來結束執行,並呼叫一個例外處理函數。
然而,控制應用程式執行執行緒對整個PLC作業系統而言是靜默的,因為上文提到這些控制應用程式執行執行緒由runtime程序控制,具體到runtime中的scheduler模組,這個模組決定控制應用程式過程的終止。
對整體作業系統而言,唯一可見的資訊就是futex系統呼叫。
futex(Fast Userspace Mutex)是一種系統呼叫,用於實現使用者空間程序之間的互斥鎖。futex主要用於程序之間的同步和通訊,可以用於實現多執行緒的互斥鎖和條件變數等功能。
在Linux作業系統中,futex系統呼叫包括兩個主要的函數:
futex():用於等待或喚醒一個futex。
futex_wait():等待futex的值發生變化。
在使用futex系統呼叫實現互斥鎖時,一般需要藉助其他的系統呼叫,如mmap()和munmap()等,來實現共用記憶體的操作,從而實現多個執行緒之間的資料共用和同步。
需要注意的是,futex系統呼叫在Linux作業系統中是一個比較底層的系統呼叫,使用起來需要一定的技術水平和經驗。在實際的應用中,一般使用高階的並行程式設計庫,如pthread等,來實現多執行緒的互斥和同步操作。
但是!控制應用程式的終止可以從父程序或者直接的祖宗程序監測到!也就是說可以用整出多個父程序或者祖宗程序來執行一個控制應用程式就行了!
上文提到,codesys runtime是由一個wrapper指令碼呼叫並啟動,那麼就可以fork多個wrapper程序來執行多個測試物件並接收對應的輸入,fork出一個wrapper程序後,用wait()方法掛起這個程序,只要這個程序的子程序收到一個PID終止訊號,那就終止並重新出實話runtime程序。
要知道,PLC控制應用程式的輸入一般是真實的物理訊號、模擬訊號、或者數位訊號,由一個專門的外圍I/O裝置or模組管理。本質上,這個I/O裝置or模組連線到感測器獲取訊號,通過標記為「GPIO」的LINUX裝置將訊號值傳遞給PLC,然後通過KBUS子系統與runtime進行通訊。然後每個掃描週期,KBUS將輸入的資料傳遞到控制應用程式。
整體流程見上圖介紹如下:
I/O模組接收來自感測器的訊號,通過GPIO轉發給PLC。
GPIO接收並儲存輸入資料在它們的記憶體對映空間。
KBUS開啟GPIO裝置檔案並執行讀取系統呼叫,將輸入資料移動到runtime程序中自己的記憶體空間中。
KBUS_CYCLE_TASK是與控制應用過程一起生成的執行緒,它通過write系統呼叫將輸入資料交付給控制過程的記憶體空間。基於控制應用程式的掃描週期長度,此事件可重複發生。
作者通過逆向工程和偵錯實現近似GPIO的功能,以在輸入資料處理方面模仿GPIO。然而,它與作業系統的互動與典型的GPIO不同:使用客製化的系統呼叫來中繼資料,而不是典型的讀/寫。
此外,根據I/O模組,資料可以逐位地傳遞到裝置,而I/O模組必須手動重構,這是一個非常不可靠的過程。 對於輸入互動來說,KBus是一個更有吸引力的選擇,因為輸入已經被接收並儲存為數位值。
KBus的地址空間可以通過在支援Linux的裝置中/proc/maps上獲得的記憶體對映資訊來提取。 KBus還被範例化為Linux檔案系統中的一個裝置,可以在/dev資料夾中存取,作為整個系統的通訊通道。
插樁是模糊測試一種反饋資訊的方法,插樁指在程式中插入幾個樁點,每當程式執行到一個樁點時,累加計數,除以總樁點就是覆蓋率。
上文提到控制應用程式有很多NOP指令,作者實驗發現這些NOP指令並不會影響程式的操作,而是跟控制應用程式的大小有關,程式越大那麼NOP的指令數量越多。
於是作者通過將NOP指令(下圖中的mov r0,r0)替換為右圖的STR指令,用PC暫存器計數,來獲取覆蓋率資訊。
上文說到,runtime本質上是個elf檔案,它通過將控制應用程式二進位制檔案生成為它的一個執行緒來載入和執行它。通常,當一個執行緒通過設定了CLONE_VM標誌的clone()系統呼叫生成時,任何使用mmap()執行的記憶體對映都會影響程序和執行緒。
說人話就是這兩個二進位制檔案(runtime和控制應用程式二進位制檔案)之間的相互依賴意味著它們的安全性在本質上是強相關的。
因此,分析runtime二進位制檔案和控制應用程式本身所採用的安全措施很重要。很簡單,使用checksec工具check一下發現如下,基本上傳統軟體的保護措施都沒開,除了codesys 3.x版本開了個NX
Runtime是一個複雜的應用程式,包括大量的實用函數,用於與環境互動、執行維護、處理控制應用程式並與之通訊。
Runtime應用程式不能被認為是一個獨立的軟體來fuzzing,因為它只能存在於runtime上下文中,共用相同的記憶體空間。許多函數和實用程式與控制元件應用程式有直接聯絡,並影響其執行狀態。
作者對任何與runtime相關的東西進行fuzzing的唯一可能性是通過動態連結的庫、Linux中的共用物件(.so)及其包含的函數。因為庫本身不是有效的執行目標,所以它必須託管在一個外部程式中,即一個測試工具,它會動態地載入庫並宣告一些包含的函數。程式碼必須短小精悍,以最大化效能並簡化分析崩潰範例時的偵錯工作,如下圖對KBUS傳送訊息的函數的fuzzing:
這個輸入引數是已知的,而對於那些未知輸入引數的函數庫,就無法繼續fuzzing。
•ICSFuzz利用KBUS子系統向控制應用程式提供輸入,因此需要使用物理PLC,使其不可延伸。
•如果測試程序與控制應用程式的掃描週期的同步丟失,ICSFuzz週期性地下降模糊輸入,因此很慢。
•ICSFuzz缺乏觀察控制程式狀態的自動化,涉及人工的崩潰監控。
•ICSFuzz還對WAGO PLC上的Codesys執行時的共用庫函數進行了有限的無狀態模糊,發現了一些崩潰。然而,由於fuzzing的無狀態和脫離上下文的性質,它錯過了需要執行時執行上下文的漏洞。