modbus基礎知識
modbus協議最初是由Modicon公司在1971年推出的全球第一款真正意義上用於工業現場的總線協議,最初是為了實現串行通信,運用在串口(如RS232、RS485等)傳輸上的,分為ModbusRTU、Modbus ASCII兩種,后來施耐德電氣將該公司收購,並在1997年推出了基於TCP/IP的Modbus TCP。現在使用最多的就是Modbus TCP了,我們今天的主角也是它。
Modbus作為一種通信協議,它和我們之前介紹的Zigbee有很大不同,Zigbee有自己完整的協議棧,而Modbus是一種應用層的報文傳輸協議,它既可以在物理層面上選擇串口進行簡單的串行通信,也可以使用TCP的方式進行傳輸。
上圖可以看到Modbus的協議棧僅僅是在傳統ISO/OSI模型的基礎上對數據鏈路層和應用層做了定義。也正是因為modbus是應用層的協議,所以它的安全漏洞並不只是它本身,TCP/IP的漏洞也可以利用在modbus上,最典型的就是18年工控比賽的題目,中間人。
modbus是一種主從協議,主設備的一方向從設備的一方下達指令,從設備的一方根據指令做出反應並回復主設備,主設備可以有多個從設備。具體來說,工作人員的計算機可認為是master,而PLC之類的具體設備就是slave了。每個設備有自己的“代號”,主設備通過“代號”來找到某一個對應的設備,當然也可以使用廣播的方式,代號0即為廣播。
從協議棧還可以看到,Modbus有自己的數據鏈路層定義,其實主要是對於傳輸數據格式和校驗等方面的規定。具體來說,modbus定義了自己的數據單元,功能碼與具體的數據組成了PDU(協議數據單元 Protocol Data Unit),所謂的功能碼也就是代表了主向從下達的指令是什么,這是很重要的一個知識,后面我們會具體講功能碼代指的功能,數據也就是這次指令要用到的“參數”。
很顯然,只有PDU並不夠,我們還需要知道從設備的“代號”才能知道數據往哪發,還要想辦法保證數據的完整性、一致性和可靠性。所以在PDU的基礎上我們還需要添加一個地址,和一個差錯校驗,這就構成了ADU(Application Data Unit)。但要注意,由於三種Modbus在傳輸中存在差異,所以ADU,特別是校驗部分會有不同。
modbus功能碼
前面說過通過功能碼主設備能夠對從設備下達指令,功能碼有效范圍在1~255之間。其中大部分都是保留的,如128-255為異常響應保留,舉幾個栗子:
- 01 讀線圈狀態
- 02 讀離散輸入狀態
- 03 讀保持寄存器
- 04 讀輸入寄存器
- 05 寫單個線圈
可能看到這里大家就懵了,這都是啥啊。其實很簡單,modbus可以說是將讀寫指令分為了兩大類,一類是離散的,也就是位操作,非1即0;第二類是模擬的,也就是數字,可以叫做字操作。而每一類下面都有輸出和輸出之分,於是就有了下面四種說法:
- DO(digital output 數字量輸出),所謂線圈就是離散的輸出狀態,01即讀一個離散的輸出狀態,舉個不恰當的栗子,你家燈泡接到某個控制器上(實際上並不會存在這種情況……),我們可以通過01加上數據,比如1,讓他亮,加上0,讓他滅。
- DI (digital input 數字量輸入),所謂的離散輸入就是它,還是上面的栗子,我們想知道燈的開關是咋樣的呢?就用02指令看看,如果是1,哦,按下去了,如果是0就是沒按。通過這個不恰當的栗子我們大概也可以猜到,這是不可寫的(如果你隨便一個指令把開關給按死了,那我這燈不是徹底開不了了?),可以理解為外部對工控系統所帶來的“開關”影響。
- AO(AnalogOutput 模擬輸出),保持寄存器的功能,和DO最大的不同就是它不再是0或1,可以是一個數值,比如,我們設定的PID運行參數,或者是溫度的上下限等等
- AI(Analog Input 模擬輸入),也就是輸入寄存器,和DI一樣,可讀但不可寫,可以理解為外部對於系統的多位輸入
當然有寫單個的就必然有寫多個的,比如15就是寫多個線圈,16是寫多個保持寄存器。此外還有讀文件記錄的20,寫文件記錄的21,獲取異常狀態的08等等,這里就不在多說了,具體的大家可以自行查看手冊。
當然,說到功能碼就不得不提Modbus在施耐德設備上的一個重要漏洞了。這就是在defcon上展示過的fun with 0x5a,這個0x5a的功能碼是由施耐德自己實現的非標准的功能碼,該功能碼實現了Modbus標准未允許的功能。在defcon中大佬為我們展示了以下幾項
- 獲取項目和PLC信息
- 開啟、停止PLC
- 下載程序
- 更改程序
這里先不做過多介紹,在之后我們再詳細看一看0x5a攻擊的流量包。
Modbus TCP
上面說了我們這次的主角是Modbus TCP。我們可以通過wireshark對Modbus的流量包進行抓取進而觀察Modbus TCP的數據格式
Transaction identifier : 事務標識符
Protocol identifier : 默認為0 Length : 數據的長度 Unit identifier : 從機地址,因為使用了TCP/IP所以用ip地址來標識從機,所以該位可忽視,或者做進一步分發 Function code : modbus的功能碼 Data :具體的數據
可以看到在遵從TCP/IP的基礎上Modbus加了自己的修改,主要有以下三個部分:
- 由於TCP/IP本身具有數據校驗部分,所以ADU的差錯校驗沒有了
- 實用ip可以確定從機,ADU的附加地址也不再有效。但是目標可以繼續是一個主機,再向其他從機發送數據,這時ADU的附加地址可以作為下一個主機分發數據包時的地址。
- 增加了TCP/IP的頭部,比如length、協議標識符等
Modbus賽題及漏洞
2019工控安全比賽 線上賽第一場 Modbus題目(第一版)
首先說明一下,這個題目就是前不久工控比賽的線上賽第一場的簽到題,但貌似后來又換了個附件,因為也不知道題目是否正確,這里就不再追求flag了,看一下題目本身涉及的知識點。
首先wireshark打開pcap流量包
可以看到在tcp握手后就是清一色的func 90。這就是上面我們提到過的施耐德高危功能碼,它是施耐德自定義的非標准功能碼,功能及其強大,就相當於root般強大。
這個高危功能碼是通過Unity Pro與PLC通信時發送的,Unity Pro像其他開發工具一樣提供了stop、讀取項目信息、debug、修改代碼等功能,這些功能並沒有辦法通過標准的指令碼發送,所以就有了0x5a(也就是90)。后來,工控安全研究和顧問公司Digital Bond在metasploit上放出了0x5a的poc,再后來defcon上有人做了”fun with modbus 0x5a“的演講,這個漏洞算是被熟知了。
首先看第一個Modbus的數據包,我們前面說過,Modbus作為主從協議,必然是主發從響應的,也就是說,在該環境下,100為主機,而253很顯然就是從設備了。功能碼是0x5a高危功能,攜帶數據為0002,此時,我們並不知道該功能到底做了什么。
接着看返回的包
可以看到它符合我們前面對於Modbus的說明,指令碼同樣是0x5a,同時帶回來一部分數據,沒有進行任何的加密,直接解碼發現存在字符串140 CPU 311 10
>>> str.decode("hex") "x00xfex10xffZx01x01x00x00x00px02x00x00'x00tx00x08x00x00x00x00x00x0e140 CPU 311 10x01x01x01x00x00x00x00x11x00"
搜索后發現是施耐德家的一款產品,就是下面圖上的家伙
我們可以推測,上面的指令應該是在獲取從機的設備信息。往下走還可以看到有這樣一個數據包
它的回復則是這樣的
Project字符串很明顯,而x0cx3bx0cx0ex01xdex07是項目文件上次修改的日期,x08x00x00是項目的修訂號,翻譯成人話就是項目在2017年1月14日12時59分12秒進行了第8版的修改
繼續向下探索還可以發現諸如”USER-714E74F21B“之類的字符串,可以看到它獲得了大量的設備及項目相關信息。再往下就不在具體分析了,有興趣的可以自行研究。(建議去利用unity Pro操作,抓取相應的流量包來分析具體0x5a的data功能)
我們可以利用主機的流量包還原出攻擊者的腳本,實際上就是類似funwithmodbus0x5a的攻擊腳本,大家去github下載,msf上也有相應的。
2019工控安全比賽 線上賽第一場 Modbus題目(第二版)
這個題目是真的簽到題,非常簡單……首先還是看一下流量包
可以看到,比起上一個這個可太友善了……都是些正常的功能碼。23是主機,33則是從機,沒什么很明顯的TCP/IP攻擊的痕跡,整體看上去沒啥問題。
接下來就該考慮是否有數據的寫入,flag很有可能就是寫入的數據。那么首先排除12345的功能碼,因為flag既不可能是位操作,也不可能是字操作,都太短了,所以聚焦的就應該是包長度大的,或者是類似16功能碼那樣寫多個字的指令。
這里給出我的腳本,是第二種思路,也就是篩選1234之外功能碼的包並打印內容
import pyshark func_code = [1,2,3,4] def find_flag(): pcap = pyshark.FileCapture("q1.pcap") for c in pcap: for pkt in c: if pkt.layer_name == "modbus": temp = int(pkt.func_code) if temp not in func_code: payload = str(c["TCP"].payload).replace(":", "") print("content[*] is " + payload)
也可以用scu–igroup老哥的腳本,用第一種思路,找長度最大的包提取數據,結果相同(速度還很快…自己寫的腳本在遇到流量包很大的情況下速度很慢)
轉換為ascii碼就得到了最終答案
2018工控安全比賽 線上賽第一場 Modbus題目
這個題目就涉及到了Modbus在TCP上的漏洞了,首先我們還是看看題目給的流量包
說真的一看這包我當時就愣了,這啥玩意,一個modbus的包咋這么多tcp呢?定睛一看才發現都是TCP的重傳,說明啥?說明有可能是中間人攻擊啊。
但是,其實這個題目的中間人其實並沒有什么關系……因為我們要的是流量,而modbus的長流量必然是在1234四個功能碼之外的……當然這道題還有變數,因為存在S7comm,所以其中也有可能藏有flag,所以我們先手動檢查S7comm,發現沒有重要信息后,再來進行下一步
還是上一個題目的腳本,很容易找到相應的流量
這段數據不是傳統的data,進行了加密,所以我一時陷入了僵局,只能通過胡亂組合最終還真碰巧出來了…….因為我並沒有參加18年的工控比賽,所以我不知道當時主辦方給沒給加密方法,但搜到的題目中有相應的解密腳本,如下:
#!/usr/bin/python #coding:utf-8 coils_bytes = 'c29e46a64eeaf64e3626c2ae0ec2a22ac24c0c8c1c'.decode('hex') print len(coils_bytes) flag = '' for data in coils_bytes: #print int('{:08b}'.format(ord(data))) #print int('{:08b}'.format(ord(data)), 2) #print int('{:08b}'.format(ord(data))[::-1]) #print int('{:08b}'.format(ord(data))[::-1], 2) #print int('{:08b}'.format(ord(data)),2),int('{:08b}'.format(ord(data))[::-1], 2) flag += chr(int('{:08b}'.format(ord(data))[::-1], 2)) print flag
這里還有個坑,我跑腳本時沒有過濾掉data外其他的數據,所以導致腳本跑不出來……因為19年那個題沒加密直接轉字符就成,所以我也沒注意到這個問題(實際上就是截圖中flag意外的奇怪字符),只能手動找到了相應的流量包提取了data……
最終flag為
總結
通過上面幾道題目可以看到modbus目前在ctf中還是以簡單題目為主,基本上就是過濾出特殊功能碼的流量包進行簡單的轉換即可,但實際上modbus的還存在許許多多的安全隱患,未來有很多可以出題的點。
- 傳統TCP/IP存在的問題,比如18年的中間人攻擊,雖然並沒有涉及到過多的知識點,但毫無疑問這方面可以做文章
- 異常的功能碼,比如19年的第一版,施耐德的高危功能碼,這是非常難的,從之前的分析可以看到這些保留的功能碼在廠商自定義后對於我們普通的參賽選手來說是很難真正讀懂流量包的,需要配合相應的正向使用知識,和正向使用的流量包來進行學習
- 認證、授權、加密的一系列問題。從題目可以看到,該協議根本沒有認證方面的定義,攻擊者需要的僅僅是一個合適的ip地址而已,至於授權更是無從談起,加密方面也是漏洞百出。
- 緩沖區溢出,未來沒准會出現pwn?