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自定義解析協議

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM