[原創]西門子PLC固件逆向之定位s7comm協議的一個切入口
【未經同意禁止轉載】
鑒於本博客涉及的信息安全技術具有破壞計算機信息系統的風險,建議讀者在學習/研究/探討之前,確保已經充分了解以下內容:
本博客所討論的技術僅限於研究和學習,旨在提高計算機信息系統的安全性,嚴禁用於不良動機,任何個人/團隊/組織不得將其用於非法目的,否則后果自負,特此聲明。
Choice,the problem is choice!
在西門子PLC CPU固件逆向工程的研究工作中[1],我常常找不到一個合適的切入點。
眾所周知,一個CPU芯片私有、嵌入式操作系統私有、文件系統私有、應用層協議棧私有,甚至開發以上軟件的編譯器都是私有,並且沒有符號表symbol table時,逆向工作是多么的困難。這和常見的物聯網設備逆向[2]的難度是不可同日而語的。
那么,沒有頭緒的情況下,我並沒有choice的空間,只有妄圖依靠灰箱知識找到一個突破口。
我們知道s7comm的protocol id是0x32,所以使用“== 0x32”這個條件去全局搜索反編譯結果,搜索結果如下
Line 112668: if ((piParm1 == 0x32) && (piParm1[0x16] == 0x14)) {
Line 391457: if (bVar1 == 0x32) {
Line 446366: if (psParm1 == 0x32) {
Line 491055: if ((((pcParm1 == 0x32) && (pcParm1[1] == 0x01)) && (pcParm1[10] == 0xf0)) &&
Line 492543: if (pcVar4 == 0x32) {
Line 530415: if (uParm2 == 0x32) goto LAB_00241bd0;
我很快定位到491055行這條語句,如下所示。
undefined4 s7comm_job_setup_communication_judge(char *pcParm1)
//當然,在沒有符號表的情況下,這個函數名是我自己修改的
{
undefined4 uVar1;
if ((((*pcParm1 == 0x32) && (pcParm1[1] == 0x01)) && (pcParm1[10] == 0xf0)) &&
((*(short *)(pcParm1 + 0xc) != 0 && (*(short *)(pcParm1 + 0xe) != 0)))) {
uVar1 = 1;
}
else {
uVar1 = 0;
}
return uVar1;
}
看到這個非常冗長的判斷條件自然令人興奮,它和我們熟悉的s7comm協議中建立應用層連接的報文似乎存在千絲萬縷的關系。
我們找到一個s7comm協議的數據包例子,對比s7comm_job_setup_communication_judge
函數的判斷條件和真實的setup communicaiton報文有什么關系?
我以網絡上公開的一個s7comm數據包集合為例[3],下圖展示了我要分析的一條setup communicaiton報文。
從上圖中抽出s7comm應用層的字段描述和值,擺在下面。
//job_setup_communication報文應用層payload
0000 32 01 00 00 02 00 00 08 00 00 f0 00 00 01 00 01 2...............
0010 01 e0 ..
Header: (Job)
Protocol Id: 0x32
ROSCTR: Job (1)
Redundancy Identification (Reserved): 0x0000
Protocol Data Unit Reference: 512
Parameter length: 8
Data length: 0
Parameter: (Setup communication)
Function: Setup communication (0xf0)
Reserved: 0x00
Max AmQ (parallel jobs with ack) calling: 1
Max AmQ (parallel jobs with ack) called: 1
PDU length: 480(字節?)
接着,我們細致分析固件中這條判斷條件
if ((((*pcParm1 == 0x32) && (pcParm1[1] == 0x01)) && (pcParm1[10] == 0xf0)) &&
((*(short *)(pcParm1 + 0xc) != 0 && (*(short *)(pcParm1 + 0xe) != 0)))) {
分開來寫,
條件1)s7comm.header.protid == 0x32,s7comm
條件2)s7comm.header.rosctr == 0x01,Job
條件3)s7comm.param.func == 0xf0,Setup communication
條件4)s7comm.param.maxamq_calling != 0,Max AmQ calling
條件5)s7comm.param.maxamq_called != 0,Max AmQ called
可以看出,這五個判斷條件和s7comm數據包實例中字段是可以一一對應上的,且符合我們對s7comm協議的灰盒知識。
由此,印證了我們之前的推論,s7comm_job_setup_communication_judge
函數是判斷PLC設備接收到的某條報文是不是符合條件的s7comm.header.rosctr為job&&s7comm.param.func為setup communication報文。
我們找到了s7comm_job_setup_communication_judge
作為西門子PLC CPU設備固件逆向的一個突破口,圍繞這個突破口,我們展開下一階段的工作設想。
從s7comm_job_setup_communication_judge函數向前看
函數判斷條件中那沒有涉及到的字段有
1)s7comm.header.redid
2)s7comm.header.pduref
3)s7comm.header.parlg
4)s7comm.header.datlg
5)s7comm.param.setup_reserved1
6)s7comm.param.pdu_length
為什么沒有判斷,job_setup_communication中各個字段有什么作用?
s7comm_job_setup_communication_judge函數的輸入極有可能是socket_recv的報文隊列,它的地址是什么?
socket_recv函數和這個隊列的關系?
從s7comm_job_setup_communication_judge函數向后看
- 如果s7comm_job_setup_communication_judge函數輸出等於1,下面PLC應該構造ack_data_setup_communication報文,構造函數是什么?
- 構造好的ack_data_setup_communication報文,如何通過socket_send函數發送出去?
以上問題,我們日后將一一嘗試解答。
致謝
感謝眾多安全研究員公開發表自己的研究成果,並熱情地回復我的疑問,這些人包括但不限於Dillon Beresford/Ali Abbasi/Thomas Weber/Ralf Ramsauer/Gene blue。
我手上under test的西門子PLC CPU模塊屬於早期的S7-1200系列(十分抱歉,我不能公開該設備的訂貨號、固件版本等指紋信息),它既支持s7comm協議,也支持s7comm-plus協議;因為產品硬件版本和固件版本太低,極大概率不支持s7comm-plus-plus協議。我使用IDA Pro和Ghidra等工具反匯編/反編譯了該設備的固件。 ↩︎
《揭秘家用路由器0day漏洞挖掘技術》 ↩︎
https://github.com/gymgit/s7-pcaps/blob/master/snap7_s300_setupCommunication.pcapng ↩︎