实战编写 wireshark 插件解析私有协议

  • A+
所属分类:安全文章

在对嵌入式设备进行分析时,有时会遇到一些私有协议,由于缺少对应的解析插件,这些协议无法被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协议。

实战编写 wireshark 插件解析私有协议

通过对协议进行分析,以及对对应的程序进行逆向,得到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_openAlways,同时设置console.log.level255,这样在启动Wireshark时会自动打开debug窗口,以便查看打印的内容。

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

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

插件编写

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

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

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

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

-- create a Proto objectlocal synoFinderProtocol = Proto("SynoFinder", "Synology Finder Protocol")local protoName = "syno_finder"-- create ProtoField Objectslocal magic = ProtoField.bytes(protoName .. ".magic", "Magic", base.SPACE)-- (1) register fieldssynoFinderProtocol.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))   endlocal udp_port = DissectorTable.get("udp.port")-- bind port to protocoludp_port:add(9999, synoFinderProtocol)

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

实战编写 wireshark 插件解析私有协议

由于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    endend

实战编写 wireshark 插件解析私有协议

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

  • 将每个tlv进行聚合,同时根据type类型的不同显示不同的名称;

  • 根据value对应的类型以不同的方式呈现其值,比如ip地址、mac地址等,同时考虑对应的字节序。

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

实战编写 wireshark 插件解析私有协议

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 fieldlocal 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/6303864local 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 fieldssynoFinderProtocol.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-declarationfunction getFieldName(field)    local fieldString = tostring(field)    local i, j = string.find(fieldString, ": .* " .. protoName)    return string.sub(fieldString, i + 2, j - (1 + string.len(protoName)))endfunction 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")))endfunction 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    endendfunction formatValue(type, value)    local tmp_func = typeFormats[type]    if(tmp_func) then        return tmp_func(value)    else        return ""    endendfunction 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)    endendlocal 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

实战编写 wireshark 插件解析私有协议

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: