在对嵌入式设备进行分析时,有时会遇到一些私有协议,由于缺少对应的解析插件,这些协议无法被Wireshark
解析,从而以原始数据的形式呈现,不便于对协议的理解与分析。正好之前看到了介绍用Lua
脚本编写Wireshark
协议解析插件的文章:
https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html
于是以群晖NAS
设备中的某个私有协议为例,动手写了一个协议解析插件。
协议简介
Synology Assistant
是群晖提供的一个用于在局域网中发现和管理其设备的工具,其通过9999/udp
端口来和NAS
设备进行交互,在Wireshark
捕获到的部分数据包示例如下。可以看到,由于该协议为私有协议,Wireshark
中缺少对应的解析插件,故无法对其进行解析。
根据该协议的作用,暂且称之为
syno_finder
协议。
通过对协议进行分析,以及对对应的程序进行逆向,得到syno_finder
协议的格式如下。其中,协议最开始的8个字节固定为x12x34x56x78x53x59x4ex4f
,后面部分可以看作是由一系列的tlv
组成。
# define MAGIC "x12x34x56x78x53x59x4ex4f"
struct tlv
{
uint8 type;
uint8 length;
uint8 value[length];
}
在了解了协议的格式后,就可以开始编写对应的解析插件了。
协议解析插件编写
Wireshark
本身以及其自带的很多插件都是用C语言写的,同时其也提供了对应的Lua
接口,使得编写协议解析插件变得很容易。
插件安装及调试
在"帮助 -> 关于 Wireshark -> 文件夹"
中可以看到Lua
插件的保存路径,将插件放到对应的路径中即可,然后通过Ctrl Shift L
快捷键来重新加载插件使其生效。
至于调试Lua
脚本,一般采用print()
的方式就足够了,在"工具 -> Lua"
中打开Console
窗口可查看打印的内容。另一种方式是在"编辑 -> 首选项 -> 高级"
中设置gui.console_open
为Always
,同时设置console.log.level
为255
,这样在启动Wireshark
时会自动打开debug
窗口,以便查看打印的内容。
笔者在测试时,发现每次按
Ctrl Shift L
快捷键重新加载插件时Console
窗口会自动关闭,导致看不到打印的内容。
另外,如果编写的Lua
插件在运行时出现错误,对应的错误信息会出现Wireshark
的协议解析窗口中,可以根据该错误信息去查看Wireshark
或Lua
的相关文档。一个比较有用的小技巧是,有时候在编写插件时不知道某个参数的类型或者某个对象实例有哪些方法,可以通过故意出错的方式来产生错误信息,然后根据该信息去查阅文档。
插件编写
一个基本的协议解析插件的代码框架如下。其中,协议解析的主要逻辑在dissector()
函数中,该函数有3个参数,如下:
buffer
:类型为Tvb
,包含对应数据包的内容pinfo
:类型为Pinfo
,包含数据包列表中的列信息tree
:类型为TreeItem
,包含数据包详情面板中的相关信息
-- 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
个字段,然后在循环中进行解析即可,对应的解析结果如下。
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
地址等,同时考虑对应的字节序。
参考Wireshark
中CDP
协议解析插件的实现方式,最终呈现的效果以及完整的插件代码如下。
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 文件及协议解析插件:
https://github.com/myh0st/scripts/blob/master/syno_finder.zip
相关链接
Creating a Wireshark dissector in Lua 系列:
https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html
Wireshark dissector packet-cdp:
https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-cdp.c
Example: Dissector written in Lua:
https://www.wireshark.org/docs/wsdg_html_chunked/wslua_dissector_example.html
Wireshark’s Lua API Reference Manual:
https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html