wireshark lua api - 协议解析
七月 31, 2025 [网络与安全] #obsidian #协议与标准 #网络wireshark lua api - 协议解析

1. 背景
wireshark是一个分析报文的强大工具,它已经内置了非常多类型的报文解析,对于我们分析常见的报文非常有用。但是,越来越多的公司倾向于自研协议,这些协议没有被wireshark默认支持,因此如果我们要通过wireshark来高效地分析这些协议,就需要自定义解析。
今天,我们就来聊聊,如何通过lua来定义报文格式,让wireshark能够支持各种我们自定义的协议格式,将协议解析成我们能够理解与分析的形式,并进行过滤分析。
本文作为一篇入门的文章,如果要深入学习,我建议读者在阅读完本文后,再去wireshark页面,去全面了解下Wireshark's Lua API Reference Manual。
2. lua解析小试牛刀
在深入之前,我们先小试牛刀,来实现一个入门的协议解析。
假如,我们设计了一个协议叫Foo Protocol,属于一个基于UDP的自定义协议,我们的协议总是会占用9999端口。
搭好框架
在了解细节前,我们先搭好框架。整个解析的框架代码如下:
-- file: foo.lua
-- 创建一个协议
local foo_protocol = Proto("FooBar", "Foo Protocol")
foo_protocol.fields = {
-- 该协议支持的所有字段
}
-- 协议解析方法
function foo_protocol.dissector(buffer, pinfo, tree)
-- 协议解析
end
local udp_table = DissectorTable.get("udp.port")
-- 方式1:遇到端口为9999的,自动解析为foo protocol
udp_table:add("9999",foo_protocol)
-- 方式2:用add_for_decode_as,可以在界面上支持“Decode As”
udp_table:add_for_decode_as(foo_protocol)
框架搭好后,我们把文件添加到配置文件加里面,并且在init.lua(如果没有,请创建)中增加调用:
-- file: init.lua
dofile("foo.lua")
lua脚本放哪?可以打开wireshark,About Wireshark > Folders > Personal Configuration,这个是个人配置,放在这里就好了:

来,让我们用wireshark打开一个抓包文件,然后选择需要解的包,Decode As,如下,就可以用我们自定义的方式解析了。当然,现在我们没有做任何解析。

丰富细节
作为foo protocol的设计者,我们擅长自研,借鉴了rtp协议,创造了我们的foo协议,遥遥领先。在wireshark里面解析foo协议,也得替换掉,不能露馅。
我们先定义好,这个协议里面所有的定长字段:
-- 过滤名称为ver,显示为十进制,0xc0表示取uint8的最高两位
rtp_ver = ProtoField.uint8("ver", "rtp version", base.DEC, nil, 0xc0)
rtp_pad = ProtoField.uint8("padding", "rtp padding", base.DEC, nil, 0x20)
rtp_ext = ProtoField.uint8("ext", "rtp extension", base.DEC, nil, 0x10)
rtp_cc = ProtoField.uint8("CSRCcount", "csrc count", base.DEC, nil, 0x0f)
rtp_marker = ProtoField.uint8("rtp_marker", "rtp marker", base.DEC, nil, 0x80)
rtp_pt = ProtoField.uint8("payloadtype", "rtp payload type", base.DEC, nil, 0x7f)
rtp_seq = ProtoField.uint16("sequencenumber", "rtp seq", base.DEC)
-- 上面的字段需要增加到协议的表里面,否则不支持直接add
foo_protocol.fields = {
rtp_ver,
rtp_pad,
rtp_ext,
rtp_cc,
rtp_marker,
rtp_pt,
rtp_seq
}
解析方法:
function foo_protocol.dissector(buffer, pinfo, tree)
-- 协议解析
local foo_tree = tree:add(foo_protocol, buffer(), "FooBar Protocol")
-- 增加一个子节点, 对应第一个字节:FooBar Protocol>first byte>...
-- 因为这里的字段有定义过,因此只需要给字段变量,内存范围即可
local first_byte_tree = foo_tree:add(buffer(0, 1), "first byte")
first_byte_tree:add_le(rtp_ver, buffer(0, 1))
first_byte_tree:add_le(rtp_pad, buffer(0, 1))
first_byte_tree:add_le(rtp_ext, buffer(0, 1))
first_byte_tree:add_le(rtp_cc, buffer(0, 1))
-- 增加一个子节点,对应第二个字节:FooBar Protocol>second byte>...
local second_byte_tree = foo_tree:add(buffer(1, 1), "second byte")
second_byte_tree:add_le(rtp_marker, buffer(1, 1))
second_byte_tree:add_le(rtp_pt, buffer(1, 1))
-- 剩下的暂时都加入到一个父节点里面
foo_tree:add_le(rtp_seq, buffer(2, 2))
-- 自定义解析,可以用于变长编码解析
-- 对于ssrc,我们不去定义字段,而是在这里动态解析为uint,需要我们自己去控制显示
foo_tree:add_le(buffer(4, 4), "seq:"..buffer(4, 4):uint())
-- 剩下的留给大家自己实现
foo_tree:add(buffer(8), "all payloads...")
end
于是,我们就完成了大部分的协议的解析了,可以将内存与协议映射,并且按照层级结构显示。

上面是不是很简单?基于这些信息,你就可以写一个更复杂的协议解析了。
3. 深入wireshark lua api
为了让牛马们更好地玩wireshark,必须要深入了解wireshark的lua api。这个章节,我们将从原理上介绍。
协议分层解析
wireshark将报文从二进制解析成我们能够理解的方式,并在UI中以层级的树形方式,呈现给我们,让我们更快地理解数据;结构化的数据,可以在wireshark中进行过滤分析,可视化流量分析等等。如下图所示,就是一个RTP的分层协议分析:

我们都知道协议是嵌套的,udp可以嵌套rtp,rtp还可以嵌套h264等。我们的foo协议,同样可以携带h264码流。因此,在foo解析完成后,还需要对payload进行编码的解析。如下:
-- 获取h264解析协议,从buffer的offset字节开始解析为h264,并添加到foo_tree里面
local dissector = Dissector.get("h264")
dissector:call(buffer(offset):tvb(), pinfo, foo_tree)
dissector与dissector table
在wireshark分析数据,需要理解的最基础的概念就是dissector,即报文解析器。它的功能是:
输入一段buffer,将buffer解析成结构化方式,并添加的解析树中,可用于可视化以及过滤等。
解析器(dissector)通常会被添加到dissector table中。一个表对应一个上层协议的过滤条件。
在wireshark里面,协议解析是逐层解析的。比如,对于在UDP协议上传输的RTP报文,需要按照ethernet、IP、UDP、RTP逐层解析。这些协议是嵌套的,每一个协议都有一个dissector table,在当前层协议解析完成后,可以调用这个表里面的协议继续解析。在这个表里面的协议,支持decode as。
默认配置下,UDP的dissector table中就存在RTP协议,支持用户decode as为RTP。

通过DissectorTable.get("udp.port"):add("9999",foo_protocol),可以将foo_protocol这个协议,添加到udp的dissector table中,触发foo协议解析的条件是udp.port == 9999。 通过DissectorTable.get("udp.port"):add_for_decode_as(foo_protocol),将对应udp端口的报文decode as这个foo协议,并且会将这条规则加入dissector table中。 dissector table可以从上面查询到。
字段与解析树
我们知道,wireshark在界面上,以树行方式呈现协议结构。在lua代码中,用TreeItem来描述这个树以及树的节点。而用字段(Field)将变量与报文内存做映射。
在解析过程中,我们将内存映射为字段(Field),并将字段绑定到TreeItem,同时构建出一颗协议解析树。这就是整个过程。
dissector、post dissector、tap
对于解析,wireshark中有3中协议解析,这里来澄清下它们的区别。 - dissector:普通的协议解析器,当命中某个已解析协议的dissector table规则后,可以按照这条规则解析协议。 - post dissector:在完成所有普通的dissector完成后,再次处理报文。在这里可以额外增加一些字段,这些字段放在wireshark解析字段的最后面。 - tap:从多个报文聚合信息分析。比如可以做一些码率统计、个数统计等。tap一般不会聚合单个包的信息,我们通常会用它做一些窗口操作。
Tvb和TvbRange
在写脚本的时候,必然会存在内存的读取,以及将内存转换为实际的各种类型的数据,以及将这些数据呈现出来。这些操作就会用到tvb和tvbrange。
Tvb在wireshark lua中,用于表示一个报文的内存数据块。常用的函数有: - tvb:len()获取tvb长度 - tvb:bytes([offset], [length])获取一段ByteArray - tvb:offset()获取相较于初始的tvb偏移 - tvb:range([offset], [length])创建一个TvbRange对象 - tvb:raw([offset], [length])获取lua的报文内存表示
tvb是内存数据库,它没有数据类型,当我们需要读取的时候,会对它做内存切片,这个切片在wireshark lua中就是TvbRange。我们用TvbRange表示一段Tvb内存,类似于C++里面的string_view、或者其他语言里面的切片(slice)。TvbRange可以从tvb创建,可以可以从一个已有的tvbrange创建:
-- 见tvb:range([offset], [length])
-- 从tvb这个对象创建TvbRange,偏移为offset,长度为length
-- 如果不指定length,则范围为offset~tvb结束
-- 如果offset和length均不指定,则以整个tvb为TvbRange
tvbrange = tvb(offset, length)
tvbrange = tvbrange:range(offset, length) -- 支持套娃,tvgrange上创建tvbrange
读取一定长度数据,我们就可以很容易将其转化为已知的类型,如下所示:
-- 基础类型:int/uint、int64、uint64(32bit以及以下的整形统一为int/uint)、float
-- 不同类型支持自动大端序和小端序读取
tvbrange:uint() -- 默认大端序
tvbrange:le_uint() -- 小端序
-- 支持ip、时间、string类型
tvbrange:ipv4()
tvbrange:ipv6()
tvbrange:nstime()
tvbrange:string([encoding])
tvbrange:stringz([encoding]) -- \0结束的字符串
-- 支持bitflag
tvbrange:bitfield([position], [length])
有了上面的信息,我们再来详细解释下下面代码:
function foo_protocol.dissector(buffer, pinfo, tree)
-- FooBar,覆盖整个buffer,切片(tvbrange)长度等于输入的tvb长度
local foo_tree = tree:add(foo_protocol, buffer(), "FooBar Protocol")
-- first byte,覆盖了一个字节,切换长度为1
local first_byte_tree = foo_tree:add(buffer(0, 1), "first byte")
-- 这些报文字段解析,直接输入了1个字节的切片分析
first_byte_tree:add_le(rtp_ver, buffer(0, 1))
-- ...
-- 拿到一个4字节切片,转化为uint数据
foo_tree:add_le(buffer(4, 4), "seq:"..buffer(4, 4):uint())
-- ...
end
64位数据处理
如何处理位运算?用bit,参考文档。按位与使用bit.band。
lua的int都使用double处理,不能表示到所有64位数据,代码里面不能直接使用64位常量,怎么处理?参考文档,使用UInt64、Int64:
-- (低32为,高32位),0x3f91df0b2b89dd1e
local mynum = UInt64(0x2b89dd1e, 0x3f91df0b)
-- 同样支持64位的位运算
mynum:band(UInt64(0xFFFFFFFF, 0))
写一个post dissector
post dissector和dissector比较类似,只是在最后处理的。下面的例子,从已经解析的tree中获取rtp.ssrc,做一些后处理(rtp.ssrc + 1),并在已有的树添加一个新的字段(这个字段可以被过滤):
-- 和普通dissector一样,也是要定义个协议
local wtf_info = Proto("RtpWtf", "Rtp Wtf Post dissector")
-- 和普通协议一样,也是要定义新增字段
local wtf_ssrc_field = ProtoField.uint32("wtf.ssrc_plus_1")
wtf_info.fields = {
wtf_ssrc_field
}
-- Field.new创建字段,是用来从当前的解析树中获取已有数据
local rtp_ssrc_field = Field.new("rtp.ssrc")
function wtf_info.dissector(vtb, pinfo, tree)
-- 如果当前是rtp协议,那么我们就可以获取到非空的rtp.ssrc数据
-- field:__call()返回FieldInfo
-- field.range返回一个TvbRange对象
local rtp_ssrc_ex = rtp_ssrc_field()
if rtp_ssrc_ex == nil then
return
end
local rtp_ssrc = rtp_ssrc_ex.range()(0, 4):uint()
-- 向解析树最后增加一个ssrc,set_generated用于标识这个是生成字段
tree:add(wtf_ssrc_field, rtp_ssrc + 1):set_generated()
end
-- 注册为一个post dissector
register_postdissector(wtf_info)
通过上面的方式,我们就可以在原有解析树基础上,增加一个wtf.ssrc_plus_1字段,并且表示为生成的字段(用[xxx]标识)。

4. 总结
通过上面的介绍,想必大家已经回学会了协议解析了。如果想深入,当然还是要多多阅读官方文档,以及学习下lua语法。遇到不会写的代码,多请教下大模型。