wireshark插件开发 - 自定义协议
搞网络的对于 Wireshark 这个抓包工具应该非常熟悉了,在抓包分析的时候非常好用,很大的一个原因就是 Wireshark 内置了大量的协议解析插件,基本上你叫得上来的协议,Wireshark都能给你解析出来。
网上查了一下相关的资料,发现可以用C去写插件,然后编译成链接库给Wireshark用,比较复杂放弃使用了。
这里采用直接编写LUA脚本由Wireshark解析。
0x01 基础知识
Wireshark内置了对Lua脚本的支持,可以直接编写Lua脚本,无需配置额外的环境,使用起来还是非常方便的。 [Wireshark Developer's Guide]里的第10章和第11章都是关于Lua支持的文档,有需要的话可以详细查阅。
使用Lua编写Wireshark协议解析插件,有几个比较重要的概念:
- Dissector,中文直译是解剖器,就是用来解析包的类,我们最终要编写的,也是一个Dissector。
- DissectorTable,解析器表是Wireshark中解析器的组织形式,是某一种协议的子解析器的一个列表,其作用是把所有的解析器组织成一种树状结构,便于Wireshark在解析包的时候自动选择对应的解析器。例如TCP协议的子解析器 http, smtp, sip等都被加在了"tcp.port"这个解析器表中,可以根据抓到的包的不同的tcp端口号,自动选择对应的解析器。
0x02 一个例子
一个Lua插件的Dissector结构大致如下:
do -- 协议名称为 m_MeteoricProto,在Packet Details窗格显示为 XXX Protocol local struct = Struct local data_dis = Dissector.get("data") local m_MeteoricProto = Proto("meteoric_proto","XXX Protocol") function m_MeteoricProto.dissector(buffer, pinfo, tree) --在主窗口的 Protocol 字段显示的名称为 XX_Protobuf pinfo.cols.protocol:set("XX_Protobuf") if Meteoric_dissector(buffer, pinfo, tree) then else -- data 这个 dissector 几乎是必不可少的; 当发现不是我的协议时, 就应该调用data data_dis:call(buffer, pinfo, tree) end end DissectorTable.get("tcp.port"):add(tcp_port, m_MeteoricProto) end
我们先来看一下上面说的那个封装格式的脚本例子:(--
后面的是注释)
do --协议名称为DT,在Packet Details窗格显示为QAX.TZ DT local p_DT = Proto("DT","QAX.TZ DT") --协议的各个字段 local f_identifier = ProtoField.uint8("DT.identifier","Identifier", base.HEX) --这里的base是显示的时候的进制,详细可参考https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_class_ProtoField local f_length = ProtoField.uint8("DT.length", "Length", base.DEC) local f_data = ProtoField.string("DT.data", "Data", base.ASCII) --这里把DT协议的全部字段都加到p_DT这个变量的fields字段里 p_DT.fields = {f_identifier, f_length, f_data} --这里是获取data这个解析器 local data_dis = Dissector.get("data") local function DT_dissector(buf,pkt,root) local buf_len = buf:len(); --先检查报文长度,太短的不是我的协议 if buf_len < 16 then return false end --验证一下identifier这个字段是不是0x12,如果不是的话,认为不是我要解析的packet local v_identifier = buf(0, 1) if (v_identifier:uint() ~= 0x12) then return false end --取出其他字段的值 local v_length = buf(1, 1) v_length = tonumber(tostring(v_length),16) local v_data = buf(2,v_length) --现在知道是我的协议了,放心大胆添加Packet Details local t = root:add(p_DT,buf) --在Packet List窗格的Protocol列可以展示出协议的名称 pkt.cols.protocol = "DT" --这里是把对应的字段的值填写正确,只有t:add过的才会显示在Packet Details信息里. 所以在之前定义fields的时候要把所有可能出现的都写上,但是实际解析的时候,如果某些字段没出现,就不要在这里add t:add(f_identifier,v_identifier) t:add(f_length,v_length) t:add(f_data,v_data) return true end --这段代码是目的Packet符合条件时,被Wireshark自动调用的,是p_DT的成员方法 function p_DT.dissector(buf,pkt,root) if DT_dissector(buf,pkt,root) then --valid DT diagram else --data这个dissector几乎是必不可少的;当发现不是我的协议时,就应该调用data data_dis:call(buf,pkt,root) end end local tcp_encap_table = DissectorTable.get("tcp.port") --因为我们的自定义协议的接受端口是1314,所以这里只需要添加到"tcp.port"这个DissectorTable里,并且指定值为1314即可。 tcp_encap_table:add(1314, p_DT) end
将其保存为 packet-dt.lua
文件
上面这段代码已经看起来非常清楚了,如果是解析一般的自定义协议,上边的代码基本上够用了。
0x03 Lua插件的启用
想要启用Lua插件,首先要确认你的Wireshark版本是支持Lua的(Windows版本默认应该都是启用支持了的)。可以通过【帮助】-【关于】窗口确认:
如果是这种With Lua的,应该就是可以的了。
然后去文件夹选项卡,找到Global Configuration文件夹的位
在这个文件夹里找到init.lua文件,使用文本文件编辑器打开它,在文件的最后添加:
dofile("c:\\path\\to\\packet-dt.lua")
填写好正确的packet-dt.lua所在的位置,保存文件就可以了。
然后重新启动Wireshark或者点击【分析】-【重新载入Lua插件】,就可以启用你自己的lua插件了。
0x04 测试与调试
测试的话,直接抓包就可以看到对应的包的协议列变成了DT,并且Packet详情窗口里可以看到对应的协议行了。
如果出现问题,Wireshark会直接在对应位置报错,按照报错信息修改packet-dt.lua文件,保存后重新载入Lua插件就可以。
如果沒有自己对应的Pcap包时候,可以通过python的socket来构造pcap包。
客户端代码:
# -*- coding: utf-8 -*- import socket import os import json import time import sys import random from random import randint master_ip = "127.0.0.1" master_port = 1314 socket_token = "qwertyuiopasdfghjklzxcvbnm" ADDRESS = (master_ip, master_port) def generate_random_str(randomlength=16): """ 生成一个指定长度的随机字符串 """ random_str = '' base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz,.' length = len(base_str) - 1 for i in range(randomlength): random_str += base_str[random.randint(0, length)] return random_str def sendall(client,data): header = bytes([18,len(data)]) send_Data = header + data client.sendall(send_Data) def send_data(client, cmd, **kv): global client_type jd = {} jd['COMMAND'] = cmd jd['data'] = kv jd['sault'] = generate_random_str(randint(0, 50)) jsonstr = json.dumps(jd) print('send: ' + jsonstr) sendall(client,jsonstr.encode('utf8')) def recv_data(recv_Date): msg = recv_Date[2:] msg = msg.decode(encoding='utf8') jd = json.loads(msg) cmd = jd['COMMAND'] data = jd['data'] return cmd,data if '__main__' == __name__: client = socket.socket() client.connect(ADDRESS) while True: try: recv_Date = client.recv(1024) cmd,data = recv_data(recv_Date) if 'SendTime' == cmd: time_str = data["time"] print('收到time: {0}'.format(time_str)) tt = time.time() send_data(client, 'RecvTime', time=tt) elif 'Init' == cmd: msg = data["msg"].encode("utf-8") print(msg) send_data(client, 'CONNECT', token=socket_token) except Exception as e: print(e) client.close() break
服务端代码:
import socket # 导入 socket 模块 from threading import Thread import time,os import json import random from random import randint ip = "0.0.0.0" port = 1314 token = "qwertyuiopasdfghjklzxcvbnm" ADDRESS = (ip, port) # 绑定地址 def generate_random_str(randomlength=16): """ 生成一个指定长度的随机字符串 """ random_str = '' base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz,.' length = len(base_str) - 1 for i in range(randomlength): random_str += base_str[random.randint(0, length)] return random_str def sendall(client,data): header = bytes([18,len(data)]) send_Data = header + data client.sendall(send_Data) def send_data(client, cmd, **kv): global client_type jd = {} jd['COMMAND'] = cmd jd['data'] = kv jd['sault'] = generate_random_str(randint(0, 50)) jsonstr = json.dumps(jd) print('send: ' + jsonstr) sendall(client,jsonstr.encode('utf8')) def recv_data(recv_Date): msg = recv_Date[2:] msg = msg.decode(encoding='utf8') jd = json.loads(msg) cmd = jd['COMMAND'] data = jd['data'] return cmd,data if __name__ == '__main__': g_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) g_socket_server.bind(ADDRESS) g_socket_server.listen(5) # 最大等待数(有很多人理解为最大连接数,其实是错误的) client, info = g_socket_server.accept() # 阻塞,等待客户端连接 send_data(client, "Init", msg="connect server successfully! please send token!") while True: try: recv_Date = client.recv(1024) cmd, data = recv_data(recv_Date) client_index = "{0}_{1}".format(info[0], str(info[1])) # 如果是第一次链接 if 'CONNECT' == cmd and data["token"] == token: tt = time.time() send_data(client, "SendTime", time=tt) if 'RecvTime' == cmd: tt = time.time() send_data(client, "SendTime", time=tt) time.sleep(1) except Exception as e: print(e) client.close() break
抓包如下:
0x05 高级一点的玩法
虽然我们实现了基本的包解析功能,但是其实我之前说过,我们的UDP的PayLoad里封装的其实是以太网包,能不能让Wireshark在我们的插件执行完之后,继续按照以太网格式解析其他部分呢?肯定是可以的。
这里,我们只需要重新构造一下需要继续解析的数据,然后获取出一个以太网解析器就可以继续做下去了:
local raw_data = buf(2, buf:len() - 2) Dissector.get("eth_maybefcs"):call(raw_data:tvb(), pkt, root)
把这段添加在刚才的 t:add(f_speed, v_speed)
之后,就可以了。
这里要注意两点,第一点是获取的解析器名称应该是 eth_maybefcs
,这个坑了我很久,因为DissectorTable里写的也是eth,但是提示找不到。网上查了很久之后才发现应该用这个名字去获取,意思是可能带有fcs的eth帧。。。
第二点是raw_data需要调用一下tvb()函数,不然会提示你这个是userdata,不能使用。tvb的全称应该是Testy Virtual Buffer,用来存储Packet buffer的,要处理必须先转成这个。
这样你测试的时候,就可以看到,Packet Details窗口里的"Nselab.Zachary DT"栏的下面,又出现了Ethernet、IP等,这就是内部的数据解析出来的结果。
当然,你也会发现列表的协议栏又被改成了ARP、ICMP等内部协议的名称了,这是因为调用eth_maybefcs
解析器的时候,这些解析器又会给协议栏赋值,覆盖掉我们之前写的DT
。为了和其他的区分,我们还可以玩得更骚气一点,在上面的代码之后加上:
pkt.cols.protocol:append("-DT")
这句话的意思就是不管协议栏被改成了啥,我都在后面加上-DT
,这样ARP、ICMP等就会变成 ARP-DT
、ICMP-DT
了,一眼就可以跟那些平淡无奇的ARP和ICMP区分出来。
0x06 结束语
总的来说,使用Lua来编写Wireshark的协议解析插件还是比较简单的,相对于使用C语言,配置、开发、调试应该都方便了不少。当然,如果要详细开发,肯定还是要多看看官方的开发文档:Wireshark Developer's Guide.
wireshark源代码:https://code.wireshark.org/review/#/admin/projects/wireshark
wireshark开发指南:https://www.wireshark.org/docs/wsdg_html_chunked/
添加自定义协议解析器示例:https://www.wireshark.org/docs/wsdg_html_chunked/ChDissectAdd.html
11.6. Functions For New Protocols And Dissectors (wireshark.org)
参考资料:自己动手编写Wireshark Lua插件解析自定义协议 - 知乎 (zhihu.com)
wireshark自定义协议字段解析_luminais的博客-CSDN博客_wireshark自定义解析协议