前言
Wireshark可以显示客户端和服务器之间的数据包交互。对于游戏服务器来说,一般使用的都是自定义的协议,协议中包含protobuf格式的真实数据。这时想让Wireshark显示这些自定义协议就需要单独开发插件。
这里使用相对简单的lua插件的方式进行开发。开发完成后属于有用,但没那么有用,不过胜在开发成本低,还能学习到项目框架协议的编解码.
可以解决的问题
- 客户端到底有没有收到数据包。虽然游戏中打印了日志,但消息一般会经过多个流程,最终客户端有没有收到确实有待检查。
- 后台想要得知前台使用了什么协议。比如登录会发送登陆包,如果能够通过Wireshark看到登陆包,就能直接得知本次发送的协议名称和协议内容,方便查看对应的代码。
- 客户端or服务器发送的数据包有没有问题,省去加日志的步骤。
Wireshark相关支持介绍
- wireshark本身支持protobuf数据的解析,给定proto、message名称和message序列化后的数据就能以树状的形式展示在wireshark中。同时天然支持按字段进行索引。
- wireshark的lua插件目前没有找到合适的解密库的使用方法,所以需要关闭客户端和服务器的加密后使用。
- wireshark会以tcp包为单位提供数据包,所以只需要从数据包中编码后的自定义协议中拿到message的数据和message的名称就能完成解析
lua插件
lua插件基本格式
-- 自定义的协议名称
local proto_name = "tsf4g"
-- 服务器端口 wireshark通过端口选择协议来解析
local server_ports = {10000}
local proto = Proto(proto_name, proto_name)
-- 源端口的获取方式
-- 由于C->S和S->C之间的协议格式不一定是一致的 所以需要进行区分
srcport = Field.new("tcp.srcport")
-- 个性化字段定义 - 位的显示
-- 最后的0xf0代表取字节的高4位
crypto_type_field_enum = {
[1] = "unencrypted", -- 高4位是0001时 将会显示unencrypted
[2] = "encrypted" -- 高4位是0002时 将会显示encrypted
}
crypto_type_field = ProtoField.uint8("crypto_type", "crypto_type", base.DEC, crypto_type_field_enum, 0xf0)
-- 个性化字段定义 - 指定格式的显示
-- 这里是uint32格式和string两种格式
flag_field = ProtoField.uint32("flag", "flag", base.DEC, flag_field_enum)
debug1 = ProtoField.string("debug1", "debug1")
-- 注册字段
proto.fields = {
crypto_type_field,
flag_field,
debug1
}
-- 算是个工具函数 从数据中以protobuf序列化后的格式读取一个数字
function read_number(tvb, begin_sub)
local sub = begin_sub
local number = 0
local bit_lshift = 0
repeat
local num = tvb(sub, 1):uint()
local t_num = bit32.band(num, 0x7f)
number = bit32.bor(bit32.lshift(t_num, bit_lshift), number)
bit_lshift = bit_lshift + 7
sub = sub + 1
if sub >= tvb:len() then
break;
end
until (bit32.band(num, 0x80) == 0)
return number
end
-- 这里的tvb可以理解为一个数组,tree则是wireshark左下角的树状展示所需要的数据
function proto.dissector(tvb, pinfo, tree)
if tvb:len() > max_pack_size then
return false
end
pinfo.cols.protocol = proto_name
-- 判断是客户端发送到服务器 还是 服务器发送到客户端
-- local srcport = srcport();
-- svr_to_client = false
-- for i, port in ipairs(server_ports) do
-- if tostring(srcport) == tostring(port) then
-- svr_to_client = true
-- end
-- end
local sub = 0
-- 从数据的开始位置读取4个字节,并作与运算 得到包长
local pkg_size = bit32.band(tvb(sub, 4):uint(), 0x00ffffff)
-- 在树中创建一个子树,这里的长度会影响选中后的高亮
local subtree = tree:add(proto, tvb(0, pkg_size), "tsf4g")
-- 在子树中继续创建子树
local header = subtree:add(proto, tvb(), "header")
-- 添加字段用于展示
-- 由于在定义crypto_type_field设置了0xf0,所以这里实际展示内容为 第一个字节的高4位
header:add(crypto_type_field, tvb(sub, 1))
-- 字段判断,这里判断的是有无开启加密
local crypto_type = bit32.rshift(bit32.band(tvb(sub, 4):uint(), 0xf0000000), 28)
sub = sub + 1
if not(crypto_type == 1) then
return true
end
-- 添加字段的同时 添加注释
local cmd = tvb(sub, 2):uint()
header:add(cmd_field, tvb(sub, 2)):append_text(" [0 (Game Proto), other (tsf4g proto)]")
sub = sub + 2
-- 这里已经将头部数据解析完毕 sub开始到data_size就是protobuf数据了。
if msg_type == 2 then
-- 获取protobuf解释器
local protobuf_dissector = Dissector.get("protobuf")
-- 设置协议名称
pinfo.private["pb_msg_type"] = "message,main.CSMsg"
-- 给定protobuf序列化后的数据 进行解析
pcall(Dissector.call, protobuf_dissector, tvb(sub, data_size), pinfo, tree)
Dissector.call(protobuf_dissector, tvb(sub, data_size):tvb(), pinfo, subtree)
end
-- 由于一个包中可能有多条message 这里嵌套进行解析
sub = sub + data_size
if sub < tvb:len() then
proto.dissector(tvb(sub):tvb(), pinfo, tree)
end
end
local udp_port_table = DissectorTable.get("tcp.port")
for i, port in ipairs(server_ports) do
udp_port_table:add(port,proto)
end
wireshark安装插件和相关设置
Wireshark安装后的根目录中存在init.lua文件
if not running_superuser or run_user_scripts_when_superuser then
dofile(DATA_DIR.."console.lua")
-- 添加自定义的lua插件
dofile("D:\\tools\\wireshark1.0\\dict.lua")
end
存在的问题
- 暂不清楚wireshark如何处理的书中经常提到的tcp数据流本身没有分界线的问题。编写插件时,wireshark提供的数据包似乎不用考虑截断的问题,最多考虑下很少出现的粘包情况。
- 没有找到合适的加解密库的使用方式,一般协议使用aes加密,即使知道加密流程也没有库来调用进行解密。
- 当一个序列化后的数据包超过1460后会导致分包,这时候上述的方式无法解析,这个由于基本遇不到也就没有处理。
- 看过其他的插件是用的是将数据放到buff中对buff进行解析,这是更为合适的方式,不过目前直接解析也够用。