工业网络通讯数据分析之Wireshark插件编写

在分析工控设备流量时,通过Wireshark内置的协议解码插件可以解析一些开放协议的数据格式,但是很多厂家考虑到安全性和产品独特性并不会公开私有的报文格式。这就需要通过逆向工程或者查阅相关文档来了解通讯协议的数据格式,从而编写Wireshark协议解码插件来解析未知的工业网络通讯数据报文格式。

协议分析

本文中,我们以一个国产某工控设备的上位机流量作为例子,如下:

使用Wireshark抓取到的流量如下,可以看到该数据报文是基于TCP协议的,下位机的端口为500。

由于上位机是C#写的,可以用dnSpy快速定位到其协议解析的逻辑代码,通过静态分析和动态调试的手段来分析出上位机和下位设备之间通信的报文格式。

该报文简单格式如下表格,这里只分析到报文格式的头部,不过对于工业网络通讯数据分析来说,识别出该报文对应的业务操作已经足够了。

插件编写

以Lua脚本为例:

目前对于Wireshark来说,C/C++语言和Lua脚本是编写插件的主流语言,对于C/C++语言这类语言来说,不可否认它具有非常高的性能,但是其编译配置较为麻烦,每次修改都要重新编译,而Lua脚本虽然解析效率没前者那么高,但是它的语法和修改都非常简单,可以提高了开发效率。所以,这里选择了Lua作为编写插件的语言。

Wireshark软件是否支持Lua插件脚本的检查方法:启动Wireshark,依次点击”帮助”,”关于 Wireshark“菜单,在打开的对话框中的”Wireshark”标签页上观察版本信息,如果如下图一样显示With Lua,说明此版本支持Lua插件。

  1. 开始编写Lua插件,首先简单定义协议名称和协议说明。
  1. 根据不同的报文字段类型添加每个字段的名称。

在这里,可以将不同的命令解释显示出来,从而增加可读性,如下:

  1. 构建一个解析器函数,根据每个字段的偏移和大小,对每个字段进行显示。
  1. 最后将编写好的解析器添加到TCP 500端口上。

最后的插件代码如下:

--Etrol plugin 
local subcmd_desc={
    [1]="SYSCOM",
    [2]="SCANCOM",
    [3]="EVENCOM",
    [4]="PIDCOM",
    [5]="PIDPARA",
    [6]="SYSMSG",
    [7]="HARTCOM",
    [8]="SYSRESET",
    [17]="DNP3DBCOM",
    [18]="DNP3CHN0COM",
    [19]="ZIGBEECOM",
    [20]="DNP3OTHER",
 
}
local operate_desc={
    [0]="CLRMOD",
    [1]="READCOM",
    [2]="WRITECOM",
    [4]="WRITEFLASH",
}
local cmd_desc={
    [137]="read/write",
    [144]="PROG_update",

}
Echo_protocol=Proto("Echo_500","Echo_500 protocol")
pktid = ProtoField.uint32("Echo_500.ID", "ID", base.DEC)
pktlen = ProtoField.uint32("Echo_500.length", "length", base.DEC)
station=ProtoField.uint32("Echo_500.station", "station", base.DEC)
cmd = ProtoField.uint32("Echo_500.cmd", "cmd", base.DEC,cmd_desc)
address = ProtoField.uint32("Echo_500.address", "address", base.DEC)
subcmd = ProtoField.uint32("Echo_500.subcmd", "subcmd", base.DEC,subcmd_desc)
operate = ProtoField.uint32("Echo_500.operate", "operate", base.DEC,operate_desc)
subaddress = ProtoField.uint32("Echo_500.subaddress", "subaddress", base.DEC)
datalen = ProtoField.uint32("Echo_500.datalen", "datalen", base.DEC)
data = ProtoField.bytes("Echo_500.Data", "Data")

hex=function(num) return string.format("%#x",num) end

Echo_protocol.fields={pktid,pktlen,station,cmd,address,subcmd,operate,subaddress,datalen,data}
function Echo_protocol.dissector(buffer,pinfo,tree)
    local length=buffer:len()
    if length<14 then return end
    pinfo.cols.protocol=Echo_protocol.name
    local subtree=tree:add(Echo_protocol,buffer(),"Echo Protocol Data")
    subtree:add(pktid,buffer(0,4))
    subtree:add(pktlen,buffer(4,2))
    subtree:add(station,buffer(6,1))
    subtree:add(cmd,buffer(7,1))
    subtree:add(address,buffer(8,1))
    subtree:add(subcmd,buffer(9,1))
    sub_cmd=buffer(9,1):uint()
    subtree:add(operate,buffer(10,1))
    Operate=buffer(10,1):uint()
    subtree:add(subaddress,buffer(11,2))
    subtree:add(datalen,buffer(13,1))
    if length>14 then
        local databuf=buffer(14,length-14)
        local data_subtree=subtree:add(data,databuf)
    end
end

local tcp_port=DissectorTable.get("tcp.port")
tcp_port:add(500, Echo_protocol)

运行

为了Wireshark加载编写好的插件,需要打开Wireshark主目录下的init.lua文件,确保disable_lua的值为false,即开启了lua。同时将编写好的插件保存为echo_500.lua放到Wireshark主目录,在init.lua文件最后一行添加dofile(DATA_DIR..”echo_500.lua”),通过这样的方式加载自定义的插件。

重启Wireshark,重新抓取数据包,可以看到协议已经解析成功。这里识别出来这是一个SYSMSG命令,用于读取设备状态。

之后每次修改脚本后,可以通过快捷键“Ctrl+Shift+L”重新加载脚本,不需要每次重启Wireshark软件。

总结

本文简单介绍了Wireshark插件的编写方法,通过编写插件来解析第三方的私有协议,帮助我们更好理解工控上位机和下位机的交互。当然,有些工控流量可能比本文的例子相对复杂一些,可能包含加密和签名等字段,这就需要更长的逆向分析时间,只要把协议数据的报文格式了解清楚了,那么编写插件也是水到渠成的事情。

作者:绿盟科技 工业物联网安全实验室 陈杰

Spread the word. Share this post!

Meet The Author

Leave Comment