實戰(zhàn):從網卡抓包到協(xié)議解析)
簡介華中科技大學計算機網絡安全實驗的完整C源碼基于WinPcap庫實現數據包嗅探與分析工具并集成ARP欺騙模塊演示中間人攻擊。面向高校網絡空間安全專業(yè)學生及對協(xié)議分析感興趣的開發(fā)者適合作為網絡安全課程設計、畢業(yè)設計或綜合實驗的參考實現。壓縮包共27個文件包含8個頭文件與7個cpp源文件搭配2個Qt界面文件及png/jpg圖形資源項目結構清晰capthread與sendpktthread分別負責抓包與發(fā)包protocol.h和arphead.h定義TCP/IP與ARP報文格式arpcheatdialog提供欺騙交互界面各模塊職責明確便于閱讀。整體大小僅293KB輕量便于加載運行。已有2814人學習源碼對WinPcap初始化、設備枚舉、數據包逐層解析、ARP緩存欺騙等核心環(huán)節(jié)均有實現讀者可借此深入理解網絡嗅探與中間人攻擊原理并二次開發(fā)擴展檢測功能同時可作為實驗報告配套材料對照源碼梳理思路與關鍵代碼邏輯。1. 基于WinPcap的數據包嗅探器把網卡黑匣子拆開給你看的網絡安全實驗當我第一次在自己的筆記本上把這份基于 WinPcap 的嗅探器編譯跑起來看著瀏覽器發(fā)出的 HTTP 請求在十六進制區(qū)一行行滾出來我才真正理解網絡協(xié)議棧是什么——不是什么抽象的分層矩形而是一串串比特在網卡、驅動、協(xié)議頭之間流動。這份來自某高校計算機網絡安全課程的實驗源碼目標很直接在 Windows 平臺上用 C 語言實現一個數據包分析器嗅探器把網卡收到的原始幀逐層拆成以太網、IP、TCP/UDP 字段按 BPF 規(guī)則過濾流量并把抓到的報文落盤保存成通用格式。這套源碼最適合三類人正在做網絡實驗或課程設計的學生想快速跑通 WinPcap 抓包全流程的開發(fā)者以及在不引入重型分析框架的前提下想理解“流量分析怎么落地”的入門者。它不是只能應付答辯的演示代碼設備枚舉、抓包線程、協(xié)議解析、過濾、落盤五個關鍵環(huán)節(jié)全部串成完整鏈路。下面我從工程結構、核心代碼、避坑經驗和二次開發(fā)幾個方面把它拆開講。2. 架構與核心模塊嗅探器由哪幾塊拼起來一個嗅探器在功能上只有三步從網卡拿數據、按協(xié)議拆數據、把結果給人看。但落到代碼工程里這三步需要拆成設備選擇、抓包循環(huán)、過濾規(guī)則、協(xié)議解析和結果展示幾個獨立模塊。這份實驗源碼的工程組織就是沿著這條鏈路走的結構清晰到可以拿來做網絡實驗的項目模板。2.1 文件結構解壓后每個文件都在干什么先看整體。工程是標準的 VC 項目核心代碼全用 C 編寫界面可以是 MFC 對話框也可以退化成控制臺輸出。按功能劃分源碼文件大致分四類文件職責關鍵接口DeviceList.c枚舉本機所有網卡接口EnumDevices()內部調用pcap_findalldevsCaptureEngine.c打開指定網卡、設置過濾、啟動和停止抓包OpenAdapter()、StartCapture()、StopCapture()ParsePacket.c解析以太網、IP、TCP/UDP 協(xié)議頭ParseEthernet()、ParseIP()、ParseTCP()Storage.c原始幀落盤與讀取SaveToFile()、LoadFromFile()這個分工最值得留意的設計決策是“抓包與界面分離”。packet_handler()回調里只負責接收裸幀所有解析和展示邏輯都通過回調參數傳給外部模塊處理抓包循環(huán)不會因為界面刷新慢而被拖死。如果你把解析直接寫進回調流量大時界面會卡到懷疑人生——這是新手最常見的第一個設計坑。除了這四類文件工程里還帶filter.txt和config.ini兩個配置文件。前者保存 BPF 過濾規(guī)則文本后者記錄默認網卡序號、抓包長度上限snaplen和讀取超時timeout。參數外置在實驗代碼里不常見但對二次開發(fā)很實用改過濾規(guī)則不用重新編譯。2.2 數據流鏈路一幀報文從網卡到屏幕經過哪五步把整個程序從啟動到界面刷新拉直了看數據流轉是這樣走的pcap_findalldevs()枚舉全部網卡虛擬機網卡也會出現在界面下拉框里列出名稱和描述用戶選定網卡后pcap_open()以混雜模式打開它返回一個pcap_t*句柄pcap_compile()把用戶輸入的過濾字符串比如tcp port 80編譯成 BPF 字節(jié)碼再交給pcap_setfilter()掛到句柄上程序啟動一個獨立抓包線程在線程里循環(huán)調pcap_next_ex()阻塞式取幀。每取到一幀就觸發(fā)一次回調回調里完成解析并把結果格式化成一串文本解析結果通過PostMessage投遞回主線程界面表格更新顯示同時如果開啟存儲模式原始幀會被追加寫入 pcap 文件。這套鏈路最關鍵的設計是第 4 步抓包在獨立線程里。pcap_next_ex()在 Windows 上是阻塞等待數據包如果放在 UI 線程里抓包期間窗口完全無法響應。實驗代碼的處理方式是創(chuàng)建抓包線程回調里用PostMessage把解析結果發(fā)回主線程再刷新表格。這也是一種界面與數據解耦的思路。WinPcap 提供了兩套抓包機制pcap_next_ex()適合自己控制循環(huán)節(jié)奏隨時能退出pcap_loop()把回調注冊進庫后由內部循環(huán)驅動適合無界面的命令行工具。這份源碼選的是前者因為實驗場景需要響應“開始/停止”按鈕。你換到命令行工具場景時pcap_loop更簡潔。2.3 為什么選 WinPcap 而不是原始套接字這個問題在實驗答辯時幾乎必問。為什么不用 Windows 自帶的原始套接字Raw Socket我總結了四個理由。第一混雜模式。普通套接字只能收到發(fā)給本機的幀嗅探器的核心價值在于能接收共享介質上的所有流量把網卡切到混雜模式正是 pcap 這類庫存在的理由。Windows 原始套接字要開混雜模式繞的圈子遠比pcap_open傳一個 flag 大。第二內核態(tài)過濾。BPF 表達式例如host 10.1.1.1 and tcp port 80是編譯后傳給驅動層處理的過濾在數據到達用戶態(tài)之前就已發(fā)生。自己寫套接字抓包過濾邏輯要放在用戶態(tài)性能和復雜度完全不是一個量級。第三API 可移植。WinPcap 是 Libpcap 的 Windows 實現函數簽名幾乎一樣。寫好的解析模塊到 Linux 上換個設備枚舉和打開的調用就能繼續(xù)用課程實驗之后如果要擴展跨平臺場景成本很低。第四文件格式兼容。pcap 文件格式是事實標準抓完的數據拿到協(xié)議分析工具里二次核對非常方便。原始套接字自行保存的數據沒有通用格式后處理要自己寫解析器。3. 核心代碼拆解設備枚舉、過濾規(guī)則與三層協(xié)議解析這一章落到真實代碼。我挑三段最能代表整個項目技術點的核心代碼網卡枚舉與打開、BPF 過濾與抓包線程、從以太網到 TCP/UDP 的逐層拆包。這三段是嗅探器的三個承重墻界面代碼本質上只是這三段輸出的展示層。3.1 設備枚舉與網卡打開先拿到所有可用網卡第一段代碼解決“抓誰的數據”。Windows 上有多塊網卡時程序必須先完整掃描所有適配器讓用戶選。枚舉接口用pcap_findalldevs實現#include pcap.h #include stdio.h int list_devices(void) { pcap_if_t *alldevs; pcap_if_t *dev; char errbuf[PCAP_ERRBUF_SIZE]; int index 0; if (pcap_findalldevs(alldevs, errbuf) -1) { printf(枚舉網卡失敗: %s\n, errbuf); return -1; } for (dev alldevs; dev ! NULL; dev dev-next) { printf(%d: %s, index, dev-name); if (dev-description) { printf( (%s), dev-description); } printf(\n); } return index; }alldevs是 WinPcap 維護的鏈表dev-name是形如\Device\NPF_{GUID}的設備標識dev-description是對用戶友好的描述文本。枚舉完成后必須調用pcap_freealldevs(alldevs)釋放鏈表否則每次枚舉都泄漏一塊內存跑久了必然出問題。設備選定后進入打開階段。這里有兩個必須關注的參數snaplen決定單幀最多捕獲多少字節(jié)設為 65535 表示盡量抓完整幀timeout決定讀取接口在無流量時最多阻塞多久。打開失敗時返回值是空指針errbuf里會寫好原因pcap_t *handle pcap_open(dev-name, 65535, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf); if (handle NULL) { printf(打開網卡失敗: %s\n, errbuf); pcap_freealldevs(alldevs); exit(1); }PCAP_OPENFLAG_PROMISCUOUS打開的是混雜模式意味著網卡不再只收發(fā)給自己的幀同一網段共享介質上的廣播幀和直接碰撞域里的幀都能收到。注意筆記本無線網卡和家庭路由器的環(huán)境不同無線網卡在驅動不支持混雜抓包時能看到的通常只有本機會話流量如果開了虛擬機\Device\NPF_{GUID}列表里還會混進虛擬網卡選錯設備是抓不到數據的常見原因。3.2 BPF 過濾與抓包線程讓收包過程更可控網絡流量非常嘈雜必須用過濾規(guī)則把不關心的幀擋在用戶態(tài)之前。BPF 過濾表達式要用pcap_compile編譯再用pcap_setfilter掛到句柄上struct bpf_program filter; char filter_exp[] tcp port 80; // 只關心 TCP 80 端口流量 if (pcap_compile(handle, filter, filter_exp, 1, PCAP_NETMASK_UNKNOWN) -1) { printf(過濾器編譯失敗: %s\n, pcap_geterr(handle)); return; } if (pcap_setfilter(handle, filter) -1) { printf(過濾器設置失敗: %s\n, pcap_geterr(handle)); return; }pcap_compile的第三個參數是 BPF 表達式第四個參數optimize傳 1讓庫對過濾邏輯做布爾化簡第五個netmask只有在表達式里用到廣播地址時才有必要填多數場景直接給PCAP_NETMASK_UNKNOWN即可。我習慣把過濾表達式抽到 config 文件里抓包時動態(tài)讀取再編譯這樣改規(guī)則不需要重編整個程序實驗調試階段能省下大量時間。抓包線程用 Win32 線程 API 創(chuàng)建回調里只做三件事解析原始幀、拼裝顯示文本、把文本投遞回主線程。這段循環(huán)是抓包程序的常駐主體DWORD WINAPI capture_thread(LPVOID param) { pcap_t *handle (pcap_t*)param; struct pcap_pkthdr *header; const u_char *pkt_data; int res; while (running) { res pcap_next_ex(handle, header, pkt_data); if (res 1) { char line[1024]; parse_packet(pkt_data, header-caplen, line, sizeof(line)); PostMessage(g_hwnd, WM_PACKET_READY, 0, (LPARAM)line); } else if (res -1) { break; // 網卡被移除或驅動出錯 } } return 0; }pcap_next_ex的返回值有三種1 表示成功抓到一幀0 表示本次超時超時時間內沒有幀-1 表示出錯。這里必須嚴格判斷res 1很多新手寫成了res 0結果把超時也當成數據幀處理界面瘋狂刷空行?;卣{參數里的header-len是原始幀長度header-caplen是實際捕獲字節(jié)數兩者在 snaplen 小于幀長時會不一致。解析時邊界判斷只能用caplen按len去訪問幀數據會在截斷幀時越界。如果要把抓到的幀存成 pcap 文件不要自己拼文件頭。WinPcap 自帶pcap_dump系列接口pcap_dumper_t *dumper pcap_dump_open(handle, capture.pcap); // 在回調里追加原始幀 pcap_dump((u_char*)dumper, header, pkt_data); // 退出前關閉 pcap_dump_close(dumper);pcap_dump_open會寫好 24 字節(jié)文件頭和每幀前的 16 字節(jié)記錄頭時間戳也自動換算成正確格式抓完的文件能被外部協(xié)議分析工具直接打開對照。3.3 逐層拆包從以太網頭到 IP、TCP/UDP 端口核心中的核心是協(xié)議解析。以太網幀頭固定 14 字節(jié)其中 12 字節(jié)是 MAC 地址后面 2 字節(jié)是以太網類型。類型是0x0800時幀體為 IPv4 報文。代碼用字節(jié)數組下標方式解析避免直接結構體強轉造成字節(jié)序混亂#define ETHER_HEADER_LEN 14 void parse_packet(const u_char *pkt, int caplen, char *out, int out_len) { if (caplen ETHER_HEADER_LEN 20) { snprintf(out, out_len, 幀過長或被截斷長度%d, caplen); return; } // 以太網類型是網絡字節(jié)序手動拼成可讀值 u_short ether_type (u_short)((pkt[12] 8) | pkt[13]); if (ether_type ! 0x0800) { snprintf(out, out_len, 非IPv4幀類型0x%04x, ether_type); return; } const u_char *ip pkt ETHER_HEADER_LEN; int ip_header_len (ip[0] 0x0f) * 4; // IP頭長度字段單位4字節(jié) int protocol ip[9]; // 協(xié)議字段6TCP 17UDP char src_ip[16], dst_ip[16]; snprintf(src_ip, 16, %d.%d.%d.%d, ip[12], ip[13], ip[14], ip[15]); snprintf(dst_ip, 16, %d.%d.%d.%d, ip[16], ip[17], ip[18], ip[19]); if (protocol 6) { // TCP頭偏移由ip_header_len往后找 int tcp_header_len ((ip[ip_header_len 12] 4) 0x0f) * 4; u_short sport (u_short)((ip[ip_header_len] 8) | ip[ip_header_len 1]); u_short dport (u_short)((ip[ip_header_len 2] 8) | ip[ip_header_len 3]); snprintf(out, out_len, TCP %s:%d - %s:%d, src_ip, sport, dst_ip, dport); } else if (protocol 17) { u_short sport (u_short)((ip[ip_header_len] 8) | ip[ip_header_len 1]); u_short dport (u_short)((ip[ip_header_len 2] 8) | ip[ip_header_len 3]); snprintf(out, out_len, UDP %s:%d - %s:%d, src_ip, sport, dst_ip, dport); } else { snprintf(out, out_len, 協(xié)議 protocol%d, protocol); } }這段代碼有三個值得反復看的細節(jié)。第一字節(jié)序。協(xié)議頭字段一律按網絡字節(jié)序傳輸端口號必須手動做高字節(jié)左移再拼低位不能直接把數據區(qū)強轉后讀取。第二變長頭部。IP 頭不是固定 20 字節(jié)ip[0]的低 4 位是頭長度值乘以 4 才是真實字節(jié)數TCP 頭的數據偏移字段同理需要定位到 TCP 頭內部偏移 12 的字節(jié)取高 4 位再乘 4。解析時不能假設頭部都是固定長度。第三邊界防御。一幀數據如果只是部分包比如caplen小于頭部長度的合計后續(xù)訪問就越界了。用caplen做前置判斷是必須的。把這段解析邏輯和前面的抓包循環(huán)拼在一起整個嗅探器的主體功能就已經齊了。4. 避坑與排查編譯運行中會翻車的五個典型現場實驗課常見的流程是把代碼拷貝到自己的電腦上改一改、編一編、跑一跑然后把抓包結果截圖寫進報告。這個看起來簡單的流程里翻車點比想象中多。下面是幾個每次開課都會被反復問到的典型問題按現象、原因、解決的順序拆解。我在調試這類實驗時有個習慣永遠先用控制臺跑一次最小流程只開通設備和解析再從控制臺切回界面。純粹的界面問題線程、消息、刷新和邏輯問題字節(jié)序、過濾規(guī)則、頭部解析混在一起時你會不知道先查哪邊。所以如果你被某個詭異問題卡住先把界面邏輯注釋掉用 printf 把解析結果打出來往往一眼能看到真相。4.1 現象新環(huán)境編譯報錯pcap.h 找不到現象把工程拷到一臺剛裝好 IDE 的機器上一編譯就報“fatal error C1083: 無法打開包括文件: pcap.h”緊接著還有一堆“找不到標識符”的連帶錯誤。原因WinPcap 分為運行庫和開發(fā)庫。運行庫負責驅動開發(fā)庫WpdPack 安裝包里的 Include 和 Lib 目錄才提供編譯所需的頭文件和導入庫。只裝了運行庫、沒裝開發(fā)包或者裝了開發(fā)包但工程屬性里沒有把目錄指過去就會出現這個報錯。解決裝好 WpdPack 開發(fā)包記錄解壓路徑。在 IDE 工程屬性的“C/C → 常規(guī) → 附加包含目錄”填入...\WpdPack\Include在“鏈接器 → 常規(guī) → 附加庫目錄”填入...\WpdPack\Lib再在“附加依賴項”里加上wpcap.lib。如果鏈接時報 LNK2001 找不到pcap_open之類的外部符號幾乎都是這一步沒做全。4.2 現象編譯通過但一運行就提示 wpcap.dll 缺失現象代碼編譯順利雙擊生成的 exeWindows 彈窗提示“找不到 wpcap.dll”進程直接退出。原因wpcap.dll是 WinPcap 運行庫的用戶態(tài)組件程序在鏈接期只需要導入庫wpcap.lib運行期卻必須在系統(tǒng)目錄或 exe 同目錄下找到對應的 dll。精簡安裝、安全軟件清理、或者重裝系統(tǒng)后運行庫被卸載都會出現這個結果。解決安裝官方發(fā)布的完整 WinPcap 運行包。如果是自己在開發(fā)機上調試把WpdPack\bin目錄下的wpcap.dll、packet.dll手動復制到 exe 輸出目錄更直接。工程里加一個 post-build 命令行自動復制可以一勞永逸。4.3 現象程序正常啟動抓包列表卻一直空現象網卡也選了開始按鈕也點了界面就是一條記錄都不出等幾分鐘也沒有反應。原因多數情況是選錯了設備。裝了虛擬機軟件之后WinPcap 枚舉出的設備列表里會多出虛擬網卡它也有自己的\Device\NPF_{GUID}名稱和描述很容易排在列表靠前的位置。選中它之后宿主機如果沒有虛擬流量自然什么都收不到。另一種常見原因是過濾表達式太嚴格比如寫了tcp port 443但實際測試用的流量根本不走這個端口。解決先在無過濾條件下抓一次確認有數據再逐步加過濾條件。選設備時優(yōu)先選描述里帶“Ethernet”“有線”字樣的物理網卡不放心的話在命令行跑route print看本機默認路由走哪個接口再到程序列表里對號入座。抓包期間也可以用ping 127.0.0.1產生最基礎的本地回環(huán)流量來驗證設備是否工作。4.4 現象抓到了幀但 IP 地址顯示成倒序現象抓包列表里有數據但內容不對勁IP 地址顯示成像0.0.0.0或12.1.168.192這種倒序值TCP 端口也是亂數。原因字節(jié)序問題。協(xié)議頭里的 IP 地址和端口號是網絡字節(jié)序存儲直接按內存地址強轉讀取得到的是反序整數如果拿這個數再做一次網絡序轉換就會出現倒序 IP。還有一個同源坑以太網類型字段也必須從大端轉成小端再和0x0800比較。解決所有頭部字段都用字節(jié)下標方式讀取再手動拼裝或調用ntohs。上面 3.3 節(jié)的解析代碼就是用數組下標手動拼端口從根上避開字節(jié)序問題。網絡字節(jié)序看起來像“玄學”實際上只要記住“所有跨主機傳輸的頭部字段都是大端”這一句就夠了。4.5 現象抓包很流暢但界面卡死沒法點停止現象抓包開始后界面無響應按鈕點了沒反應只能強行結束進程。原因抓包循環(huán)寫在了 UI 線程里。pcap_next_ex是阻塞調用一幀等不到就一直卡住主線程根本輪不到處理窗口消息界面自然就假死了。解決抓包必須放獨立線程?;卣{里用PostMessage把解析結果發(fā)回主線程UI 線程只負責接收消息刷新列表。在 MFC 工程里就是AfxBeginThread開線程回調里PostMessage通知主窗口更新這套雙線程寫法也是二次開發(fā)時的固定骨架。5. 二次開發(fā)把實驗源碼變成可用的協(xié)議分析工具如果只是交實驗報告跑通就夠了。但這份代碼的結構給了它很大的改造空間我建議按下面三個方向練手。第一個方向把過濾器變成動態(tài)選擇?,F在過濾表達式是從 config 文件讀的改成界面上一個下拉框預置“HTTP 流量tcp port 80”“DNS 流量udp port 53”“ICMP 流量icmp”等常用項用戶選擇后調用一次pcap_compile加pcap_setfilter重新掛載。這種改造只涉及新增一個控件和幾行函數調用不影響抓包線程適合作為第一個練手目標。第二個方向加深應用層協(xié)議識別。在拿到 TCP 端口后增加一個端口到協(xié)議名的映射表80 標記 HTTP443 標記 HTTPS53 標記 DNS22 標記 SSH。如果再進一步在parse_packet中把 TCP 載荷前幾個字節(jié)提取出來判斷是否以 GET、POST、HTTP/1.1 開頭就能識別明文 HTTP 請求——這是最基礎的應用層還原。做這個方向時要特別注意載荷長度不足時的邊界判斷直接用caplen做保護。第三個方向把底層庫從 WinPcap 換到 Npcap。已經停更多年的 WinPcap 在新版 Windows 上驅動簽名校驗越來越麻煩如果你要在自己電腦上長期用建議工程遷移到 Npcap SDK。兩者 API 幾乎一致頭文件和庫目錄改一下再把pcap_open的標記參數換成 Npcap 的對應常量其余代碼基本不用動。驗證改造結果有一個很直接的辦法開著程序訪問幾個網站列表里應能看到本機隨機端口到服務器 443 端口的 TCP 記錄本地ping 127.0.0.1應能看到 ICMP 記錄命令行執(zhí)行nslookup應能看到 UDP 53 的 DNS 記錄。每加一種協(xié)議解析分支就用對應的實測流量去驗證能當場發(fā)現問題。我從這個項目中學到最深的一點是做嗅探器這類工具不要同時開發(fā) UI 和解包邏輯。先把抓包、解析做成無界面的核心模塊UI 只是它的一層殼之后換底層庫、加存儲、加深解析改的都是殼或者解析層抓包循環(huán)可以保持不變。從那以后我做任何網絡程序都會強制把數據采集和解釋器嚴格分離這個習慣讓后續(xù)幾輪改造省了不少心。希望這個習慣對你也有效。本文還有配套的精品資源點擊獲取