我們之前看過了法國施耐德的Modbus、德國西門子的S7comm,這次就讓我們把目光投到美洲,看看加拿大的HARRIS的DNP3有什么特別之處。
這次選用的流量包部分來自w3h的gitbub:
https://github.com/w3h/icsmaster/tree/master/pcap/dnp3
分析借助到的可執行文件和代碼來自:
https://code.google.com/archive/p/dnp3/
基礎知識
DNP3全稱是Distributed Network Protocol 3,分布式網絡協議3,這個協議在各種工業系統中都應用很多。它比起s7comm大刀闊斧做的協議棧要簡單的多,是完全基於TCP/IP的,只是修改了應用層(但比modbus的應用層要復雜得多),在應用層實現了對傳輸數據的分片、校驗、控制等諸多功能。
DNP借助TCP在以太網上運行,使用的端口是20000端口,
我們首先來看看最基本的結構,再去進行實際的流量包分析。這一部分可能會很枯燥,但只有徹底理清一遍DNP3的結構,才能幫我們更好的分析DNP3的相關流量包
可以看到,在應用層DNP3分為了幾個部分來處理,就像是在應用層是又搭建了一個協議棧一般。下面就讓我們細致的探索一下
首先是數據鏈路層,這個名字聽着有點出戲(OSI第二層的鏈路層一臉懵逼)……它主要是規定了傳輸的規則,在官方文檔上將這部分的數據單元稱為LPDU。
它的組成很簡單,主要就是以下幾部分:
- start bytes,表明數據開始的起始字節,固定為0x0564
- length,長度
- control,控制字,這是最重要的一個字段,用一個字節來進行標記
- 第一位,direction,聽着很高端,但其實就是表明發送的方向
- 第二位,primary,表示發送設備是主設備還是從設備
- 第三位,FCB,翻譯過來是“幀的計數位”,但實際上不是用來計數而是用來糾錯的。如果是response的話則為保留位
- 第四位,FCV,這一位說明FCB是否有效,在上圖中為0,就表示FCB未開啟。如果是response的話則為DFC,數據鏈路層是否發生緩沖區溢出
- 后四位,控制方法碼,這個碼可沒有S7comm的功能碼那么“變態”,它並不直接指示功能,只是對包進行分類而已,更像是Type,對於主設備來說
- 0,鏈路重置
- 1,進程重置
- 3,請求發送數據
- 4,直接發送數據
- 9,查詢當前鏈路的狀態
對於從設備來說
- 0,同意
- 1,拒絕
- 11,回應當前鏈路狀態
- Destination,目標地址,16位
- Source,源地址,也是16位
- checksum,校驗碼,DNP3采用的是CRC算法
到此數據鏈路層結束,接下來又冒出來個TC(Transport control),看這個名字就知道是要仿照OSI的傳輸層,不過這個比起上面可要簡單很多,實現的功能主要就是標注當前的包是第幾個包。
- 第一位是final,標識是否為最后一個包
- 第二位是first,標識是否為最后一個包
- 后六位為seq,表明當前是第幾個包
這里可以注意到一個小細節,本來六位標識的話,包的數量為2的6次方,也就是0到63,乍一看好像是一次最多拆分成這么多包,但是因為有first位、final位,只要final位不為1,那就可以一直傳啊!事實上也是如此,在真實情況中,傳到63后,下一個包會從0再繼續。所以這個設計可太巧妙了,乍一看是“浪費”了兩位,實際上反倒是徹底解除了位數的限制。
接下來到了DNP3的應用層的應用層,別覺得很怪,人家就是這么起的名字……
可以看到應用層結構還是比較清晰的,
主要有以下幾部分組成:
- control 控制字,按位分為以下:
- 第一位,first,表明是否為第一個
- 第二位,final,表明是否為最后一個
- 第三位,confirm,表明是否需要回復,圖中即表示不需要回復
- 第四位,unsolicited,表明是否為主動提出的
- 后四位,seq,為隊列號,這里的設計和上面傳輸層的類似,同樣可以支持大於2的四次方的包的數量
- function code,最最最最重要的字段,指明功能,圖中為0x01,即讀取數據,0x2就是寫數據,這里不再詳細說明,在具體的流量分析中我們再來細看
- read request data object,即要讀的數據對象,這里用了qualifier用來限制要讀取的對象是什么,個人覺得這里說是數據對象倒不如說是對數據對象的限制。這里wireshark的解析同樣有點問題,應該是object、qualifier、range各為一個字段,但wireshark把他們都套進去了,我們還是按分開的來分析。object,即對象,它主要有兩部分組成:
- Obj,即圖中的0x3c,這里是數據的基本類型,比如離散還是模擬
- var,即圖中的0x02,進一步說明數據的類型,比如是模擬的話,那你是32位還是64位
這里給大家一份可以查詢的表格,有需要時查表即可
到這就可以完整的說明數據的類型了,接下來就是限定詞,給出了一系列的限制。
- 第一位,保留,wireshark同樣沒有給出解析信息
- 三位,限定碼,這個不太好理解,簡單點說是表明一個數據對象的索引的字節數
- 后四位,range,也就是指定的范圍的意思,圖中6即為讀取所需類型的全部數據。往后就是最后的對象了,這個包中並沒有,之后會看到。
到這里我們就把DNP3的細節過了一遍,可以看到,DNP3在自己的應用層又搭建一個簡易的協議棧用來傳輸數據,讓他有了自己的數據檢測、數據分段功能,雖然有很多巧妙的設計帶來了傳輸的幫助,但是其略顯繁瑣的設計必定要也會導致性能的下降,同時我們也可以看到,它在安全性方面,實在是沒什么保障。
file_read
那么就開始看流量包吧,簡單的read write之類的就不再贅述,相信看了上面的描述之后你們絕對是沒問題的,讓我們來看幾個稍稍復雜一點點的。
不知道大家看到這種流量會不會覺得親切,我之前有門課是《網絡管理與服務設計》,配置CIFS和NFS時抓到的包都和這種類似,看着包多,但是邏輯很簡單,就像是小學生對話一樣……
最重要的參數顯然就是object了,這里指定了一堆東西,實際上咱們大部分都可以不管了,注意file control mode為read,也就是讀,而文件的最大塊的size為1024,最后的filename是重點,也就是說主設備發送了一個請求,想要打開testfile.txt文件
回應包的內容告訴主設備:“ok,我知道了,你可以打開,它的句柄是0x12345678”,句柄是啥相信不用我再提了吧。
主設備告訴從設備:“我要讀句柄為0x12345678的文件,而且從第一個block開始讀,我要讀到最后”,這里的file block即表明開始的位置,而last block就是是不是最后的塊,不設置的話就是一口氣讀到底。
從設備回復:“ok,最后的塊為第一個塊,文件的數據為XXXX”,這里從設備幫我們確定了文件的塊數,並將數據返回給我們,數據即為:This is a test file
主設備:“我想要關閉0x12345678句柄的文件”
從設備:“ok,已關閉”
到此讀文件的過程完畢,可以看到雖然看上去很繁瑣,但整體就是簡單的“對話”,比起s7comm來說相當容易分析了。write的過程類似,我們就只看write包吧
主設備:“我想往0x12345678句柄的文件里寫東西,開始的塊是0,就一個塊,內容是hi there”
file_list_directory
總體流量包很清晰,基本和read file一樣,都是打開文件、讀文件、關閉文件三部曲,但中間穿插了一些奇奇怪怪的東西,實際上就是DNP3對數據的分段處理,讓我們具體看看
主設備:“我想打開name為.的文件”,有Linux基礎的同學應該都知道這個name就是文件夾。
從設備:“ok,句柄為0x12345678”
主設備:“我要讀取句柄0x12345678的內容”
從設備:“有點多。。。你等等”
由於目錄的所包含的內容過多,所以並不能一次傳輸完成,所以這時就要借助傳輸層的機制來實現分段了
我們上面說過,傳輸層通過控制字對每一段進行划分,第一個包即為01XXXXXX
第二個包為00xxxxxx+1,如此就是實現了分段,而最后一個包變為10xxxxxx,那么主設備也知道了,哦,數據傳輸完了。
freeze
其實這樣的流量包特別特別簡單,但是主要是想提一下freeze這個操作的含義,request如下
可以看到沒什么特別的地方,還是簡單的指定了數據對象而已,再看看response就要復雜得多
里面有一堆參數,我們先不着急看,先搞明白什么是freeze
freeze翻譯成中文有凍結的意思,具體的操作是將指定的數據對象放到一個緩沖區(稱其為凍結緩沖區)中,但根據他的功能,我覺得叫做shotsnap或許更為合適,它有以下幾種類型:
- immediate Freeze,0x7,凍結,需要從設備給予回應
- immediate Freeze no ack,0x8,凍結,不需要回應
- Freeze&Clear ,0x9,凍結,刪除原來的數據(這才是真正的凍結吧,打入冷宮),需要從設備回應
- Freeze&Clear no ack,0x10,同上,但無需回應
- Freeze with Time,0x11,指定時間凍結
我們這里用的顯然是0x7的,所以不會對原來的數據造成損失,可以看到回復包中一堆的參數,表明的是從設備的“狀態”問題,包括像是設備是否重啟、設備是否有問題、時間同步等等,這里就不在一一說明了。
full_exchange
下面來看看這個流量包,開頭是TCP的三次握手,不再說了
第一個DNP就是個讓人懵圈的包,從下面很容易看出來1為主設備,130為從設備,怎么上來130就response了呢?其實這是一種特殊情況,我們可以打開包來具體
我們發現它自發的向主設備發送了一系列的狀態信息,而在這之中只有1位是1,重啟。那我們可以推測,設備應該是剛剛經歷了一次重啟,所以它在與主設備重新獲得聯系后趕緊發送了當前的狀態,雖然違背了request-response的規則,但是也不無道理,畢竟主設備在從設備重啟后的第一時間就需要知道從設備的相關信息。那我們同樣也可以猜測,這個function code很有可能就是從設備在特殊情況下自發傳輸某些數據。再結合官方文檔,我們就可以真正理解這個function,確實是自發上送數據。
接着就是正常的請求包了
function code為0x15,其實恰好上面,是禁止自發上送某些信息,這個也很好理解,畢竟從設備也不可能說是把數據都自主上傳吧,主設備肯定要做一些限制,所以這里就開始對class1 class2 class3進行限制,不允許對這些進行自發的上送。
從設備對該function進行了標准的response,接着主設備又發送了confirm表示確認(該function也是一種特殊情況,不需要從設備進行回復),接下是一個write的function
write是看明白了,寫的是0也看明白了,但是他寫了個啥?intenal indication?上網一搜,啥也沒有;谷歌翻譯,內部指示,一臉懵逼。
實際上這可不是什么內部指示,只不過是wireshark給的一個名詞而已,我們仔細向上翻,其實是能在response里找到的,就是我們之前說得很簡單的“狀態”,wireshark給的這個名字讓我們產生了誤解而已。
所以write就是在改寫從設備的“狀態”信息,看看response有什么變化
這次全都是0了,可以看到算是恢復了初始狀態。
接着進行read操作,讀取了相應的信息。最后使用0x15的相反功能,允許自發上送數據,允許從設備對class1、2、3進行上送操作,tcp四次揮手,該包完畢。
lan_time_sync
先看看整體的流量包
大家要是去查現成的資料的話可能會找不到這個Record Current Time的function說明,如果按照它的function code去查的話找到的可能是“未使用”三個大字。
實際上這是因為2017年DNP3進行了一次更新,加入了部分新的function,其中就有這個,實際上就是功能就是記錄當前的時間,response倒沒什么特別的,接下來是個write操作
它去寫的是時間和日期,傳送的方式是時間戳,非常簡單。
總結
這次總結了DNP3的相關細節,並分析了幾種實際的數據包來加強理解,當然作為一個協議,他還是很多很多沒有提到的地方,大家有興趣的可以繼續嘗試下去。