1. 實驗介紹
本次實驗的目的在於學習WinPcap的使用方法,利用它捕獲以太網中的數據包並進行簡單的解析,最終使用MFC畫界面,展示捕獲后解析出來的信息。
2. 使用WinPcap + MFC進行數據包的捕獲與分析
2.1 WinPcap簡單介紹
WinPcap是一個開源的數據包捕獲體系結構,它的主要功能是進行數據包捕獲和網絡分析。它包括了內核級別的包過濾、低層次的動態鏈接庫(packet.dll
)、高級別系統無關的函數庫(wpcap.dll
)等。在編寫程序之前我們先按以下步驟配置好WinPcap的開發環境。
-
下載WinPcap並安裝
-
打開VS2015,新建->項目->MFC應用程序(基於對話框,經典菜單)
-
在項目上,右鍵->屬性
-
工具->屬性->項目和解決方案-> VC++目錄->包含文件->添加WinPcap開發包中的
Include
目錄 -
工具->屬性->項目和解決方案-> VC++目錄->庫文件->添加WinPcap開發包中的
lib
目錄 -
項目->項目屬性->配置屬性->預處理定義->添加
WPCAP
和HAVE_REMOTE
-
項目->項目屬性->配置屬性->連接器->命令行->附加選項框中加入
wpcap.lib
-
在程序中要加入
pcap.h
頭文件#include pcap.h
2.2 WinPcap程序設計思路
使用WinPcap捕獲數據包一般有三個步驟:
-
獲取設備列表
-
打開網絡適配器
-
在打開的網絡適配器上捕獲網絡數據包
2.2.1 獲取設備列表
在開發以WinPcap為基礎的應用程序時,第一步要求的就是獲取網絡接口設備(網卡)列表。這可以調用WinPcap提供的pcap_findalldevs_ex()
函數,該函數原型如下:
int pcap_findalldevs_ex( char * source; //指定從哪兒獲取網絡接口列表 struct pcap_rmauth auth; //用於驗證,由於是本機,置為NULL pcap_if_t ** alldevs; //當該函數成功返回時,alldevs指向獲取的列表數組的第一個 //列表中每一個元素都是一個pcap_if_t結構 char * errbuf //錯誤信息緩沖區 );
在上面注釋中提到的pcap_if_t
結構定義如下:
struct pcap_if{ struct pcap_if *next; //指向鏈表中下一個元素 char *name; //代表WinPcap為該網絡接口卡分配的名字 char *description; //代表WinPcap對該網絡接口卡的描述 struct pcap_addr* addresses; //addresses指向的鏈表中包含了這塊網卡的所有IP地址 u_int flags; //標識這塊網卡是不是回送網卡 }
2.2.2 打開網卡
在獲取設備列表之后,可以選擇感興趣的網卡打開並對其上的網絡流量進行監聽。在這里我們可以使用pcap_open()
(有時也用pcap_open_live()
)函數將網卡打開,其函數原型如下:
pcap_t * pcap_open( const char *source; //要打開的網卡的名字 int snaplen, int flags, //指定以何種方式打開網卡,常用的有混雜模式 int read_timeout, //數據包捕獲函數等待一個數據包的最大時間,超時則返回0 struct pcap_rmauth *auth, char *errbuf )
在調用pcap_open()
函數之后,如果它返回的是NULL
,則有errbuf
傳遞出錯的詳細信息;如果調用成功,返回一個指向pcap_t
的指針,這個指針在pcap_next_ex
中使用。
2.2.3 捕獲數據包
WinPcap提供三種方法捕獲數據包,其中,使用回調函數捕獲的有pcap_dispatch()
和pcap_loop()
,而pcap_next_ex()
則是不使用回調直接捕獲。在本次實驗中,我使用的是pcap_next_ex()
,它的函數原型如下所示:
int pcap_next_ex( pcap_t *p; struct pcap_pkthdr ** pkt_header, u_char ** pkt_data )
其中,
-
p:這個參數當為調用
pcap_opn()
成功之后返回的值,它指定了捕獲哪塊網卡上的數據包 -
pkt_header:在
pcap_next_ex()
函數調用成功后,該參數保存了數據包的一些信息,如捕獲該數據包的時間戳、數據包的長度等等 -
pkt_data:指向捕獲到的網絡數據包
調用pcap_next_ex()
會返回1、0、-1三個值。
-
返回1,調用成功,pkt_header的確保存了數據包的一些信息,pkt_data也真的指向了捕獲到的網絡數據包
-
返回0,代表在
pcap_open()
函數指定的時間內未捕獲到數據包,pkt_header和pkt_data不可用 -
返回-1,調用中發生錯誤
2.3 MFC程序設計思路
在了解了整體需求之后,我們可以用基於對話框的MFC應用程序設計出兩個界面,一個支持展示捕獲的數據包的簡單解析結果(源MAC地址和目的MAC地址)和菜單功能(界面1),一個支持展示設備列表和設備的選擇(界面2)。在菜單中進行選擇之后可彈出界面2,在界面2中選擇感興趣的網卡,然后回到界面1,點擊開始捕捉按鈕就可以在界面1看到需要的信息。
2.4 具體實現——數據包的捕獲、解析與信息展示
2.4.1構建兩個對話框( Packet與Device )、相應的類以及變量,設計菜單
(1)Packet對話框
其中,在該對話框中加入了List Control控件,並為其設置了m_packetList變量,加入了菜單IDR_MENU1
(2)Device對話框
其中,在該對話框中也加入了List Control控件,並為其設置了m_deviceList變量,其他無需細說
2.4.2 完成設備列表的展示與網卡的選擇
在展示DeviceDialog.cpp之前,先展示定義的一些變量、結構和函數:
CListCtrl m_deviceList; //list control控件相關的變量 virtual BOOL OnInitDialog(); //初始化對話框 afx_msg void OnBnClickedOk(); //綁定按鈕對應的事件處理程序 //保存了選取的網卡的指針(alldevs) afx_msg void OnBnClickedCancel();//取消按鈕對應的事件處理程序 //點擊List Control控件中一欄,則在編輯框中顯示選中的設備名 afx_msg void OnClickDeviceList(NMHDR *pNMHDR, LRESULT *pResult); pcap_if_t* GetDevice(); //確定得到的設備名的確在設備列表中,並獲取指針 pcap_if_t* returnDev(); //用於在兩個對話框之間傳參 pcap_if_t *alldevs; //指向設備鏈表首部指針 pcap_if_t *d; char errbuf[PCAP_ERRBUF_SIZE]; //錯誤信息緩沖區 CString deviceName; //記錄選擇的適配器名稱
當調用Device對話框時,應該立馬顯示出本機設備列表,這需要在CDeviceDialog的OnInitDialog()
初始化函數中實現。具體代碼及必要注釋如下:
BOOL CDeviceDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // TODO: 在此添加額外的初始化 CRect rect; //獲取列表視圖控件位置和大小 m_deviceList.GetClientRect(&rect); //為列表視圖控件添加全行選中和柵格風格 m_deviceList.SetExtendedStyle(m_deviceList.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); //為列表視圖控件添加兩列 m_deviceList.InsertColumn(0,_T("設備名"), LVCFMT_LEFT, rect.Width() / 2, 0); m_deviceList.InsertColumn(1, _T("設備描述"), LVCFMT_LEFT, rect.Width() / 2, 0); /*獲取網絡適配器*/ if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) { MessageBox(_T("找不到適配器!")); exit(1); } for (d = alldevs; d != NULL; d = d->next) { //向Device中的List寫入設備名和設備描述 m_deviceList.InsertItem(0,(CString)d->name); m_deviceList.SetItemText(0,1,(CString)d->description); } d = NULL; //方便其他函數使用 return TRUE; // return TRUE unless you set the focus to a control // 異常: OCX 屬性頁應返回 FALSE }
在顯示設備列表之后,還需考慮如何將選中的網卡傳遞到界面1也就是對話框Packet中,首先,需要先確定選中了List中的哪一行,這由OnClickDeviceList()
實現,具體代碼如下:
void CDeviceDialog::OnClickDeviceList(NMHDR *pNMHDR, LRESULT *pResult) { LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR); // TODO: 在此添加控件通知處理程序代碼 NMLISTVIEW *pNMListView = (NMLISTVIEW*)pNMHDR; // 判斷是否有列表項被選擇 if (-1 != pNMListView->iItem) { //獲取被選擇列表項第一列的文本內容 deviceName = m_deviceList.GetItemText(pNMListView->iItem, 0); //顯示到編輯框 SetDlgItemText(IDC_EDIT1, deviceName); } *pResult = 0; }
在上面我們已經獲取了選中的deviceName,接着按下綁定按鈕,執行OnBnClickedOk()
:
void CDeviceDialog::OnBnClickedOk() { // TODO: 在此添加控件通知處理程序代碼 GetDevice(); d = GetDevice(); if (d != NULL) { MessageBox(_T("綁定成功!")); CDialogEx::OnOK(); } else MessageBox(_T("請選擇一個網卡綁定!")); } //GetDevice()`獲取指向具有該名字的設備的指針,代碼如下: pcap_if_t* CDeviceDialog::GetDevice() { if (deviceName) { //在獲取的設備列表中一一查詢直到找到對應的那一行的指針d for (d = alldevs; d != NULL; d = d->next) if (d->name == deviceName) return d; } return NULL; }
上面的代碼獲取了設備d,仍需傳遞至另一個對話框,這由returnDev()
實現,當對話框Packet調用的Device的對象,通過該對象就可獲取設備d,具體代碼如下:
pcap_if_t* CDeviceDialog::returnDev() { return d; }
2.4.3 完成網卡的打開與數據包的捕獲、解析
先看看在Packet的頭文件中定義的一些變量、結構和函數:
CListCtrl m_packetList; //指向List Control的變量 afx_msg void OnDevCh(); //菜單欄中“選擇適配器”對應的事件處理函數,獲取設備 afx_msg void OnStart(); //菜單欄中“開始捕獲數據包”對應的事件處理函數 afx_msg void OnStop(); //菜單欄中“停止捕獲數據包”對應的事件處理函數 pcap_if_t* m_device; //全局變量,保存DevCh()獲取的設備 int m_count; //用於list的輸出計數 bool m_flag; //為true時代表開始捕獲,為false代表停止捕獲 //6字節的MAC地址 struct FrameHeader_t { //幀首部 u_char byte1; u_char byte2; u_char byte3; u_char byte4; u_char byte5; u_char byte6; }; struct IPHeader_t { //IP首部 FrameHeader_t daddr; // 目的MAC地址 FrameHeader_t saddr; // 源MAC地址 u_short type; // 協議類型 }; void ShowPacketList(const pcap_pkthdr * pkt_header, const u_char * pkt_data); //用於輸出解析的數據
另外,Packet的初始化代碼在OnInitialDialog()
中實現,具體添加的代碼如下:
// TODO: 在此添加額外的初始化代碼 CRect rect; //獲取列表視圖控件位置和大小 m_packetList.GetClientRect(&rect); //為列表視圖控件添加全行選中和柵格風格 m_packetList.SetExtendedStyle(m_packetList.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); //為列表視圖控件添加兩列 m_packetList.InsertColumn(0, _T("源MAC地址"), LVCFMT_LEFT, rect.Width() / 2, 0); m_packetList.InsertColumn(1, _T("目的MAC地址"), LVCFMT_LEFT, rect.Width() / 2, 0); m_flag = false; m_device = NULL; m_count = 0;
在上面,Device對話框已經為Packet准備好了網卡,在Packet中需要將其打開,首先,獲取Device准備的設備d,這由OnDevCh()
函數實現,具體代碼如下:
void CPacketDlg::OnDevCh() { // TODO: 在此添加命令處理程序代碼 CDeviceDialog devDlg; //建立對話框Device的對象 if (devDlg.DoModal() == IDOK) { m_device = devDlg.returnDev(); //獲取選中的適配器,並保存到全局變量m_device中 } UpdateData(FALSE); }
在點擊菜單中的“開始捕獲數據包”之后,發出信號,開始打開網卡,並捕獲、解析數據包,這由OnStart()
實現,具體代碼如下:
//創建一個線程 DWORD WINAPI CapturePacket(LPVOID lpParam); void CPacketDlg::OnStart() { // TODO: 在此添加命令處理程序代碼 if (m_device == NULL) { //如果未選擇適配器 MessageBox(_T("請先選擇一塊網卡綁定!")); return; } m_flag = true; //標志開始捕獲 //啟動線程開始抓包 CreateThread(NULL,NULL,CapturePacket,(LPVOID)this,true,NULL); } DWORD WINAPI CapturePacket(LPVOID lpParam) { //打開網卡 CPacketDlg * pDlg = (CPacketDlg *)lpParam; char errbuf[PCAP_ERRBUF_SIZE]; int res; pcap_t *adhandle; //接受pcap_open()返回值 struct pcap_pkthdr *pkt_header; const u_char* pkt_data; if ((adhandle = pcap_open_live(pDlg->m_device->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000,errbuf)) == NULL) { AfxMessageBox(_T("請選擇要綁定的網卡")); return -1; } //捕獲數據包 while ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) { if (res == 0) { //AfxMessageBox(_T("沒有捕獲到數據包")); continue; } if (!pDlg->m_flag) //如果不是要抓包,則停止 break; CPacketDlg *pDlg1 = (CPacketDlg *)AfxGetApp()->GetMainWnd(); //解析數據包 pDlg1->ShowPacketList(pkt_header,pkt_data); pDlg1 = NULL; } pcap_close(adhandle); //釋放指針 pDlg = NULL; //釋放對象 return 1; } void CPacketDlg::ShowPacketList(const pcap_pkthdr * pkt_header, const u_char * pkt_data) { IPHeader_t *eh; eh = (IPHeader_t *)pkt_data; //強制類型轉換,以便解析數據 //顯示源MAC地址 CString str; str.Format(_T("%x:%x:%x:%x:%x:%x"), eh->saddr.byte1, eh->saddr.byte2, eh->saddr.byte3, eh->saddr.byte4, eh->saddr.byte5, eh->saddr.byte6); m_packetList.InsertItem(m_count,str); //顯示目的MAC地址 str.Format(_T("%x:%x:%x:%x:%x:%x"), eh->daddr.byte1, eh->daddr.byte2, eh->daddr.byte3, eh->daddr.byte4, eh->daddr.byte5, eh->daddr.byte6); m_packetList.SetItemText(m_count, 1, str); m_count++; }
菜單欄”停止捕獲數據包“的對應事件處理程序OnStop()
具體代碼如下:
void CPacketDlg::OnStop() { // TODO: 在此添加命令處理程序代碼 m_flag = false; }
至此,所有代碼已經完成。
2.4 程序使用方法與結果展示
在VS2015(其他版本應該也可以運行)中打開代碼,運行,按以下步驟執行:
-
在菜單中執行 ”選擇->選擇適配器“,將展示設備列表
-
選擇一個網卡
-
執行 ”綁定->確定“
-
回到了Packet對話框,在菜單項執行 ”操作->開始捕獲數據包“,此時主機應打開一個網頁來獲取數據包,當然任何其他能真正與網絡互通獲取來自網絡的數據包的操作都可以
-
在菜單中執行 ”操作->停止捕獲數據包“,即可停止停止捕獲
3. 程序在其他機器運行需要設置的地方和可能出現的問題
-
需要重新配置WinPcap環境,這可以按前面提供的配置方法解決
-
可能會出現VS不兼容的問題,宜改成VS2015