[原创]西门子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 ↩︎