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
