在网络开发与调试中,我们通常使用 Wireshark 进行抓包分析。Wireshark 本身提供了大量常用协议的解析器,方便我们对抓包内容进行详细分析,比如 http、tcp、udp 等数据包,我们都可以通过 Wireshark 直观的查看相关协议头、payload 等信息。在游戏项目中,我们经常使用自定义的前后台通信协议,这部分内容 Wireshark 就无法直接解析展示,分析起来比较麻烦。为了提高私有协议的问题分析效率,我们尝试使用 Wireshark 提供的插件扩展的方式,实现私有协议解析器。

实现私有协议解析插件的核心即为编写对应的协议 Dissector。解析器(Dissector)是 Wireshark 的 “翻译官”,负责将捕获的二进制数据包转换为人类可读的协议字段(如协议头、载荷内容)。Wireshark 支持两种方式实现私有协议解析插件:

  • Lua 脚本插件:轻量化、无需编译、开发速度快,适合简单协议或快速验证场景。
  • C++ 原生插件:性能强、支持复杂协议,但需配置编译环境,适合对效率要求高的场景。

Lua 解析器插件

Lua 是 Wireshark 内置的脚本语言,无需编译,直接编写脚本即可实现解析功能,适合一些简单协议的快速开发。

解析器开发

官方文档提供了 Lua API 实现解析器的相关说明,我们可以根据其官网文档 Chapter 11. Wireshark’s Lua API Reference Manual 章节内容,实现私有协议解析器的开发。

核心 API

Wireshark Lua API

创建协议对象

首先,需要创建一个协议对象,协议名称、描述等信息会用于在 wireshark 的界面展示或过滤器筛选。

1
2
3
-- 语法:Proto("协议名称", "协议描述"),
-- 协议名称用于过滤,例如在 Wireshark 过滤栏输入 uap
local myapp_proto = Proto("UAP","GCloud.UAP")

capture-uap

定义协议字段 (ProtoField)

接着需要定义协议中每个字段的元数据,如名称、数据类型、显示格式等。这些字段将在 Wireshark 的 Packet Details 面板中显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 为了方便管理,我们将所有字段存储在协议对象的 fields 表中
local fields = myapp_proto.fields

-- 定义头部字段
fields.magic = ProtoField.uint16("uap.magic","Magic", base.HEX)
fields.version = ProtoField.uint16("uap.version","Version", base.DEC)
fields.command = ProtoField.uint16("uap.commond", "CMD", base.HEX, {
[0x01] = "UAP-SYN",
[0x02] = "UAP-ACK",
[0x03] = "UAP-HEART"
})

-- 定义数据部分字段
-- 注意:payload 字段的长度是动态的,我们在解析函数中处理
fields.payload = ProtoField.bytes("uap.body.data", "Body[raw]", base.NONE)

实现解析器主函数 (dissector)

这是脚本的核心逻辑。当 Wireshark 捕获到一个与我们协议关联的数据包时,这个函数就会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- 语法:function 协议对象.dissector(tvb, pinfo, tree)
-- tvb: Testy Virtual Buffer,包含了数据包的原始字节。
-- pinfo: Packet Info,包含了数据包的元信息,如时间戳、源地址、目的地址等。
-- tree: Protocol Tree,用于在 Packet Details 面板中构建树形结构。
function myapp_proto.dissector(tvb, pinfo, tree)
-- 1. 设置数据包的基本信息
-- 将 Protocol 列设置为我们的协议名称
pinfo.cols.protocol = myapp_proto.name
-- 将 Info 列设置为更详细的描述,这里我们用命令名称
local command_val = tvb(2, 1):uint() -- 先读取 command 值
local command_str = fields.command.descriptions[command_val] or "Unknown"
pinfo.cols.info = string.format("MyApp: %s (v%d)", command_str, tvb(1, 1):uint())

-- 2. 构建 Packet Details 面板中的树形结构
-- 添加一个主节点
local subtree = tree:add(myapp_proto, tvb(), "My Application Protocol Data")

-- 按照协议格式,依次添加各个字段
-- 语法:subtree:add(字段定义, tvb切片)
subtree:add(fields.magic, tvb(0, 2))
subtree:add(fields.version, tvb(1, 1))
subtree:add(fields.command, tvb(2, 1))
subtree:add(fields.length, tvb(3, 2))

-- 3. 处理变长的 Payload
-- 从头部的 Length 字段获取总长度
local total_length = tvb(3, 2):uint()
-- 计算 payload 的长度:总长度 - 头部长度 (0+2 + 1 + 1 + 2 = 6 字节)
local payload_length = total_length - 6

-- 检查 payload 是否存在且长度合法
if payload_length > 0 and tvb:len() >= total_length then
-- 添加 payload 字段
local payload_item = subtree:add(fields.payload, tvb(6, payload_length))

-- 4. (可选) 根据 Command 字段解析 Payload
if command_val == 0x01 then
-- 如果是登录请求,我们假设 payload 是 "usernamepassword" 格式
-- 这里简单地将其作为字符串显示
payload_item:append_text(" (Username: " .. tvb(6, 8):string() .. ", Password: " .. tvb(14, 6):string() .. ")")
elseif command_val == 0x02 then
-- 如果是数据传输,可以有更复杂的解析逻辑
payload_item:append_text(" (Data Content)")
end
end
end

将解析器注册到 Wireshark

此步骤用于告诉 Wireshark 什么时候应该使用我们的解析器。Wireshark 中有两种解析器:

  • Heuristic-dissector(启发式解析器),直接解析原始数据。
  • Post-dissector(后置解析器),会在标准解析器之后触发,依赖标准解析器解析出的协议字段。(比如 DNS 协议是依赖 tcp.port 为 53的前置解析结果)
1
2
3
4
5
6
--场景1:仅需要针对tcp 的 8085 端口情况进行解析,因此添加到"tcp.port"这个DissectorTable。
--local tcp_table = DissectorTable.get("tcp.port")
--tcp_table:add(8085, myapp_proto)

--场景2:需要针对所有的TCP数据进行解析,因此加入到 tcp 的启发式解析中
myapp_proto:register_heuristic("tcp", Dissector_UAP)

对于注册的解析器,可以在 Wireshark 解析器表中查看。

DissectortTables

部署与加载脚本

第三方的 lua 插件统一放置在 Wireshark 的 Lua 插件目录。可以通过在 Wireshark 中点击 帮助 -> 关于 Wireshark -> 文件夹 来找到这个目录,通常是:

  • Windows: C:\Program Files\Wireshark\lua\plugins 或 %APPDATA%\Wireshark\plugins
  • macOS: ~/Library/Application Support/Wireshark/plugins

wiresshark-about

放置完脚本后,点击工具 -> Lua -> 重新加载 Lua 脚本,即可完成私有协议解析器加载,如果当前分析的抓包文件中包含私有协议消息,则会直接在 wireshark 界面中展示出来。Wireshark 在启动时也会自动加载插件目录下的所有 .lua 文件。

注意:如果自定义协议解析器太多,或协议比较复杂,会影响抓包文件的加载速度,这时可以通过停用协议的方式暂停解析器加载,待抓包文件载入完成后,再根据需要启用对应解析器。

C++ 解析器插件

对于复杂的协议,尤其是涉及到加解密,或者 pb/tdr 等序列化算法时,使用 lua 开发需要重新实现相关算法,耗时耗力。这时就需要借助 Wireshark 的 C++ 层开发能力。

Wireshark 本身就是基于 C++ 开发,其代码也是开源的,官方文档也提供了 C++ API 实现解析器的相关说明,我们可以根据其 官网文档 Chapter 9. Packet Dissection 章节内容,实现私有协议解析器的开发。

但是在实践中我们发现,C++ 插件的开发,依赖 wireshark 项目中的很多接口符号,需要跟 wireshark 项目一起编译才行,导致我们必须额外熟悉并搭建 wireshark 项目的开发环境,极大提高了开发门槛。
经过探索尝试,发现可以通过以下方式绕开 wireshark 核心逻辑的编译,将开发重心重新转移到自定义解析器的实现上来。

解决头文件依赖、动态库链接等问题

获取头文件

Wireshark 代码是开源的,我们可以根据自己安装的版本,直接从 githlab 下载对应的源码,以获取到我们开发所需要的头文件。然后将头文件路径添加到我们的解析器开发工程(dll 动态库工程)中。

1
2
{path}\wireshark
{path}\wireshark\include

获取 glib2 头文件

由于 wireshark 的头文件存在 glib2 的引用,但这部分依赖不在源码中,因此,还要额外准备 glib2 头文件。
根据官网文档 5. Library Reference 章节,Wireshark Windows 平台所有的依赖库可以在 https://dev-libs.wireshark.org/windows/ 下载获得。

下载所需的 vcpkg-export 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vcpkg-export-xxxxxxxx-x-x64-windows-ws
├── installed
│ ├── ...
│ └── x64-windows
│ ├── bin
│ │ ├── ...
│ │ ├── glib-2.0-0.dll
│ │ ├── glib-2.0-0.pdb
│ │ └── ...
│ ├── include
│ │ ├── ...
│ │ ├── glib-2.0
│ │ │ ├── ...
│ │ │ ├── glib.h
│ │ │ └── ...
│ │ └── ...
│ ├── lib
│ │ ├── ...
│ │ ├── glib-2.0
│ │ │ └── include
│ │ │ └── glibconfig.h
│ │ ├── glib-2.0.lib
│ │ └── ...
│ └── ...
└── ...

将 glib 的头文件路径添加到到我们的解析器开发工程(dll 动态库工程)中即可。

1
2
{path}\vcpkg-export-xxxxxxxx-x-x64-windows-ws\installed\x64-windows\lib\glib-2.0\include
{path}\vcpkg-export-xxxxxxxx-x-x64-windows-ws\installed\x64-windows\include\glib-2.0

wireshark-win-header.png

链接 libwireshark.dll

配置好 Wireshark 和 glib 头文件,解决了代码的编译问题。但是,最终生成 dll 还是需要依赖链接 Wireshark 中的相关接口符号。

我们直接可以将自定义解码器源码合入 Wireshark 开源项目中,编译出带自定义解码器的完整 Wireshark 工具(exe)。但是,这样要搭建整个 Wireshark 编译环境,比较麻烦。而且,不利于自定义解码器的共享,每个人都需要安装带特定解码器的 Wireshark 版本才行。

我们安装的公版 Wireshark 已经带了 libwireshark.dll,通过反编译工具可以查到开发解析器需要使用的 api 符号都未隐藏,如果解析器开发工程能直接链接使用,就可以生成独立的自定义解码器插件(dll),方便在任何版本的 wireshark 中直接使用。

所以,问题的关键变为如何使用 Wireshark 中不带 .lib 的 libwireshark.dll。

  1. 手动编写 libwireshark.def 定义需要导出的函数。(wireshark 的导出函数,使用 WS_DLL_PUBLIC 宏标记 )
1
2
3
4
EXPORTS
tvb_address_to_str
tvb_address_var_to_str
...
  1. 使用 dumpbin.exe 查看 libwireshark.dll 是32位还是64位。
    1
    2
    3
    # C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.16.27023\bin\HostX64\x64

    .\dumpbin.exe /headers libwireshark.dll
  2. 使用 lib.exe 从 .def 生成 libwireshark.lib
    1
    2
    3
    # C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.16.27023\bin\HostX64\x64

    .\lib.exe /def:libwireshark.def /machine:x64 /out:libwireshark.lib

有了 libwireshark.lib,就可以在 vs 中直接链接 libwireshark.dll 了。

wireshark-win-lib

解析器开发

C++ 解析器的开发逻辑基本同 lua 一致,基本上将对应 lua api 替换为 c++ api 即可。

核心 API

Wireshark-CPP-DissectorAPI

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

void proto_register_uap(void) {
static gint* ett[] = {
& ett_uap,
& ett_uap_syn,
& ett_uap_ack,
& ett_uap_heartbeat,
& ett_uap_cstop,
& ett_uap_sstop,
& ett_uap_data
};
proto_register_subtree_array(ett, array_length(ett));

// 协议定义
proto_uap = proto_register_protocol(
"GCloud UAP Protocol", // 名称
"UAP", // 简短名称
"uap" // 过滤器名称
);

// 注册字段
proto_register_field_array(proto_uap, hf_uap_fields, array_length(hf_uap_fields));
//...
}

void proto_reg_handoff_uap(void) {
dissector_handle_t uap_handle = create_dissector_handle(dissect_uap_packet, proto_uap);
//dissector_add_uint("tcp.port", 40926, uap_handle); // 添加TCP端口
// 注册所有TCP端口
// 将范围添加到解码器表中
dissector_add_uint_range_with_preference("tcp.port", "1-65535", uap_handle);
}

static int dissect_uap_packet(tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree) {
// 确保数据包长度足够
if (tvb_captured_length(tvb) < 14) return 0;

// 验证 identifier
if (tvb_get_guint16(tvb, 0, ENC_BIG_ENDIAN) != 0x????) return 0;

col_set_str(pinfo->cinfo, COL_PROTOCOL, "GCloud-UAP");

...
guint16 headlen = tvb_get_ntohs(tvb, 8);
guint32 bodylen = tvb_get_ntohl(tvb, 10);

// 创建协议树
proto_item* item = proto_tree_add_item(tree, proto_uap, tvb, 0, -1, ENC_NA);
proto_tree * uap_tree = proto_item_add_subtree(item, ett_uap);
proto_item_append_text(uap_tree, ", HeadLen: %u", headlen);
proto_item_append_text(uap_tree, ", BodyLen: %u", bodylen);


// 添加字段到协议树
proto_tree_add_item(uap_tree, hf_uap_magic, tvb, 0, 2, ENC_BIG_ENDIAN);
proto_tree_add_item(uap_tree, hf_uap_headlen, tvb, 8, 2, ENC_BIG_ENDIAN);
proto_tree_add_item(uap_tree, hf_uap_bodylen, tvb, 10, 4, ENC_BIG_ENDIAN);

//调用相应的解剖函数
switch (cmd) {
case 0x1024:
return dissect_uap_syn_head(tvb, pinfo, uap_tree);
case 0x1024:
return dissect_uap_ack_head(tvb, pinfo, uap_tree);
case 0x1024:
return dissect_uap_heartbeat(tvb, pinfo, uap_tree);
case 0x1024:
return dissect_uap_cstop(tvb, pinfo, uap_tree);
case 0x1024:
return dissect_uap_sstop(tvb, pinfo, uap_tree);
case 0x1024:
case 0x1024:
return dissect_uap_data(tvb, pinfo, uap_tree);
default:
break;
}

return tvb_captured_length(tvb);
}

// 插件注册
void plugin_register(void) {
static proto_plugin plug;

plug.register_protoinfo = proto_register_uap;
plug.register_handoff = proto_reg_handoff_uap;
proto_register_plugin(&plug);
}

编译与部署

将编译生成的 dll 放在 Wireshark 的安装目录即可。

lua 与 C++ 插件方案对比

维度 C++ 原生插件 Lua 脚本插件
性能 高(直接调用核心 API) 中(脚本解释执行)
开发效率 低(需配置环境、编译) 高(无编译、实时生效)
复杂度支持 支持复杂协议(加密、嵌套) 适合简单协议
部署成本 需分发动态库,与wireshark版本耦合性强 仅需分发脚本文件,兼容性强

C++ 与 Lua 交互(混合方案)

C++ 方案由于涉及到 Wireshark 接口导出、新的 API 用法学习,成本会相对比较高。
通常我们会采用折中的方案,即 “Lua 调用 C++” 的混合方案:用 C++ 实现核心解析逻辑,暴露接口给 Lua,兼顾灵活性与性能。

  • Lua 层:注册 Wireshark 解析器,具体协议执行调用 C++ 注册的函数,处理复杂逻辑后继续解析。
  • C++ 层:编写函数(如 decrypt_payload 解密载荷),用 lua_register 注册到 Lua 环境。

这样,C++ 层开发时,只需要引入 lua 开发环境及自身逻辑所需要的依赖,大大降低了开发成本。