概要
在进行网络通信开发与调试的过程中,Wireshark 作为一款强大的网络协议分析工具,常常被用来捕获和分析网络报文。而通过编写 Lua 脚本,我们可以对 Wireshark 捕获的报文进行自定义解析,以便更好地理解和处理特定的协议数据。本文将基于一个示例代码,带领大家入门使用 Lua 脚本解析 Wireshark 抓包报文的方法。
效果演示如下图展示:
整体架构流程
我们可以把整个过程思路简单的分为两个步骤,首先就是在众多信息帧里面找到我们需要进行解析的那一个(教程中1-4步);其次就是对它进行解析(教程中5-6步)。我们的教学也将分为这两大板块,事不宜迟马上开始吧。
对于大家可以直接copy的部分会直接在标题部分进行标注,极大提高大家的效率。
一、准备工作
-
安装 Wireshark :确保已安装 Wireshark 软件,并且版本支持 Lua 脚本扩展。可以在 Wireshark 官方网站下载适合操作系统的安装包进行安装。
-
了解 Lua 基础语法 :虽然本文是入门教学篇,但对 Lua 语言的基本语法有一定了解会更有助于理解后续内容。如果还不熟悉 Lua 语言,可以提前学习一些基础语法知识,如变量定义、函数声明、数据类型等。
二、教学解析第一步(找到目标帧)
本文的示例代码是一个用于解析的普通协议(假设这是一种特定通信协议)报文的 Lua 脚本,以下是本次示例的待解析报文的说明书:
信息域定义 | 字节编号 | 字段 | 长度 | 信息位定义及说明 |
接口信息类型 | 1 | 类型高位 | 2字节 | 0x0102:XX-XXXX接口帧头 |
2 | 类型低位 | |||
发送方标识信息 | 3 | 源ID | 4字节 | 发送方设备ID |
4 | ||||
5 | ||||
6 | ||||
接收方标识信息 | 7 | 目的ID | 4字节 | 接收方设备ID |
8 | ||||
9 | ||||
10 | ||||
数据版本校验信息 [注1] | 11 | 版本一致性信息 | 4字节 | 数据版本校验信息。 |
12 | ||||
13 | ||||
14 | ||||
本方消息序列号 [注2] | 15 | 序列号 | 4字节 | 记录发送本条消息时,本方的周期计数。 |
16 | ||||
17 | ||||
18 | ||||
通信周期 | 19 | 通信周期 | 2字节 | 设备通讯周期,单位: ms |
20 | ||||
对方消息序列号[注2] | 21 | 序列号 | 4字节 | 记录收到对方上一条消息中的对方消息序列号。 默认值:0xFFFFFFFF。[注3] |
22 | ||||
23 | ||||
24 | ||||
收到上一条消息时本方序列号 [注2] | 25 | 序列号 | 4字节 | 记录收到对方上一条消息时,本方的周期计数。 默认值:0xFFFFFFFF。[注3] |
26 | ||||
27 | ||||
28 | ||||
协议版本号 [注1] | 29 | 协议版本号 | 1字节 | 协议版本 |
基于这些信息,我们便可以开始着手解析。
1. 创建解析器对象(可直接copy)
local NAME = "XX-YY_40.0"
local foo = Proto(NAME, "XX-YY_40.0")
--解析器函数 tvb报文内容 pinfo 报文信息 tree解析树结构
function foo.dissector (tvb, pinfo, tree)
end
-- 首先定义foo协议的各个字段
-- 根据foo协议字段类型的不同,分别调用ProtoField的不同方法创建它们,
-- 其中第一个参数是字段的缩写,第2个参数是字段的全名,另外还有一些可选参数表示进制,掩码之类
-- XX-YY
local XX_YYMsgfields = foo.fields
这里首先定义了协议的名称 NAME
,然后使用 Proto
函数创建了一个名为 foo
的协议解析器对象,该对象将用于后续对报文的解析操作。示例中需要解析的帧是由xx发给yy的第40.0版本,诸位可随意修改双引号内的信息。
2. 将解析器在wireshark中注册(copy时请修改关键参数)
DissectorTable.get("udp.port"):add(50001, foo)
这行代码将解析器 foo
注册到 Wireshark 的 UDP 端口解析表中,指定当遇到 UDP 端口号为 50001 的报文时,使用 foo
解析器进行解析。此代码可重复添加,达到同时监管多个端口的作用。
那么问题来了,怎么知道自己需要端口是哪个?现在有三个办法:
办法1:找到开发,用你的拳头质问他写哪个端口上了;
办法2:翻一翻接口说明书,里面大概率标注了应用的端口号,若没有,你可以回到办法1
办法3:假如没有说明书,也找不到开发,那就亲手抓包找到一个原始帧,其中wiresahrk标识出来了源端口和目标端口,如下图所示。
3. 格式化数据(可直接copy)
--格式化数据
local typeFormats = { -- 定义一个表格 typeFormats
[0x01] = function(value) -- 为键 0x01 定义一个匿名函数,参数为 value
return string.format("%d.%d.%d.%d", value[1], value[2], value[3], value[4]) -- 将 value 的前四个元素格式化为字符串
end, -- 匿名函数的结束
} -- typeFormats 表的结束
function formatValue(type, value)
-- 从 typeFormats 表中获取与给定类型关联的格式化函数
local tmp_func = typeFormats[type]
-- 检查是否找到了对应的格式化函数
if(tmp_func) then
-- 调用找到的格式化函数,并返回格式化后的结果
return tmp_func(value)
else
-- 如果没有找到对应的函数,返回空字符串
return ""
end
end
这里定义了一个 typeFormats
表,用于存储不同类型的格式化函数。以键 0x01
对应的函数为例,它将传入的 value
参数的前四个元素格式化为 “a.b.c.d
” 的形式,类似于 IP 地址的格式。formatValue
函数则根据传入的类型 type
,从 typeFormats
表中获取对应的格式化函数,并对 value
进行格式化处理,如果没有找到对应的格式化函数,则返回空字符串。
4. 解析器函数(copy时请修改关键参数)
--解析器函数 tvb报文内容 pinfo 报文信息 tree解析树结构
function foo.dissector (tvb, pinfo, tree)
local length = tvb:len() -- 获取 tvb 的总长度
for offset = 0,length-9 do
local header = tvb(offset, 2):uint() -- 获取 tvb 的前两个数字
if header == 0x0102 then
print("find: XX->YY 0x0102 at offset " .. offset)
XX_YY_dissector_Frame_0x0102(tvb, pinfo, tree, 0x0102, offset)
break
end
end
这是核心的解析器函数,它接收三个参数:tvb
(报文内容)、pinfo
(报文信息)和 tree
(解析树结构)。函数首先获取报文的总长度,然后通过循环遍历报文的每个字节(步长为 1,直到长度减去 9 的位置),在每个偏移量 offset
处提取两个字节的数据作为报文头,并判断其是否等于 0x0102
。如果找到匹配的报文头,打印出找到报文的位置信息,并调用 VOBC_ZC_dissector_Frame_0x0102
函数对后续报文内容进行进一步解析,最后通过 break
语句退出循环,避免重复解析。
大家若是不想动脑,那么我就直接告诉大家需要修改的地方:
1.“if header == 0x0102 then”这行代码是在进行逻辑匹配,示例文档表示我们需要找到关键值0102,由此可确定这是我所需要找到的帧,大家请根据自己的的实际情况来修改判断条件。
2.“print("find: XX->YY 0x0102 at offset " .. offset)”和“XX_YY_dissector_Frame_0x0102(tvb, pinfo, tree, 0x0102, offset)”这两句,前者是在控制台print输出结果,表示程序成功找到了目标帧;后者是跳入对应的解析函数。诸位请修改print的输出内容和函数名为实际情况的。
5. 定义协议字段(copy时请修改关键参数)
--0x0102
XX_YYMsgfields.Frame0102_Type = ProtoField.uint8 (NAME .. ".Frame0102_Type", "帧类型",base.HEX)
XX_YYMsgfields.Frame0102_SENDID = ProtoField.uint16 (NAME .. ".Frame0102_SENDID", "发送方设备ID",base.HEX)
XX_YYMsgfields.Frame0102_RECID = ProtoField.uint16 (NAME .. ".Frame0102_RECID", "接收方设备ID",base.HEX)
XX_YYMsgfields.Frame0102_DATAVERSION = ProtoField.uint16 (NAME .. ".Frame0102_DATAVERSION", "数据版本校验信息",base.HEX)
XX_YYMsgfields.Frame0102_CRC1 = ProtoField.uint16 (NAME .. ".Frame0102_CRC1", "消息序列号",base.HEX)
XX_YYMsgfields.Frame0102_comcycle = ProtoField.uint8 (NAME .. ".Frame0102_comcycle", "设备通讯周期,单位: ms",base.HEX)
XX_YYMsgfields.Frame0102_CRC2 = ProtoField.uint16 (NAME .. ".Frame0102_CRC2", "记录收到对方上一条消息中的对方消息序列号",base.HEX)
--XX_YYMsgfields.Frame0102_AGVERSION = ProtoField.uint (NAME .. ".Frame0102_AGVERSION", "协议版本",base.HEX)
在这一部分,通过 foo.fields
创建了协议中各个字段的描述信息,包括字段的缩写、全名、数据类型(如 uint8
、uint16
等)以及显示进制(base.HEX
表示以十六进制显示)。这些字段将用于在解析报文时,对报文中的对应数据进行提取和展示。依据则是我们上方的接口说明书。
6. 解析函数方法体(copy时请修改关键参数)
function XX_YY_dissector_Frame_0x0102(tvb, pinfo, tree, msgid, offset)
local MSGtree0102 = tree:add("VBOC→ZC:通用信息包(0x0102)")
--2 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_Type, tvb(offset, 2))
offset = offset + 2
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_SENDID, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_RECID, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_DATAVERSION, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_CRC1, tvb(offset, 4))
offset = offset + 4
--2 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_comcycle, tvb(offset, 2))
offset = offset + 2
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_CRC2, tvb(offset, 4))
offset = offset + 4
--1 帧长度
--MSGtree0102:add(XX_YYMsgfields.Frame0102_AGVERSION, tvb(offset, 1))
--offset = offset + 1
end
该函数专门用于解析帧类型为 0x0102
的报文。它首先在解析树 tree
中添加一个表示该帧类型信息的节点 MSGtree0102
,然后按照协议规定的格式,依次提取报文中的各个字段数据,并将其添加到 MSGtree0102
节点下。每次提取字段数据后,都会更新偏移量 offset
,以便正确地定位到下一个字段的位置。通过这种方式,可以将整个报文的内容按照协议结构清晰地展示在 Wireshark 的解析树中。
三、运行与测试
-
将 Lua 脚本保存为文件 :将上述示例代码保存为一个
.lua
文件,例如 “XX-YY.lua
”,并将其放置在 Wireshark 的 Lua 脚本目录下。不同操作系统的 Wireshark Lua 脚本目录位置可能有所不同,如windows操作系统下应在如下地址:
注意,4.2为当前我的wireshark版本号,实际情况诸位放在对应的版号路径下即可。 -
启动 Wireshark 并加载脚本 :重新启动 Wireshark,它会自动加载指定目录下的 Lua 脚本。
-
打开wireshark的调试框:
首先在wireshark的菜单栏中找到编辑-首选项
然后在高级选项中搜索框输入“console”,找到gui.console_open,将值改为always,最后重启wireshark即可。
重新开始抓包后,应当在console窗口出现我们代码中print的信息
捕获报文并查看解析结果 :使用 Wireshark 捕获包含目标协议报文的网络流量,然后在捕获的报文列表中查找符合 UDP 端口 50002 的报文。对于这些报文,Wireshark 应该会使用我们编写的 Lua 脚本进行解析,并在解析树中展示出按照协议结构解析后的字段信息。至于效果图中对字段的解释性描述信息,我准备放到下一篇博客中作为进阶教学。
LUA源码分享
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 小桐桐.
--- DateTime: 2025/2/10 11:28
---
-- 创建解析器对象
local NAME = "XX-YY_40.0"
local foo = Proto(NAME, "XX-YY_40.0")
--解析器函数 tvb报文内容 pinfo 报文信息 tree解析树结构
function foo.dissector (tvb, pinfo, tree)
end
----将解析器在wireshark中注册
DissectorTable.get("udp.port"):add(50002, foo)
-- 首先定义foo协议的各个字段
-- 根据foo协议字段类型的不同,分别调用ProtoField的不同方法创建它们,
-- 其中第一个参数是字段的缩写,第2个参数是字段的全名,另外还有一些可选参数表示进制,掩码之类
-- XX-YY
local XX_YYMsgfields = foo.fields
--格式化数据
local typeFormats = { -- 定义一个表格 typeFormats
[0x01] = function(value) -- 为键 0x01 定义一个匿名函数,参数为 value
return string.format("%d.%d.%d.%d", value[1], value[2], value[3], value[4]) -- 将 value 的前四个元素格式化为字符串
end, -- 匿名函数的结束
} -- typeFormats 表的结束
function formatValue(type, value)
-- 从 typeFormats 表中获取与给定类型关联的格式化函数
local tmp_func = typeFormats[type]
-- 检查是否找到了对应的格式化函数
if(tmp_func) then
-- 调用找到的格式化函数,并返回格式化后的结果
return tmp_func(value)
else
-- 如果没有找到对应的函数,返回空字符串
return ""
end
end
--解析器函数 tvb报文内容 pinfo 报文信息 tree解析树结构
function foo.dissector (tvb, pinfo, tree)
local length = tvb:len() -- 获取 tvb 的总长度
for offset = 0,length-9 do
local header = tvb(offset, 2):uint()
if header == 0x0102 then
print("find: XX->YY 0x0102 at offset " .. offset)
XX_YY_dissector_Frame_0x0102(tvb, pinfo, tree, 0x0102, offset)
break
end
end
--0x0102
XX_YYMsgfields.Frame0102_Type = ProtoField.uint8 (NAME .. ".Frame0102_Type", "帧类型",base.HEX)
XX_YYMsgfields.Frame0102_SENDID = ProtoField.uint16 (NAME .. ".Frame0102_SENDID", "发送方设备ID",base.HEX)
XX_YYMsgfields.Frame0102_RECID = ProtoField.uint16 (NAME .. ".Frame0102_RECID", "接收方设备ID",base.HEX)
XX_YYMsgfields.Frame0102_DATAVERSION = ProtoField.uint16 (NAME .. ".Frame0102_DATAVERSION", "ZC管辖范围内,ZC与VOBC间的数据版本校验信息",base.HEX)
XX_YYMsgfields.Frame0102_CRC1 = ProtoField.uint16 (NAME .. ".Frame0102_CRC1", "消息序列号,记录发送本条消息时,本方的周期计数。",base.HEX)
XX_YYMsgfields.Frame0102_comcycle = ProtoField.uint8 (NAME .. ".Frame0102_comcycle", "设备通讯周期,单位: ms",base.HEX)
XX_YYMsgfields.Frame0102_CRC2 = ProtoField.uint16 (NAME .. ".Frame0102_CRC2", "记录收到对方上一条消息中的对方消息序列号",base.HEX)
--XX_YYMsgfields.Frame0102_AGVERSION = ProtoField.uint4 (NAME .. ".Frame0102_AGVERSION", "ZC-VOBC的协议版本",base.HEX)
function XX_YY_dissector_Frame_0x0102(tvb, pinfo, tree, msgid, offset)
local MSGtree0102 = tree:add("VBOC→ZC:通用信息包(0x0102)")
--2 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_Type, tvb(offset, 2))
offset = offset + 2
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_SENDID, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_RECID, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_DATAVERSION, tvb(offset, 4))
offset = offset + 4
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_CRC1, tvb(offset, 4))
offset = offset + 4
--2 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_comcycle, tvb(offset, 2))
offset = offset + 2
--4 帧长度
MSGtree0102:add(XX_YYMsgfields.Frame0102_CRC2, tvb(offset, 4))
offset = offset + 4
--1 帧长度
--MSGtree0102:add(XX_YYMsgfields.Frame0102_AGVERSION, tvb(offset, 1))
--offset = offset + 1
end
小结
通过本篇博客的学习,我们了解了如何使用 Lua 脚本对 Wireshark 抓包报文进行自定义解析的基本流程,包括创建解析器对象、注册解析器、定义协议字段、格式化数据以及编写解析器函数等关键步骤。在实际的网络通信开发与调试工作中,我们可以根据具体的协议规范,灵活地运用 Lua 脚本对 Wireshark 进行扩展,实现对各种复杂协议报文的精准解析,从而更高效地进行问题定位与故障排查。
当然,这只是一个入门级别的示例,Lua 脚本在 Wireshark 中的应用还有很多更高级的功能和技巧等待我们去探索。例如,可以结合 Wireshark 的其他 API 函数,实现对报文的进一步处理和分析,如统计特定字段的值分布、过滤特定条件的报文等,这些将作为进阶知识放到我的下一篇博客中讲解。我使用的是pycharm进行编写lua脚本,若对此感兴趣可以翻看我的第一篇博客,这将有助于大家从代码中排查错误,提高效率。
希望本篇博客能够为大家打开使用 Lua 脚本解析 Wireshark 报文的大门,让大家在今后的网络通信工作中能够更加得心应手地运用这一强大的工具。