wireshark插件开发 - 自定义协议

搞网络的对于 Wireshark 这个抓包工具应该非常熟悉了,在抓包分析的时候非常好用,很大的一个原因就是 Wireshark 内置了大量的协议解析插件,基本上你叫得上来的协议,Wireshark都能给你解析出来。

网上查了一下相关的资料,发现可以用C去写插件,然后编译成链接库给Wireshark用,比较复杂放弃使用了。

这里采用直接编写LUA脚本由Wireshark解析。

0x01 基础知识

Wireshark内置了对Lua脚本的支持,可以直接编写Lua脚本,无需配置额外的环境,使用起来还是非常方便的。 [Wireshark Developer's Guide]里的第10章和第11章都是关于Lua支持的文档,有需要的话可以详细查阅。

使用Lua编写Wireshark协议解析插件,有几个比较重要的概念:

  1. Dissector,中文直译是解剖器,就是用来解析包的类,我们最终要编写的,也是一个Dissector。
  2. 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-DTICMP-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自定义解析协议

 

posted @ 2021-06-16 10:46  m0w3n  阅读(4218)  评论(0编辑  收藏  举报