Create Wireshark Dissector in Lua

前言

在对嵌入式设备进行分析时,有时会遇到一些私有协议,由于缺少对应的解析插件,这些协议无法被Wireshark解析,从而以原始数据的形式呈现,不便于对协议的理解与分析。正好之前看到了介绍用Lua脚本编写Wireshark协议解析插件的文章,于是以群晖NAS设备中的某个私有协议为例,动手写了一个协议解析插件。

协议简介

Synology Assistant是群晖提供的一个用于在局域网中发现和管理其设备的工具,其通过9999/udp端口来和NAS设备进行交互,在Wireshark捕获到的部分数据包示例如下。可以看到,由于该协议为私有协议,Wireshark中缺少对应的解析插件,故无法对其进行解析。

根据该协议的作用,暂且称之为syno_finder协议。

通过对协议进行分析,以及对对应的程序进行逆向,得到syno_finder协议的格式如下。其中,协议最开始的8个字节固定为\x12\x34\x56\x78\x53\x59\x4e\x4f,后面部分可以看作是由一系列的tlv组成。

1
2
3
4
5
6
7
#define MAGIC "\x12\x34\x56\x78\x53\x59\x4e\x4f"
struct tlv
{
uint8 type;
uint8 length;
uint8 value[length];
}

在了解了协议的格式后,就可以开始编写对应的解析插件了。

协议解析插件编写

Wireshark本身以及其自带的很多插件都是用C语言写的,同时其也提供了对应的Lua接口,使得编写协议解析插件变得很容易。

插件安装及调试

"帮助 -> 关于 Wireshark -> 文件夹"中可以看到Lua插件的保存路径,将插件放到对应的路径中即可,然后通过Ctrl+Shift+L快捷键来重新加载插件使其生效。

至于调试Lua脚本,一般采用print()的方式就足够了,在"工具 -> Lua" 中打开Console窗口可查看打印的内容。另一种方式是在"编辑 -> 首选项 -> 高级"中设置gui.console_openAlways,同时设置console.log.level255,这样在启动Wireshark时会自动打开debug窗口,以便查看打印的内容。

笔者在测试时,发现每次按Ctrl+Shift+L快捷键重新加载插件时Console窗口会自动关闭,导致看不到打印的内容。

另外,如果编写的Lua插件在运行时出现错误,对应的错误信息会出现Wireshark的协议解析窗口中,可以根据该错误信息去查看WiresharkLua的相关文档。一个比较有用的小技巧是,有时候在编写插件时不知道某个参数的类型或者某个对象实例有哪些方法,可以通过故意出错的方式来产生错误信息,然后根据该信息去查阅文档。

插件编写

一个基本的协议解析插件的代码框架如下。其中,协议解析的主要逻辑在dissector()函数中,该函数有3个参数,如下:

  • buffer:类型为Tvb,包含对应数据包的内容

  • pinfo:类型为Pinfo,包含数据包列表中的列信息

  • tree:类型为TreeItem,包含数据包详情面板中的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- create a Proto object
local synoFinderProtocol = Proto("SynoFinder", "Synology Finder Protocol")
local protoName = "syno_finder"

-- create ProtoField Objects
local magic = ProtoField.bytes(protoName .. ".magic", "Magic", base.SPACE)

-- (1) register fields
synoFinderProtocol.fields = {magic}

function synoFinderProtocol.dissector(buffer, pinfo, tree)
local buffer_length = buffer:len()
if buffer_length == 0 then return end

-- set the name of protocol column
pinfo.cols.protocol = synoFinderProtocol.name

-- create a sub tree representing the synology finder protocol data
local subtree = tree:add(synoFinderProtocol, buffer(), "Synology Finder Protocol")
-- (2) add fields
subtree:add_le(magic, buffer(0, 8))
end

local udp_port = DissectorTable.get("udp.port")
-- bind port to protocol
udp_port:add(9999, synoFinderProtocol)

基于上述代码框架,为了解析协议,只需要创建对应的协议字段并在(1)处注册,然后在(2)处添加到tree中即可。需要说明的是,后续要使用的协议字段必须在(1)处进行注册,但其注册的先后顺序并不代表其在tree中的顺序,同时注册的协议字段也可能并未使用。

由于syno_finder协议相对比较简单,同时后面的数据存在一定的规律,只需要再创建3个字段,然后在循环中进行解析即可,对应的解析结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local magic = ProtoField.bytes(protoName .. ".magic", "Magic", base.SPACE)
local type = ProtoField.uint8(protoName .. ".type", "Type", base.HEX)
local length = ProtoField.uint8(protoName .. ".length", "Length")
local value = ProtoField.bytes(protoName .. ".value", "Value")

synoFinderProtocol.fields = {magic, type, length, value}

function synoFinderProtocol.dissector(buffer, pinfo, tree)
-- ...
local subtree = tree:add(synoFinderProtocol, buffer(), "Synology Finder Protocol")
subtree:add_le(magic, buffer(0, 8))

local offset = 0
local payloadStart = 8
while payloadStart + offset < buffer_length do
local tlvLength = buffer(payloadStart + offset + 1, 1):uint()
subtree:add_le(type, buffer(payloadStart + offset, 1))
subtree:add_le(length, buffer(payloadStart + offset + 1, 1))
subtree:add_le(value, buffer(payloadStart + offset + 2, tlvLength))
offset = offset + 2 + tlvLength
end
end

到这里,一个最基本的协议解析插件就算完成了。但是从上面的图片可以看到,上述代码只是完成了最基本的功能,显示的结果并不太友好,还有进一步优化的空间:

  • 将每个tlv进行聚合,同时根据type类型的不同显示不同的名称;
  • 根据value对应的类型以不同的方式呈现其值,比如ip地址、mac地址等,同时考虑对应的字节序。

参考WiresharkCDP协议解析插件的实现方式,最终呈现的效果以及完整的插件代码如下。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
local synoFinderProtocol = Proto("SynoFinder", "Synology Finder Protocol")
local protoName = "syno_finder"

local typeNames = {
[0x1] = "Packet Type",
[0x11] = "Server Name",
[0x12] = "IP",
[0x13] = "Subnet Mask",
[0x14] = "DNS",
[0x15] = "DNS",
[0x19] = "Mac Address",
[0x1e] = "Gateway",
[0x20] = "Packet Subtype",
[0x21] = "Server Name",
[0x29] = "Mac Address",
[0x2a] = "Password",
[0x4a] = "Username",
[0x4b] = "Share Folder",
[0x70] = "Arch",
[0x73] = "Serial Num",
[0x77] = "Version",
[0x78] = "Model",
[0x7c] = "Mac Address",
[0xc0] = "Serial Num",
[0xc1] = "Category"
}

local magic = ProtoField.bytes(protoName .. ".magic", "Magic", base.SPACE)

local type = ProtoField.uint8(protoName .. ".type", "Type", base.HEX, typeNames)
local length = ProtoField.uint8(protoName .. ".length", "Length")
local value = ProtoField.bytes(protoName .. ".value", "Value")

-- specific value field
local packetType = ProtoField.uint32(protoName .. ".packet_type", "Packet Type", base.HEX)
local serverName = ProtoField.string(protoName .. ".username", "Server Name")
local ipAddress = ProtoField.ipv4(protoName .. ".ip_address", "IP")
local ipMask = ProtoField.ipv4(protoName .. ".subnet_mask", "Subnet Mask")
local dns = ProtoField.ipv4(protoName .. ".dns", "DNS")
local macAddress = ProtoField.string(protoName .. ".mac_address", "Mac Address")
local ipGateway = ProtoField.ipv4(protoName .. ".gateway", "Gateway")
local packetSubtype = ProtoField.uint32(protoName .. ".packet_subtype", "Packet Subtype", base.HEX)
local password = ProtoField.string(protoName .. ".password", "Password")
local arch = ProtoField.string(protoName .. ".arch", "Arch")
local username = ProtoField.string(protoName .. ".username", "Username")
local shareFolder = ProtoField.string(protoName .. ".share_folder", "Share Folder")
local version = ProtoField.string(protoName .. ".version", "Version")
local model = ProtoField.string(protoName .. ".model", "Model")
local serialNum = ProtoField.string(protoName .. ".serial_num", "Serial Num")
local category = ProtoField.string(protoName .. ".category", "Category")

local value8 = ProtoField.uint8(protoName .. ".value", "Value", base.HEX)
local value16 = ProtoField.uint16(protoName .. ".value", "Value", base.HEX)
local value32 = ProtoField.uint32(protoName .. ".value", "Value", base.HEX)

local typeFields = {
[0x1] = packetType,
[0x11] = serverName,
[0x12] = ipAddress,
[0x13] = ipMask,
[0x14] = dns,
[0x15] = dns,
[0x19] = macAddress,
[0x1e] = ipGateway,
[0x20] = packetSubtype,
[0x21] = serverName,
[0x29] = macAddress,
[0x2a] = password,
[0x4a] = username,
[0x4b] = shareFolder,
[0x70] = arch,
[0x73] = serialNum,
[0x77] = version,
[0x78] = model,
[0x7c] = macAddress,
[0xc0] = serialNum,
[0xc1] = category
}

-- display in subtree header
-- reference: https://gist.github.com/FreeBirdLjj/6303864
local typeFormats = {
[0x1] = function (value)
return string.format("0x%x", value:le_uint())
end,
[0x11] = function (value)
return value:string()
end,
[0x12] = function (value)
return value:ipv4() -- Address object
end,
[0x13] = function (value)
return value:ipv4()
end,
[0x14] = function (value)
return value:ipv4()
end,
[0x15] = function (value)
return value:ipv4()
end,
[0x19] = function (value)
return value:string()
end,
[0x1e] = function (value)
return value:ipv4()
end,
[0x20] = function (value)
return string.format("0x%x", value:le_uint())
end,
[0x21] = function (value)
return value:string()
end,
[0x29] = function (value)
return value:string()
end,
[0x2a] = function (value)
return value:string()
end,
[0x4a] = function (value)
return value:string()
end,
[0x4b] = function (value)
return value:string()
end,
[0x70] = function (value)
return value:string()
end,
[0x73] = function (value)
return value:string()
end,
[0x77] = function (value)
return value:string()
end,
[0x78] = function (value)
return value:string()
end,
[0x7c] = function (value)
return value:string()
end,
[0xc0] = function (value)
return value:string()
end,
[0xc1] = function (value)
return value:string()
end
}

-- register fields
synoFinderProtocol.fields = {
magic,
type, length, value, -- tlv
packetType, serverName, ipAddress, ipMask, ipGateway, macAddress, dns, packetSubtype, password, arch, username, shareFolder, version, model, serialNum, category, -- specific value field
value8, value16, value32
}

-- reference: https://stackoverflow.com/questions/52012229/how-do-you-access-name-of-a-protofield-after-declaration
function getFieldName(field)
local fieldString = tostring(field)
local i, j = string.find(fieldString, ": .* " .. protoName)
return string.sub(fieldString, i + 2, j - (1 + string.len(protoName)))
end

function getFieldType(field)
local fieldString = tostring(field)
local i, j = string.find(fieldString, "ftypes.* " .. "base")
return string.sub(fieldString, i + 7, j - (1 + string.len("base")))
end

function getFieldByType(type, length)
local tmp_field = typeFields[type]
if(tmp_field) then
return tmp_field -- specific value filed
else
if length == 4 then -- common value field
return value32
elseif length == 2 then
return value16
elseif length == 1 then
return value8
else
return value
end
end
end

function formatValue(type, value)
local tmp_func = typeFormats[type]
if(tmp_func) then
return tmp_func(value)
else
return ""
end
end

function synoFinderProtocol.dissector(buffer, pinfo, tree)
-- (buffer: type Tvb, pinfo: type Pinfo, tree: type TreeItem)
local buffer_length = buffer:len()
if buffer_length == 0 then return end

pinfo.cols.protocol = synoFinderProtocol.name

local subtree = tree:add(synoFinderProtocol, buffer(), "Synology Finder Protocol")
subtree:add_le(magic, buffer(0, 8))

local offset = 0
local payloadStart = 8
while payloadStart + offset < buffer_length do
local tlvType = buffer(payloadStart + offset, 1):uint()
local tlvLength = buffer(payloadStart + offset + 1, 1):uint()
local valueContent = buffer(payloadStart + offset + 2, tlvLength)
local tlvField = getFieldByType(tlvType, tlvLength)
local fieldName = getFieldName(tlvField)
local description
if fieldName == "Value" then
description = "TLV (type" .. ":" .. string.format("0x%x", tlvType) .. ")"
else
description = fieldName .. ": " .. tostring(formatValue(tlvType, valueContent))
end

local tlvSubtree = subtree:add(synoFinderProtocol, buffer(payloadStart+offset, tlvLength+2), description)
tlvSubtree:add_le(type, buffer(payloadStart + offset, 1))
tlvSubtree:add_le(length, buffer(payloadStart + offset + 1, 1))
if tlvLength > 0 then
local fieldType = getFieldType(tlvField)
if string.find(fieldType, "^IP") == 1 then
-- start with "IP"
tlvSubtree:add(tlvField, buffer(payloadStart + offset + 2, tlvLength))
else
tlvSubtree:add_le(tlvField, buffer(payloadStart + offset + 2, tlvLength))
end
end

offset = offset + 2 + tlvLength
end

if payloadStart + offset ~= buffer_length then
-- fallback dissector that just shows the raw data
Dissector.get("data"):call(buffer(payloadStart+offset):tvb(), pinfo, tree)
end

end

local udp_port = DissectorTable.get("udp.port")
udp_port:add(9999, synoFinderProtocol)

小结

本文以群晖NAS设备中的某个私有协议为例,介绍了采用Lua脚本编写Wireshark协议解析插件的过程。该协议相对比较简单,但方法适用于其他协议。如果经常需要与某些私有协议打交道,在了解协议格式之后,可以尝试编写对应的协议解析插件,方便对协议进行理解与分析。

附件下载

示例pcap文件及协议解析插件

相关链接

  • Creating a Wireshark dissector in Lua 系列
  • Wireshark dissector packet-cdp
  • Example: Dissector written in Lua
  • Wireshark’s Lua API Reference Manual

本文首发于信安之路,文章链接:https://mp.weixin.qq.com/s/TjFCyECoNCU7yLtj3_z17Q