搞網絡的對於 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自定義解析協議