mailto:[email protected]
【未经同意禁止转载】
因为软件著作权和软件用户协议的限制,我们没有办法对STEP7软件的关键DLL文件进行逆向工程,或者动态调试,这可真是一个头疼的问题。
我们可以考虑另一条更为狭窄的逆向之路了——借助震网病毒的研究成果。
赛门铁克的震网STUXNET病毒分析报告中声称,
震网病毒是替换掉Step7软件中
S7OTBXDX.DLL
动态链接库文件,劫持掉该DLL文件包含了一些对PLC编程和调试有关键功能的函数,完整列出
- s7db_open
- s7db_close
- s7blk_read
- s7blk_write
- s7ag_link_in
- s7blk_findnext
- s7blk_findfirst
- s7blk_delete
- s7ag_read_szl
- s7_event
- s7ag_test
- s7ag_stop
- s7ag_bub_cycl_read_create
- s7ag_bub_read_var
- s7ag_bub_write_var
- s7ag_bub_read_var_seg
- s7ag_bub_write_var_seg
github上有项目公开了一部分震网病毒STUXNET的反编译结果,震网病毒和我们关心的s7otbxdx.dll
关系很大,所以我们尝试从震网病毒样本中了解s7otbxdx.dll
的内部构造。
https://github.com/Laurelai/decompile-dump
我们观察到项目中789F6F8DE3F140CF5D73BEF0B8ABAF78.c文件中,确实列出了一些我们上面罗列的s7簇函数。
毫无疑问,使用STEP7软件中的某些功能,就会触发这些函数,上位机通过s7comm
协议和PLC交换状态和数据。
我抽一个s7ag_read_szl
函数/功能来细致研究一下。
赛门铁克的病毒分析报告《W32.Stuxnet Dossier》中声称
Used to query PLC information, through a combination of an ID and an index (it can be used for instance to get the PLC type.) The export modifies the API’s return information if it’s called with specific ID=27, index=0.
可以看出来s7ag_read_szl
函数的功能是读取PLC的系统状态列表(read system status list,status在德文中是以'z'开头的,所以szl就沿用到了现在)。
但是赛门铁克报告中说震网用了SZL-id=27/index=0,这个我是很难理解的。
按照《SIMATIC 用于S7-300/400系统和标准函数的系统软件 卷1/2》和wireshark s7comm dissector的表述,我更倾向于震网用了SZL-id=0x1c/index=0用于识别PLC的系列是不是6es7 315-2。
下面是发送s7ag_read_szl
(SZL-id=0x1c/index=0)后,PLC响应报文。
0000 ff 09 00 d6 00 1c 00 00 00 22 00 0a 00 01 53 37 ÿ..Ö....."....S7
0010 33 30 30 2f 45 54 32 30 30 4d 20 73 74 61 74 69 300/ET200M stati
0020 6f 6e 5f 31 00 00 00 00 00 00 00 00 00 00 00 02 on_1............
0030 50 4c 43 5f 31 00 00 00 00 00 00 00 00 00 00 00 PLC_1...........
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0070 00 00 00 04 4f 72 69 67 69 6e 61 6c 20 53 69 65 ....Original Sie
0080 6d 65 6e 73 20 45 71 75 69 70 6d 65 6e 74 00 00 mens Equipment..
0090 00 00 00 00 00 05 53 20 43 2d 42 31 55 33 39 33 ......S C-B1U393
00a0 31 34 32 30 31 31 00 00 00 00 00 00 00 00 00 00 142011..........
00b0 00 00 00 00 00 00 00 07 43 50 55 20 33 31 35 2d ........CPU 315-
00c0 32 20 50 4e 2f 44 50 00 00 00 00 00 00 00 00 00 2 PN/DP.........
00d0 00 00 00 00 00 00 00 00 00 08 ..........
三重证据证明了,赛门铁克报告中的描述可能有错,除非SZL-id=27/index=0是一个隐藏的功能(我未了解)。
下面,我们尝试剖开s7ag_read_szl
函数内部看看(相比s7ag*函数簇中的其他函数,这个函数是最简单的)。
int __cdecl s7ag_read_szl()
{
int v1; // eax@1
v1 = sub_1000BE40((int)sub_100026EA);
return ((int (*)(void))v1)();
}
sub_1000BE40函数的功能是,把这个函数劫持到sub_100026EA函数处。
void __stdcall sub_100026EA(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
int v8; // eax@1
char v9; // [sp+0h] [bp-28h]@1
int v10; // [sp+10h] [bp-18h]@1
char v11; // [sp+14h] [bp-14h]@1
char *v12; // [sp+18h] [bp-10h]@1
int v13; // [sp+24h] [bp-4h]@1
v12 = &v9;
sub_1000F9F2();
v13 = 0;
v8 = sub_1000D2B6(a1, a2, a3, a4, a5, a6, a7, a8);
unknown_libname_13(v8);
LOBYTE(v13) = 1;
v10 = sub_100034DB(&v11);
LOBYTE(v13) = 0;
sub_100034F7((int *)&v11);
JUMPOUT(*(unsigned int *)loc_10002758);//函数执行最后,调回loc_10002758,猜测这是s70tbxdx.dll中s7ag_read_szl函数的真实内存地址
}
sub_100026EA函数传入了a1~a8一共八个参数,这就有点意思了,因为调用它的s7ag_read_szl
一个参数都没有。我们有理由猜测:这八个参数中,一定有表示SZL-id/index的两个参数。究竟是那两个呢,我们近距离观察sub_1000D2B6函数,因为八个参数又全部传到了这个函数里。
int __cdecl sub_1000D2B6(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
int result; // eax@1
int v9; // ecx@1
v9 = (int)operator new(0x40u);
result = 0;
if ( v9 )
result = sub_1000906E(v9, a1, a2, a3, a4, a5, a6, a7, a8);
return result;
}
这个函数先new分配了一块内存,int v9[64],把首地址v9连带八个参数又传入了sub_1000906E函数。
int __thiscall sub_1000906E(int this, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
int v10; // esi@1
v10 = this;
*(_DWORD *)this = &off_1001C428;//还出现在s7blk_read函数中 对象的函数成员 构造函数?
*(_DWORD *)(this + 40) = a5;
*(_DWORD *)(this + 44) = a6;
*(_DWORD *)(this + 48) = a7;
*(_DWORD *)(this + 52) = a8;
*(_DWORD *)(this + 32) = a3;
*(_DWORD *)this = &off_100211D8;//对象的函数成员 析构函数?
*(_DWORD *)(this + 28) = a2;
*(_DWORD *)(this + 36) = a4;
*(_DWORD *)(this + 56) = a9;
sub_10003B6C(this + 4, a2, a3, a4);
return v10;
}
真的不容易,终于看到s7ag_read_szl
这个函数是怎么样构造报文了,但是函数最后又调用了sub_10003B6C。
int __thiscall sub_10003B6C(int this, int a2, int a3, int a4)
{
int result; // eax@1
*(_DWORD *)this = a2;
*(_DWORD *)(this + 4) = a3;
result = a4;
*(_DWORD *)(this + 8) = a4;
return result;
}
我们把sub_1000906E函数和sub_10003B6C函数,这一连串的结构体/对象构造过程看一下,很明显sub_10003B6C说明this+4这里又有一个小结构体/对象,属于has a关系,有可能是继承?
其实,我犯了一个错误,我忘记了现在只是在分析震网病毒,这里只是构造了一个结构体/对象,并不是最终报文的形式。这个结构体是s7otbxdx.dll与调用dll的进程交换数据的一种数据格式,我们简单把它叫做S7otbxdx_exchangeBlock
。只能大胆猜测,S7otbxdx_exchangeBlock
和报文格式有很大关联度,但是不能说两者就是一一对应的关系。
下面,我们尝试逆向分析这个S7otbxdx_exchangeBlock
的结构体/对象。
首先,S7otbxdx_exchangeBlock
的空间是由new创造的,那么先由new的大小,统计一下,有几种S7otbxdx_exchangeBlock
。因为震网病毒只劫持了十几个函数,所以我们只能从这些函数中一窥究竟。
sizeof(S7otbxdx_exchangeBlock) | 相关函数 | 基类 |
---|---|---|
0x40u | s7ag_read_szl | sub_10003B6C(this + 4, a2, a3, a4) |
s7ag_bub_read_var | sub_10003B83(this + 4, a2, a3) | |
s7ag_bub_write_var | sub_10003B83(this + 4, a2, a3) | |
0x48u | s7blk_read | sub_10003B6C(this + 4, a2, a3, a4) |
s7blk_findfirst | sub_10003B6C(this + 4, a2, a3, a4) | |
s7blk_findnext | sub_10003B6C(this + 4, a2, a3, a4) | |
s7_event | sub_10003B6C(this + 4, a2, a3, a4) | |
0x4cu | s7blk_delete | sub_10003B6C(this + 4, a2, a3, a4) |
0x38u | s7blk_write | sub_10003B6C(v10 + 4, *(_DWORD )(v10 + 28), (_DWORD )(v10 + 32), (_DWORD *)(v10 + 36)); |
0x30u | s7ag_link_in | sub_10003B6C(this + 4, a2, a3, a4) |
0x44u | s7ag_bub_read_var_seg | sub_10003B83(this + 4, a2, a3) |
s7ag_bub_write_var_seg | sub_10003B83(this + 4, a2, a3) | |
0x50u | S7ag_bub_cycl_read_create | sub_10003B83(this + 4, a2, a3) |
0x70u | S7ag_test | sub_10003B83(this + 4, a2, a3) |
其实我猜测,这些对象包含的基类很有可能是它们对应报文的参数段(parameter)的一些字段。
为什么这么说呢?以后我们从报文的特征来对应一下。
这样,我们简单认识了震网病毒样本中的s7ag_read_szl
函数,当然我们假设STEP7 s7otbxdx.dll中的s7ag_read_szl
函数也长这个样子。
至于事实上究竟如何?不让反编译和动态调试s7otbxdx.dll,也没办法了解。