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

2020-09-11 15:30:03 浏览数 (1)

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

代码语言:javascript复制
# 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,包含数据包详情面板中的相关信息
代码语言:javascript复制
-- 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个字段,然后在循环中进行解析即可,对应的解析结果如下。

代码语言:javascript复制
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协议解析插件的实现方式,最终呈现的效果以及完整的插件代码如下。

代码语言:javascript复制
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

0 人点赞