工控安全入門(九)——工控協議逆向初探


在工控領域,我們會遇到許多協議,為了進行安全研究,經常需要對協議的具體內容進行探索,今天我們就來聊聊關於工控協議逆向的問題。

在接下來的幾篇文章中,我會簡單介紹一下常用的協議逆向方法並配合一些實戰,當然,從未知到已知的探索過程不僅僅需要代碼上的實踐,還需要數學上的分析與建模,所以在這幾篇文章中不僅會有工控、協議的知識,還有大量的數學內容,因為我本身不是搞學術研究的,所以一些東西也只是略微了解而已,如果大佬們發現有什么錯誤請在評論中指出,我一定仔細查看。還要感謝@bitpeach大佬的文章讓我了解到了很多知識。

按照分類,工控協議一般可以分為以下兩種:

  • 公開協議,這里的公開主要是說它是公開發表並且無版權要求的,我們介紹的modbus就屬於這一種。
  • 私有協議,顧名思義就是廠家自有的,為正式公開的,我們介紹過的西門子S7comm就屬於這一種。

但不論是公開協議還是私有協議往往都具有一定的未知性。像是modbus,雖說大部分信息我們都是了解的,但是還有很多function code是廠商自己偷偷用的,像是施耐德我們之前就提過,有自己的0x5a來實現一堆高權限操作;S7comm這類的私有協議就更不用說了,要不是前輩們的逆向工程,我們其實是對協議內容一無所知的。我們今天要聊的就是對於這類未知的逆向過程。

對於協議的逆向我們也是分成兩類方法:

  • 基於網絡軌跡的逆向,即對抓取的流量包進行分析,利用各類數學分析、推理,對數據進行切分、關系預測、生成狀態機等等,從而推斷出協議的部分內容、進行fuzz等操作。
  • 基於接收端程序的逆向,即對協議數據的接收端程序進行逆向分析,從而得到協議的內容,這也是現在常用的方法,像是最近S7commPlus的逆向就是借助分析上位機的OMSp_core_managed.dll組件來實現的。

當然,這兩種逆向方式都需要結合相應的設備進行調試來完成,也完全可以兩種方式結合,先基於網絡軌跡大致判斷協議格式、關系,再通過逆向程序加以完善。這篇文章我們就首先看看基於網絡軌跡的逆向。

 

基於網絡軌跡的逆向

考慮到我們為入門教程,所以我們選用的數據包並不是真正的”未知“協議,我們使用系列第一篇文章的modbus數據包來進行分析,我們將其TCP以上均視為未知部分

如圖的包,我們將modbus部分的數據視作

x00x00x00x00x00x04x00Zx00x02 

現在看上去還是毫無頭緒?沒關系,我們一點點來

確定協議字段的基本知識

確定協議字段說白了就是根據流量包中的大量流量對比,”猜“出來哪些數據應該是一個字段,這個過程中涉及到協議分析算法,而這些算法又是由一些重要的數學模型、算法構成的,我選取了《Network Protocol Analysis using Bioinformatics Algorithms》、《基於網絡協議逆向分析的遠程控制木馬漏洞挖掘》兩篇論文中的某些部分進行簡要說明,來大致了解一下這部分理論知識(因為我們是要做實際分析,所以涉及到研究部分的算法等理論我就不再細化了)。

LD(LevenShtein Distance),假設我們有A、B兩個字符串,A經過插入、刪除、替換字符的最短過程變為B,經過的步驟表示兩個字符串的差異。如:

A = "modbus" B = "modicon" 

顯然我們需要把子串”bus“換為”ico“,然后添加字符”n“,所以LD(A,B)=4,python實現代碼如下:

def normal_leven(str1, str2): len_str1 = len(str1) + 1 len_str2 = len(str2) + 1 # 創建矩陣 matrix = [0 for n in range(len_str1 * len_str2)] # init x軸 for i in range(len_str1): matrix[i] = i # init y軸 for j in range(0, len(matrix), len_str1): if j % len_str1 == 0: matrix[j] = j // len_str1 for i in range(1, len_str1): for j in range(1, len_str2): if str1[i - 1] == str2[j - 1]: cost = 0 else: cost = 1 # 若ai=bj,則LD(i,j)=LD(i-1,j-1) 取矩陣對角的值 # #若ai≠bj,則LD(i,j)=Min(LD(i-1,j-1),LD(i-1,j),LD(i,j-1))+1 在對角,左邊,上邊,取最小值+1 matrix[j * len_str1 + i] = min(matrix[(j - 1) * len_str1 + i] + 1, matrix[j * len_str1 + (i - 1)] + 1,matrix[(j - 1) * len_str1 + (i - 1)] + cost) return matrix[-1] str1 = '' str2 = '' a = normal_leven(str1, str2) print(1-a/max(len(str1), len(str2))) print(type(1-a/max(len(str1), len(str2))) 

LCS(Longest common subsequence),這個和大家理解的兩個字符串求最大子序列有些不同,這里的字符並不一定要連續出現。如:A與B的最長子序列為”mod“,所以LCS(A,B)=3,而我們將A變為”m o d b u s“,LCS(A,B)仍然為3,所以,如果LCS(X,Y)=0,那么立即推,X、Y沒有任何一個字符相同。在論文中包括了GSA(全局序列對比)與LSA(局部序列對比),GSA用在對協議域的理解上,而LCS則是對相似序列進行聚類。編程中,我們可以使用Needleman/Wunsch算法來求解。我們還是以上面的兩個字符串為例

    m o d b u s
  0 0 0 0 0 0 0
m 0            
o 0            
d 0            
i 0            
c 0            
o 0            
n 0          

首先我們將矩陣初始化,即上述表格,接着按照公式進行填充

若ai=bj,則LCS(i,j)=LCS(i-1,j-1)+1

若ai≠bj,則LCS(i,j)=Max(LCS(i-1,j-1),LCS(i-1,j),LCS(i,j-1))

該公式其實非常簡單,如果行列字符一樣,則填充的值為它左上角的值加1,如果不一樣就是左上角、上邊、左邊的最大值。按照這個標准我們從第一行開始填,得到如下結果

    m o d b u s
  0 0 0 0 0 0 0
m 0 1 1 1 1 1 1
o 0 1 2 2 2 2 2
d 0 1 2 3 3 3 3
i 0 1 2 3 3 3 3
c 0 1 2 3 3 3 3
o 0 1 2 3 3 3 3
n 0 1 2 3 3 3 3

然后從右下角進行回溯操作,若ai=bj,則走左上角;若ai≠bj,則到左上角、上邊、左邊中值最大的單元格,相同的話優先級按照左上角、上邊、左邊的順序,直到左上角為止。最終按照如下規則寫出表達式(_表示插入字符或者是刪除字符操作)

  • 若走左上角單元格,A+=ai,B+=bi
  • 若走到上邊單元格,A+=ai,B+=_
  • 若走到左邊單元格,A+=_,B+=bi

多序列對比,實際上是LCS的擴展,采用引導樹、非加權成對群算術平均法等來進行,非加權成對群算術平均法即UPGMA算法,這是一種聚類算法,在我們前面已經得到的距離的基礎上進行操作,將距離最小的兩個節點進行聚合,然后再次計算新的節點間的距離,最終生成演化樹。

演化樹(Phylogenetic tree),在多序列對比中生成的樹,其實就像是生物進化圖那樣,由根衍生出一個一個節點,如圖所示為DNA系統的演化樹,我們所要建立的演化樹是關於協議的數據的,通過這種方式來尋找協議流量中的相似部分。

建樹的方法主要有兩大類,一類是基於距離的,一類的基於character的。我們上面提到的就是基於距離的建樹方式,一般來講,基於距離的方法將數據抽象為距離,從而有了較快的處理速度,但是其抽象過程中會有信息量的損失;而基於character的方法是在一個已有的模型上建立的,所以需要有一個靠譜的模型,但好處就是我們的信息不會丟失。

廣義后綴樹(Generalized Suffix Tree),用來實現求解最大公共子串、匹配字符串、找重復串等等,這里說的最大公共子串就是我們平時理解的:連續的、相同的字符串。比如”modbus“和”m od i c o n“,那么這倆的最大公共子串就是od而不是上面的”mod“。該技術常用於病毒的特征碼提取。下面舉個栗子來解釋一下:

假設我們現在有modbus、modicon兩個字符串,對於modbus,后綴有:

s
us
bus
dbus
obus
modbus

我們將其按字典序排序,然后建立樹,根節為空,每個字母為一個節點,從根到葉子就對應了一個單詞,如圖所示

我們還可以對單節點鏈條進行壓縮,當然這個單詞所有的鏈條都是單節點的……然后我們將modicon也按后綴,然后插入到這個樹中,重合的部分即為公共子串(因為我不知道怎么畫圖,所以你們就自行想象一下吧)。

聚類(cluster),大白話就是分類,一是用來對流量進行粗略分類,二是用來對提取的字符串進行分類。具體涉及的包括了k緊鄰算法、關鍵詞樹算法等等

以上是一些基礎性的問題,有了以上的基礎,我們可以大致將流量包的分析歸為以下幾步:

  • 粗略聚類,提取主要分析的流量,並將相似的流量首先分到一起
  • 采用各類算法來對字段進行划分
  • 根據某些字段再次進行聚類
  • 對一類的流量進行關系分析

Netzob划分數據

netzob是一種基於網絡軌跡的逆向工具,目的就是為了分析未知協議,當然還有其他的一些,比如PI等等,我們這里就以netzob為例進行操作。

首先當然是要安裝,netzob需要大量的前置包,安裝很麻煩,很有可能遇到各種錯誤,因為和實際環境有關,所以我也沒法全部列舉出來,大家安裝時自行嘗試吧

apt-get install python-dev      #提前需要安裝的庫
apt-get install python-impacket
apt-get install python-setuptools
apt-get install libxml2-dev
apt-get install libxslt-dev
apt-get install gtk3
apt-get install graphviz
git clone https://github.com/netzob/netzob.git
cd netzob
python3 setup.py develop --user  #開發者友好模式

以上步驟完成后我們就可以在python3中import了

from netzob.all import * 

當然也可以python3 setup.py install來安裝友好的圖形化界面,使用./netzob即可打開,但是在我的機器上出現了問題,大家可以自行嘗試。如果實在是安裝不成功的同學,官方也給了docker鏡像

docker pull netzob/netzob
docker run --rm -it -v $(pwd):/data netzob/netzob  #pwd為當前位置,掛載到根目錄下的data

搞定后我們就可以開始干活了

m = PCAPImporter.readFile("modbus.pcap").values() 

該條語句用來導入我們的流量包,我們可以查看一下它的說明

參數主要關注兩個,一個是importLayer,這是指定我們要分析的data是在哪一層,以我們modbus來說,是基於tcp的,所以data就相當於是tcp往上一層,所以填5,如果是S7comm呢則要考慮你要分析哪一層再進行選擇;另一個是bpfFilter(Berkeley Packet Filter),也即是伯克利包過濾的意思,這是一種語法,可以指定你要選擇哪些流量,如下所示:

host 0.0.0.0 and (port 138) #篩選出ip為0.0.0.0且端口為138的流量包

接着我們進行符號化,即篩選出所有相似的流量,這里就涉及到了我們之前提到的數學知識

s = Symbol(messages = m)

可以看到提取出來的就是相似的流量就是我們流量包中的modbus部分,這一步就相當於我們上面提到的粗略聚類,Netzob將modbus部分的流量提取了出來,並將這些流量放到了一起。但是現在我們還是啥也看不出來,我們希望能夠對data再次進行分析,對比得到哪些字符應該是一塊的,哪些是分開的。

Format.splitStatic(s)

該方法用來將我們的data根據相似性與靜態分布規律,划分為幾個Field,當然,我們也可以通過“肉眼觀察法”使用splitDelimiter(symbol,ASCII(“Z”)來進行人工的划分。這一步相當於“采用各類算法來對字段進行划分”,也是核心部分,如圖即為划分Field后的symbol

我們就以第一組為例,打開wireshark來檢查一下分析的結果

真正的划分如下:

'x00x00' | 'x00x00' | 'x00x04' | 'x00Z' | 'x00x02' 

可以看到差距較大,觀察流量包后發現,主要原因是因為modbus的前四個字段在該流量包中區分不太“明顯“,以第一個字段舉例,modbus用了兩個字節表示事務標識符(即Transaction identifier ),但在這些流量中最多最多就是x00x01,根本就沒有用到低字節,所以在划分時被認為是兩個字段了。同理,長度字段也是如此,該流量包中的length也沒有用到低字節,所以也被划為了兩個字段。

知道了情況我們就可以對症下葯了,一是我們引入新的流量包,選取一些數據量較大、情況足夠全面的流量,可以稍加完善;二是我們通過不斷的嘗試和日常積累進行手動划分,比如每種協議基本都有的length字段、標識字段等等進行手動划分。但是由於協議本身的規定與限制(比如,雖然給了兩個字節,但是實際上並沒有使用低字節,廠商只是為了擴展或者對齊),我們只可能完善,但絕不可能完美划分字段,不過僅僅是這樣對我們的幫助已經很大了。

之后我們要進行的步驟為“根據某些字段再次進行聚類”,但是這里由於我們對於哪個是關鍵字段並不清楚,所以暫時放棄這一步。

接下來我們要做的工作就是要猜測字段的含義,當然我們通過這種方式絕對不可能“猜”出來“Z”是施耐德專用的功能碼,我們能做的是推理這些字段之間的關系。

for symbol in symbols.values(): rels = RelationFinder.findOnSymbol(symbol) print("[+] Relations found: ") for rel in rels: print(" " + rel["relation_type"] + ", between '" + rel["x_attribute"] + "' of:") print(" " + str('-'.join([f.name for f in rel["x_fields"]]))) p = [v.getValues()[:] for v in rel["x_fields"]] print(" " + str(p)) print(" " + "and '" + rel["y_attribute"] + "' of:") print(" " + str('-'.join([f.name for f in rel["y_fields"]]))) p = [v.getValues()[:] for v in rel["y_fields"]] print(" " + str(p)) 

RelationFinder為我們提供了不同的關系分析方法,這里我們選擇使用基於符號的分析方法,也就是對我們之前進行過Field划分的符號進行分析。

當然,這只能探索符號內部的關系,像是我們的數據包,我們只能發現length字段,但是已經是非常大的進步。為我們之后進行測試、逆向程序都省下了不少功夫。

 

總結

雖然說了不少東西,但大多還是以理論為主,實際上代碼就撩撩幾行,當然這也是我們協議分析的第一步,在之后的文章中我們將基於這部分內容,繼續進行探索。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM