目前的工作都是在設計自己的一些通訊協定,所以有時都會需要觀察封包收送的內容,但一直是利用 printk / printf 的方式來做。也曾經想過是不是要弄的高級一點,弄成相容於 tcpdump / wireshark 的格式,再寫個解析器,看起來就很炫酷。但公司就是案子來來去去,很多都是一次性的專案,寫這種東西不太划算。
最近,終於有個做了很久的案子拿到了,而且客戶將來也需要客制化,所以就花了點時間研究一下 wireshark 解析與截取檔案,這樣會比較方便跟客戶吵架合作。
Wireshark 封包解析
本文的範例是參考這篇,再修改成要的樣子而來,並沒有瞭解太深入,主要是以應用為主。而本也只用到了部份功能,有更複雜需求的,可以參考 Wireshark 的文件或 Google 尋找 “Wireshark dissector lua”。原本的參考文章有提到 bool field,也是很值得參考。
Wireshark 支援以 lua 程式外掛的封包解析器,以下是我的程式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
do local p_vxlan = Proto("siml1", "SimL1"); local f_cmd = ProtoField.string("siml1.cmd", "cmd") local f_src = ProtoField.uint32("siml1.src", "src", base.OCT) local f_dst = ProtoField.uint32("siml1.dst", "dst", base.OCT) local f_msgtype = ProtoField.uint32("siml1.msgtype", "msgtype", base.OCT) local f_data_len = ProtoField.uint32("siml1.data_len", "data_len") local f_data_txpwr = ProtoField.uint32("siml1.data_txpwr", "data_txpwr") local f_data_snr = ProtoField.uint32("siml1.data_snr", "data_snr") local f_data_map1 = ProtoField.uint8("siml1.data_map1", "data_snr", base.OCT, {"desc1","desc2"}, 0x0F) local f_data_map2 = ProtoField.uint8("siml1.data_map2", "data_snr", base.OCT, {"desc1","desc2"}, 0xF0) local f_data_blob = ProtoField.bytes("siml1.data_blob", "data_blob") p_vxlan.fields = { f_cmd, f_src, f_dst, f_msgtype, f_data_len, f_data_txpwr, f_data_snr} table.insert(p_vxlan.fields, f_data_map1) table.insert(p_vxlan.fields, f_data_map2) table.insert(p_vxlan.fields, f_data_blob) function p_vxlan.dissector(buf, pinfo, root) local t = root:add(p_vxlan, buf(0,25)) local buflen = buf:len() t:add_le(f_src, buf(0,4)) t:add_le(f_dst, buf(4,4)) t:add_le(f_msgtype, buf(8,4)) t:add_le(f_data_len, buf(12,4)) t:add_le(f_data_txpwr, buf(16,4)) t:add_le(f_data_snr, buf(20,4)) t:add_le(f_data_map1, buf(24,1)) t:add_le(f_data_map2, buf(24,1)) t:add(f_data_blob, buf(25,buflen-25)) --[[ local eth_dis = Dissector.get("eth_withoutfcs") eth_dis:call(buf(12):tvb(), pinfo, root) --]] local msgtype = buf(8,4):le_uint() if (msgtype == 0) then t:append_text(", DATA") elseif(msgtype == 1) then t:append_text(", PING") elseif(msgtype == 2) then t:append_text(", PONG") end end local udp_encap_table = DissectorTable.get("udp.port") udp_encap_table:add(1234, p_vxlan) end |
以下說明都是個人心得(直覺),請酌情參考。
L2: 以Wireshark內建函數 Proto() 建立了一個解析物件 p_vxlan,用以存入其它設定。Proto() 第一個參數是filter名稱,第二個是說明。
L4~L14: 定義 protocol 中的欄位名稱與類型,此處尚位表明其位置。 ProtoField.* 後面接的是其欄位性質,如uint32, uint8。所有列表可參考官方文件的 11.3.7.2 ProtoField 的說明。ProtoFile.*() 第一個是其 filter 的子名稱,第2個是說明,第3個是表現型式(10, 16 進位)。L12,13 最後面使用了 mask,代表其是 bit fields。map1 / map2 分別取了上下 nibble。
L16~L19: 將這些欄位定義加入 p_vxlan 中。可以使用直接指定內容,或用 insert 的方式。在實際使用時,不需全部使用,位置也可另外指定。
L21~50: 是封包進來時會呼叫的判斷 function, 其它是宣告或註冊,這部份較長等一下再說明。
L52~L53: 註冊此 filter 是在 UDP Port 1234 進行過濾。
欄位處理
上面內容是偏向於型態與處理目標宣告,接下來就是要行確切位置的處理與判斷。所謂的進行解析,實現在 Wireshark 上就是把欄位印出來。因此在 Wireshark 上看到的是會多了一個說明欄位。
L22: 先產生一個此filter的根結節點,後面的說明都會附著在此之下
L23: 取得傳進來的 buf 長度,用以印出最後不解析的內容
L24~32: 加入之前 L4~L14 所定義的欄位。add_le() 是指加入該欄位,但以 little endian 的方式呈現,而後面是其所指定的資料位置。L30~31因為都是4bits,所以佔用同一個位置。而最後面的 bytes 部份,就是其剩餘的資料欄位,以列表的方式列出。
L34~37: 是要 overwrite eth 的呈現資料,但這邊不需要,就先 comment 掉
L38~48: 是根據 msgtype 在根節點 “t” 旁邊再顯示一些文字。若有需要的話,每個欄位旁都可以類似的方式顯示額外文字
PCAP 截取檔產生
上面講的是如何解析截取檔,那要如何產生符合格式的檔案呢?一個方法是真的產生如上述的實體封包,另一個方法是產生假封包存入檔案,以進行解析。這邊我們說明的是 .pcap 檔,說明是參考LibpcapFileFormat。
PCAP 的內容可以分為3部份,後面2個封包檔頭和內容則是一直重複的
- 全域檔頭
- 封包檔頭
- 封包內容
全域檔頭
1 2 3 4 5 6 7 8 9 |
typedef struct pcap_hdr_s { unsigned int magic_number; /* magic number */ unsigned short version_major; /* major version number */ unsigned short version_minor; /* minor version number */ signed int thiszone; /* GMT to local correction */ unsigned int sigfigs; /* accuracy of timestamps */ unsigned int snaplen; /* max length of captured packets, in octets */ unsigned int network; /* data link type */ } pcap_hdr_t; |
參考上述的官方 WiKi,格式等同上面,初始化可用下面內容。
1 2 3 4 5 6 7 8 |
pcap_hdr_t h; h.magic_number = 0xa1b2c3d4; h.version_major = 0x2; h.version_minor = 0x4; h.thiszone = 0; h.sigfigs = 0; h.snaplen = 0xFFFF; h.network = 0x01;//Ethernet |
magic_number 肩負了 magic number 的功能,與判斷 endian 的功能。若 wireshark 發現是反過來的,代表截取的系統與解析的系統,endianness 是相反一。其餘項目就填如上表,其中的 network (1)為 ethernet, 目前好像也沒什麼其它比較實用可填的。
封包檔頭
1 2 3 4 5 6 |
typedef struct pcaprec_hdr_s { unsigned int ts_sec; /* timestamp seconds */ unsigned int ts_usec; /* timestamp microseconds */ unsigned int incl_len; /* number of octets of packet saved in file */ unsigned int orig_len; /* actual length of packet */ } pcaprec_hdr_t; |
封包檔頭前2個欄位就是關於抓取的時間,第一包的時間為0,後面就需要計算一下前後差距。可以使用 gettimeofday() 來取得包含 usec 的時間,來做較為精確的表示。
後面的 incl_len 是封包實際存在檔案的長度,而 orig_len 則是封包應有的長度。由於有時我們只是要抓 header 而已,所以才會有這種區別。
封包內容
封包內容則只是單純的資料,要靠解析器來解析。
產生封包範例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> #include <stdlib.h> int main() { char buf[1000] = { 0xa1, 0xb2, 0xc3, 0xd4, 0x2, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0,0,0,0, 0xff, 0xff, 0xff, 0xff, 0,0,0,1, //proto type, 1=eth 0,0,0,0, 0,0,0,0, 0,0,0,60, //inc len 0,0,0,60, //net len //ethernet data start 0xff,0xff,0xff,0xff,0xff,0xff, //dst 0xff,0xff,0xff,0xff,0xff,0xff, //src 0x08,0x06, //proto, ethernet end 0,1,8,6,0, 6,4, 0,1,0xc8, 0x7f, 0x54, 0xe9, 0xd7, 0x14, 0xc0, 0xa8, 0x01, 0xa9, 0,0,0,0,0,0, 0xc0, 0xa8, 1, 6 }; int i; for(i=0;i<sizeof(buf);i++) { printf("%c", buf[i] & 0xFF); } } |
上面的程式碼,則是一個不完整的 ARP 封包產生程式,但已足夠讓 wireshark 解析其內容。下面是一個比較完整的 Linux 程式,可以封裝到 UDP 層級
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#include <stdlib.h> #include <stdio.h> #include <linux/ip.h> #include <linux/if_ether.h> #include <linux/udp.h> typedef struct { unsigned int magic_number; unsigned short version_major; unsigned short version_minor; signed int thiszone; unsigned int sigfigs; unsigned int snaplen; unsigned int network; } __attribute__((__packed__)) pcap_hdr_t; typedef struct { unsigned int ts_sec; unsigned int ts_usec; unsigned int incl_len; unsigned int orig_len; } __attribute__((__packed__)) pcaprec_hdr_t; int main() { FILE *fp; fp = fopen("/tmp/out.pcap", "w"); pcap_hdr_t h; struct timeval tim; unsigned long long start; unsigned long long now ; int i; int len = 4; int _data = 0; char *data = (char *)&_data; gettimeofday(&tim, NULL); start = tim.tv_sec; start *= 1000000; start += tim.tv_usec; h.magic_number = 0xa1b2c3d4; h.version_major = 2; h.version_minor = 4; h.thiszone = 0; h.sigfigs = 0; h.snaplen = 0xFFFF; h.network = 1; fwrite(&h, 1, sizeof(h), fp); for(i=0;i<10;i++) { _data++; usleep(1000*100); gettimeofday(&tim, NULL); now = tim.tv_sec; now *= 1000000; now += tim.tv_usec; hdr.ts_sec = (now - start) / 1000000; hdr.ts_usec = (now - start) % 1000000; hdr.incl_len = len + sizeof(eth) + sizeof(ip) + sizeof(udp); hdr.orig_len = len + sizeof(eth) + sizeof(ip) + sizeof(udp); fwrite(&hdr, 1, sizeof(hdr), fp); eth.h_proto = htons(0x0800); fwrite(ð, 1, sizeof(eth), fp); ip.version = 4; ip.ihl = 5; ip.tot_len = htons(sizeof(udp)+ len + sizeof(ip)) ; ip.frag_off = 0; ip.protocol = 17; fwrite(&ip, 1, sizeof(ip), fp); udp.source = htons(1234); udp.dest = htons(5678); udp.len = htons(len + sizeof(udp)); udp.check = 0; fwrite(&udp, 1, sizeof(udp), fp); fwrite(data, 1, len, fp); fflush(fp); } fclose(fp); } |
結語
一個說有用但也不是很有用的 topic, 單看你的需求是不是夠長久.