winpcap 编程
实验目的
了解 winpcap 的架构
学习 winpcap 编程
实验原理
WinPcap是一个基于Win32平台的,用于捕获网络数据包并进行分析的开源库。
大多数网络应用程序通过被广泛使用的操作系统元件来访问网络,比如sockets。这是一种简单的实现方式,因为操作系统已经妥善处理了底层具体实现细节(比如协议处理,封装数据包等等),并且提供了一个与读写文件类似的,令人熟悉的接口。
然而,有些时候,这种“简单的方式”并不能满足任务的需求,因为有些应用程序需要直接访问网络中的数据包。也就是说,那些应用程序需要访问原始数据包,即没有被操作系统利用网络协议处理过的数据包。
WinPcap产生的目的,就是为Win32应用程序提供这种访问方式;WinPcap提供了以下功能:
捕获原始数据包,无论它是发往某台机器的,还是在其他设备(共享媒介)上进行交换的
在数据包发送给某应用程序前,根据用户指定的规则过滤数据包
将原始数据包通过网络发送出去
收集并统计网络流量信息
以上这些功能需要借助安装在Win32内核中的网络设备驱动程序才能实现,再加上几个动态链接库DLL。
所有这些功能都能通过一个强大的编程接口来表现出来,易于开发,并能在不同的操作系统上使用。
WinPcap可以被用来制作网络分析、监控工具。一些基于WinPcap的典型应用有:
网络与协议分析器 (network and protocol analyzers)
网络监视器 (network monitors)
网络流量记录器 (traffic loggers)
网络流量发生器 (traffic generators)
用户级网桥及路由 (user-level bridges and routers)
网络入侵检测系统 (network intrusion detection systems (NIDS))
网络扫描器 (network scanners)
安全工具 (security tools)
实验内容
通过学习WINPCAP架构,编写一个网络抓包程序。
实验过程
首先要说明一点,winpcap 官网建议使用 npcap,因为根据官网说明,winpcap
已经不适用于 windows10 和
windows11,有些函数可能会出现意想不到的效果。根据我的实践,发现winpcap无法获取适配器的具体名称,比如我的
wifi6 适配器,使用 winpcap 就只能获取模糊的 "Microsoft" 这个名称,而使用
npcap 就可以获取其完整的名称,而且 winpcap
或识别不了一些适配器,而这一点在 npcap 上得到了很好的改善。
1、npcap 的下载安装与配置
访问npcap官网:https://www.winpcap.org/
选择npcap的最新版,下载Installer for Windows并安装
在VS中导入相应头文件和lib文件
安装配置的详细过程见我的这一篇博客 ,这里不再赘述。
2、获取设备列表
通常情况下,一个基于Npcap的应用程序所做的第一件事就是获得一个连接的网络适配器的列表。libpcap和Npcap都为这个目的提供了cap_findalldevs_ex()
函数:这个函数返回一个cap_if
结构的链接列表,每个结构都包含一个连接的适配器的全面信息。特别是,字段name
和description
分别包含了相应设备的名称和可读的描述。
下面的代码检索适配器列表并显示在屏幕上,如果没有找到适配器,则打印一个错误。
# ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS
# endif
# include "pcap.h"
main ( )
{
pcap_if_t * alldevs;
pcap_if_t * d;
int i = 0 ;
char errbuf[ PCAP_ERRBUF_SIZE] ;
if ( pcap_findalldevs_ex ( PCAP_SRC_IF_STRING,
NULL ,
& alldevs, errbuf) == - 1 )
{
fprintf ( stderr ,
"Error in pcap_findalldevs_ex: %s\n" ,
errbuf) ;
exit ( 1 ) ;
}
for ( d = alldevs; d != NULL ; d = d-> next)
{
printf ( "%d. %s" , ++ i, d-> name) ;
if ( d-> description)
printf ( " (%s)\n" , d-> description) ;
else
printf ( " (No description available)\n" ) ;
}
if ( i == 0 )
{
printf ( "\nNo interfaces found! Make sure Npcap is installed.\n" ) ;
return ;
}
pcap_freealldevs ( alldevs) ;
}
运行结果:
下面利用控制面板查看到的网络适配器:
注 :可以发现,通过程序打印出来的适配器列表和通过
windows
的设备管理器查看到的适配器列表有一些区别,主要是数量上的区别。已经打印出来的部分都可以和设备管理器中的对应得上。
关于这个代码的一些注释如下。
首先,pcap_findalldevs_ex()
和其他libpcap函数一样,有一个errbuf
参数。这个参数指向一个由libpcap填充的字符串,其中包括出错时的错误描述。
其次,请记住,并不是所有的操作系统都由libpcap支持,提供网络接口的描述,因此,如果我们想写一个可移植的应用程序,我们必须考虑描述为空的情况:在这种情况下,我们打印字符串"No
description available"。
最后注意,当我们用完这个列表后,我们用pcap_freealldevs()
释放它。
假设我们已经编译了该程序,让我们试着运行它。在我的 Windows10
系统上,我们得到的结果是
1. rpcap://\Device\NPF_{3CEB626A-F27B-4267-B79D-2C6280763C13} (Network adapter 'WAN Miniport (Network Monitor)' on local host)
2. rpcap://\Device\NPF_{4BC226A2-E243-421C-9438-E30A96261403} (Network adapter 'WAN Miniport (IPv6)' on local host)
3. rpcap://\Device\NPF_{D3594FDB-28EF-4133-931A-1CDF0C2F56E1} (Network adapter 'WAN Miniport (IP)' on local host)
4. rpcap://\Device\NPF_{9464D53B-3103-4A4A-9B91-75A5DA984497} (Network adapter 'Microsoft Wi-Fi Direct Virtual Adapter' on local host)
5. rpcap://\Device\NPF_{0D437C70-8383-45EE-A70B-D00C04950AEF} (Network adapter 'Intel(R) Wi-Fi 6 AX200 160MHz' on local host)
6. rpcap://\Device\NPF_{6A1116E4-93E7-4A84-99D1-7E4CB0428CCE} (Network adapter 'Microsoft Wi-Fi Direct Virtual Adapter #2' on local host)
7. rpcap://\Device\NPF_Loopback (Network adapter 'Adapter for loopback traffic capture' on local host)
8. rpcap://\Device\NPF_{B5950B27-9860-4CEE-ADF9-3AFF86481291} (Network adapter 'Realtek PCIe GbE Family Controller' on local host)
3、获取已安装设备的高级信息
上一部分(名为 "获取设备列表
"的部分)演示了如何获取关于可用适配器的基本信息(即设备名称和描述)。实际上,Npcap还提供了其他高级信息。特别是,由pcap_findalldevs_ex()
返回的每个cap_if
结构都包含一个cap_addr
结构的列表,其中包括:
该接口的地址列表。
一个网络掩码的列表(每个掩码对应于地址列表中的一个条目)。
广播地址的列表(每个地址对应于地址列表中的一个条目)。
目标地址的列表(每个地址对应于地址列表中的一个条目)。
此外,cap_findalldevs_ex()
还可以返回远程适配器和位于指定本地文件夹中的pcap文件列表。
下面的代码提供了一个 ifprint()
函数,它可以打印
pcap_if
结构的全部内容。程序对
pcap_findalldevs_ex()
返回的每个条目都会调用该函数。
void ifprint ( pcap_if_t * d)
{
pcap_addr_t * a;
char ip6str[ 128 ] ;
printf ( "%s\n" , d-> name) ;
if ( d-> description)
printf ( "\tDescription: %s\n" , d-> description) ;
printf ( "\tLoopback: %s\n" , ( d-> flags & PCAP_IF_LOOPBACK) ? "yes" : "no" ) ;
for ( a= d-> addresses; a; a= a-> next) {
printf ( "\tAddress Family: #%d\n" , a-> addr-> sa_family) ;
switch ( a-> addr-> sa_family)
{
case AF_INET:
printf ( "\tAddress Family Name: AF_INET\n" ) ;
if ( a-> addr)
printf ( "\tAddress: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> addr) -> sin_addr. s_addr) ) ;
if ( a-> netmask)
printf ( "\tNetmask: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> netmask) -> sin_addr. s_addr) ) ;
if ( a-> broadaddr)
printf ( "\tBroadcast Address: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> broadaddr) -> sin_addr. s_addr) ) ;
if ( a-> dstaddr)
printf ( "\tDestination Address: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> dstaddr) -> sin_addr. s_addr) ) ;
break ;
case AF_INET6:
printf ( "\tAddress Family Name: AF_INET6\n" ) ;
if ( a-> addr)
printf ( "\tAddress: %s\n" , ip6tos ( a-> addr, ip6str, sizeof ( ip6str) ) ) ;
break ;
default :
printf ( "\tAddress Family Name: Unknown\n" ) ;
break ;
}
}
printf ( "\n" ) ;
}
完整代码:
# ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS
# endif
# include <pcap.h>
# include <stdio.h>
# ifndef _WIN32
# include <sys/socket.h>
# include <netinet/in.h>
# else
# include <winsock.h>
# endif
# ifdef _WIN32
# include <tchar.h>
BOOL LoadNpcapDlls ( )
{
_TCHAR npcap_dir[ 512 ] ;
UINT len;
len = GetSystemDirectory ( npcap_dir, 480 ) ;
if ( ! len) {
fprintf ( stderr , "Error in GetSystemDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
_tcscat_s ( npcap_dir, 512 , _T ( "\\Npcap" ) ) ;
if ( SetDllDirectory ( npcap_dir) == 0 ) {
fprintf ( stderr , "Error in SetDllDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
return TRUE;
}
# endif
void ifprint ( pcap_if_t * d) ;
char * iptos ( u_long in) ;
char * ip6tos ( struct sockaddr * sockaddr, char * address, int addrlen) ;
int main ( )
{
pcap_if_t * alldevs;
pcap_if_t * d;
char errbuf[ PCAP_ERRBUF_SIZE + 1 ] ;
# ifdef _WIN32
if ( ! LoadNpcapDlls ( ) )
{
fprintf ( stderr , "Couldn't load Npcap\n" ) ;
exit ( 1 ) ;
}
# endif
if ( pcap_findalldevs ( & alldevs, errbuf) == - 1 )
{
fprintf ( stderr , "Error in pcap_findalldevs: %s\n" , errbuf) ;
exit ( 1 ) ;
}
for ( d = alldevs; d; d = d-> next)
{
ifprint ( d) ;
}
pcap_freealldevs ( alldevs) ;
return 0 ;
}
void ifprint ( pcap_if_t * d)
{
pcap_addr_t * a;
char ip6str[ 128 ] ;
printf ( "%s\n" , d-> name) ;
if ( d-> description)
printf ( "\tDescription: %s\n" , d-> description) ;
printf ( "\tLoopback: %s\n" , ( d-> flags & PCAP_IF_LOOPBACK) ? "yes" : "no" ) ;
for ( a = d-> addresses; a; a = a-> next) {
printf ( "\tAddress Family: #%d\n" , a-> addr-> sa_family) ;
switch ( a-> addr-> sa_family)
{
case AF_INET:
printf ( "\tAddress Family Name: AF_INET\n" ) ;
if ( a-> addr)
printf ( "\tAddress: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> addr) -> sin_addr. s_addr) ) ;
if ( a-> netmask)
printf ( "\tNetmask: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> netmask) -> sin_addr. s_addr) ) ;
if ( a-> broadaddr)
printf ( "\tBroadcast Address: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> broadaddr) -> sin_addr. s_addr) ) ;
if ( a-> dstaddr)
printf ( "\tDestination Address: %s\n" , iptos ( ( ( struct sockaddr_in * ) a-> dstaddr) -> sin_addr. s_addr) ) ;
break ;
case AF_INET6:
printf ( "\tAddress Family Name: AF_INET6\n" ) ;
# ifndef __MINGW32__
if ( a-> addr)
printf ( "\tAddress: %s\n" , ip6tos ( a-> addr, ip6str, sizeof ( ip6str) ) ) ;
# endif
break ;
default :
printf ( "\tAddress Family Name: Unknown\n" ) ;
break ;
}
}
printf ( "\n" ) ;
}
# define IPTOSBUFFERS 12
char * iptos ( u_long in)
{
static char output[ IPTOSBUFFERS] [ 3 * 4 + 3 + 1 ] ;
static short which;
u_char* p;
p = ( u_char* ) & in;
which = ( which + 1 == IPTOSBUFFERS ? 0 : which + 1 ) ;
sprintf ( output[ which] , "%d.%d.%d.%d" , p[ 0 ] , p[ 1 ] , p[ 2 ] , p[ 3 ] ) ;
return output[ which] ;
}
# ifndef __MINGW32__
char * ip6tos ( struct sockaddr * sockaddr, char * address, int addrlen)
{
socklen_t sockaddrlen;
# ifdef _WIN32
sockaddrlen = sizeof ( struct sockaddr_in6 ) ;
# else
sockaddrlen = sizeof ( struct sockaddr_storage ) ;
# endif
if ( getnameinfo ( sockaddr,
sockaddrlen,
address,
addrlen,
NULL ,
0 ,
NI_NUMERICHOST) != 0 ) address = NULL ;
return address;
}
# endif
运行结果:
然后我们看一下这个 WIFI6 适配器,发现它的地址和通过 powershell
查看到的 ipv4 地址是一样的:
结果分析:
npcap 获取了 6
块逻辑网卡的高级信息:包含设备描述、IP地址、子网掩码、广播地址等。
4、打开适配器并捕获数据包
现在我们已经看到了如何获得一个适配器来玩(对,play,所谓的 toy
programs),让我们开始真正的工作,打开一个适配器并捕获一些流量。在这一课中,我们将编写一个程序,打印出流经适配器的每个数据包的一些信息。
打开一个捕获设备的函数是 pcap_open()
。参数
snaplen
,flags
和 to_ms
值得解释一下。
snaplen
:指定了要捕获的数据包的部分。在一些操作系统上(如xBSD和Win32),数据包驱动可以被配置为只捕获任何数据包的初始部分:这减少了要复制到应用程序的数据量,因此提高了捕获的效率。在这种情况下,我们使用65536这个值,这比我们可能遇到的最大MTU要高。通过这种方式,我们确保应用程序将始终收到整个数据包。
flags
:最重要的标志是指示适配器是否将进入混杂模式的标志。在正常操作中,适配器仅捕获来自网络的发往它的数据包;因此,其他主机交换的数据包将被忽略。相反,当适配器处于混杂模式时,它会捕获所有数据包,无论它们是否发往它。这意味着在共享媒体(如非交换以太网)上,Npcap
将能够捕获其他主机的数据包。混杂模式是大多数捕获应用程序的默认模式,因此我们在以下代码中启用它。
to_ms
:指定读取超时,以毫秒为单位。适配器上的读取(例如,使用
pcap_dispatch()
或 pcap_next_ex()
)将始终在
to_ms
毫秒后返回,即使网络上没有可用的数据包。如果适配器处于统计模式,to_ms
还定义统计报告之间的间隔(有关统计模式的信息,请参阅官网教程
"wpcap_tut9")。将 to_ms
设置为 0
意味着没有超时,如果没有数据包到达,适配器上的读取永远不会返回。另一端的
-1 超时会导致适配器上的读取始终立即返回。
# ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS
# endif
# include <pcap.h>
# include <stdio.h>
# include <time.h>
# ifdef _WIN32
# include <tchar.h>
BOOL LoadNpcapDlls ( )
{
_TCHAR npcap_dir[ 512 ] ;
UINT len;
len = GetSystemDirectory ( npcap_dir, 480 ) ;
if ( ! len) {
fprintf ( stderr , "Error in GetSystemDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
_tcscat_s ( npcap_dir, 512 , _T ( "\\Npcap" ) ) ;
if ( SetDllDirectory ( npcap_dir) == 0 ) {
fprintf ( stderr , "Error in SetDllDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
return TRUE;
}
# endif
void packet_handler ( u_char* param, const struct pcap_pkthdr * header, const u_char* pkt_data) ;
int main ( )
{
pcap_if_t * alldevs;
pcap_if_t * d;
int inum;
int i = 0 ;
pcap_t * adhandle;
char errbuf[ PCAP_ERRBUF_SIZE] ;
# ifdef _WIN32
if ( ! LoadNpcapDlls ( ) )
{
fprintf ( stderr , "Couldn't load Npcap\n" ) ;
exit ( 1 ) ;
}
# endif
if ( pcap_findalldevs ( & alldevs, errbuf) == - 1 )
{
fprintf ( stderr , "Error in pcap_findalldevs: %s\n" , errbuf) ;
exit ( 1 ) ;
}
for ( d = alldevs; d; d = d-> next)
{
printf ( "%d. %s" , ++ i, d-> name) ;
if ( d-> description)
printf ( " (%s)\n" , d-> description) ;
else
printf ( " (No description available)\n" ) ;
}
if ( i == 0 )
{
printf ( "\nNo interfaces found! Make sure Npcap is installed.\n" ) ;
return - 1 ;
}
printf ( "Enter the interface number (1-%d):" , i) ;
scanf ( "%d" , & inum) ;
if ( inum < 1 || inum > i)
{
printf ( "\nInterface number out of range.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
for ( d = alldevs, i = 0 ; i < inum - 1 ; d = d-> next, i++ ) ;
if ( ( adhandle = pcap_open_live ( d-> name,
65536 ,
1 ,
1000 ,
errbuf
) ) == NULL )
{
fprintf ( stderr , "\nUnable to open the adapter. %s is not supported by Npcap\n" , d-> name) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
printf ( "\nlistening on %s...\n" , d-> description) ;
pcap_freealldevs ( alldevs) ;
pcap_loop ( adhandle, 0 , packet_handler, NULL ) ;
pcap_close ( adhandle) ;
return 0 ;
}
void packet_handler ( u_char* param, const struct pcap_pkthdr * header, const u_char* pkt_data)
{
struct tm * ltime;
char timestr[ 16 ] ;
time_t local_tv_sec;
( VOID) ( param) ;
( VOID) ( pkt_data) ;
local_tv_sec = header-> ts. tv_sec;
ltime = localtime ( & local_tv_sec) ;
strftime ( timestr, sizeof timestr, "%H:%M:%S" , ltime) ;
printf ( "%s,%.6d len:%d\n" , timestr, header-> ts. tv_usec, header-> len) ;
}
运行结果:
打开适配器后,可以使用 pcap_dispatch()
或
pcap_loop()
开始捕获。这两个函数非常相似,不同之处在于
pcap_dispatch()
在超时到期时返回(尽管不能保证),而
pcap_loop()
在捕获到 cnt
个数据包之前不会返回,因此它可以在下一个任意时间段内阻塞-利用网络。
pcap_loop()
足以满足我们的目的,而
pcap_dispatch()
通常用于更复杂的程序中。
这两个函数都有一个回调参数,packet_handler
,指向一个将接收数据包的函数。这个函数由
libpcap 为来自网络的每个新数据包调用,并接收一个通用状态(对应于
pcap_loop()
和 pcap_dispatch()
的
user
参数),一个包含关于数据包的一些信息的标头,例如时间戳和长度以及数据包的实际数据,包括所有协议头。请注意,帧
CRC
通常不存在,因为它在帧验证后被网络适配器删除。另请注意,大多数适配器会丢弃具有错误
CRC 的数据包,因此 Npcap 通常无法捕获它们。
上面的代码从 pcap_pkthdr
标头中提取每个数据包的时间戳和长度,并将它们打印在屏幕上。
不过要注意,使用 pcap_loop()
可能有一个缺点,主要与抓包驱动程序调用处理程序有关;因此,用户应用程序无法直接控制它。另一种方法(并且具有更易读的程序)是使用
pcap_next_ex()
函数。这里就不再赘述。详情可以参考官方教程 。
5、在没有回调的情况下捕获数据包
本节的代码的行为与前面的程序(名为"打开适配器并捕获数据包"的部分)完全一样,但它使用了
pcap_next_ex()
而不是 pcap_loop()
。
pcap_loop()
的基于回调的捕获机制很优雅,在某些情况下它可能是一个很好的选择。然而,处理回调有时并不实用——它常常使程序更加复杂,特别是在多线程应用程序或C++类的情况下。
在这些情况下,pcap_next_ex()
通过直接调用来获取数据包——使用pcap_next_ex()
,数据包只有在程序员想要的时候才会被接收。
这个函数的参数与捕获回调相同。它接收一个适配器描述符和几个指针,这些指针将被初始化并返回给用户(一个指向pcap_pkthdr
结构,另一个指向包含数据包的缓冲区)。
在下面的程序中,我们回收了上一节代码中的回调代码,并将其移到main()
中,紧接着调用pcap_next_ex()
。
# ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS
# endif
# include <pcap.h>
# include <stdio.h>
# include <time.h>
# ifdef _WIN32
# include <tchar.h>
BOOL LoadNpcapDlls ( )
{
_TCHAR npcap_dir[ 512 ] ;
UINT len;
len = GetSystemDirectory ( npcap_dir, 480 ) ;
if ( ! len) {
fprintf ( stderr , "Error in GetSystemDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
_tcscat_s ( npcap_dir, 512 , _T ( "\\Npcap" ) ) ;
if ( SetDllDirectory ( npcap_dir) == 0 ) {
fprintf ( stderr , "Error in SetDllDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
return TRUE;
}
# endif
int main ( )
{
pcap_if_t * alldevs;
pcap_if_t * d;
int inum;
int i = 0 ;
pcap_t * adhandle;
int res;
char errbuf[ PCAP_ERRBUF_SIZE] ;
struct tm * ltime;
char timestr[ 16 ] ;
struct pcap_pkthdr * header;
const u_char* pkt_data;
time_t local_tv_sec;
# ifdef _WIN32
if ( ! LoadNpcapDlls ( ) )
{
fprintf ( stderr , "Couldn't load Npcap\n" ) ;
exit ( 1 ) ;
}
# endif
if ( pcap_findalldevs ( & alldevs, errbuf) == - 1 )
{
fprintf ( stderr , "Error in pcap_findalldevs: %s\n" , errbuf) ;
return - 1 ;
}
for ( d = alldevs; d; d = d-> next)
{
printf ( "%d. %s" , ++ i, d-> name) ;
if ( d-> description)
printf ( " (%s)\n" , d-> description) ;
else
printf ( " (No description available)\n" ) ;
}
if ( i == 0 )
{
printf ( "\nNo interfaces found! Make sure Npcap is installed.\n" ) ;
return - 1 ;
}
printf ( "Enter the interface number (1-%d):" , i) ;
scanf ( "%d" , & inum) ;
if ( inum < 1 || inum > i)
{
printf ( "\nInterface number out of range.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
for ( d = alldevs, i = 0 ; i < inum - 1 ; d = d-> next, i++ ) ;
if ( ( adhandle = pcap_open_live ( d-> name,
65536 ,
1 ,
1000 ,
errbuf
) ) == NULL )
{
fprintf ( stderr , "\nUnable to open the adapter. %s is not supported by Npcap\n" , d-> name) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
printf ( "\nlistening on %s...\n" , d-> description) ;
pcap_freealldevs ( alldevs) ;
while ( ( res = pcap_next_ex ( adhandle, & header, & pkt_data) ) >= 0 ) {
if ( res == 0 )
continue ;
local_tv_sec = header-> ts. tv_sec;
ltime = localtime ( & local_tv_sec) ;
strftime ( timestr, sizeof timestr, "%H:%M:%S" , ltime) ;
printf ( "%s,%.6d len:%d\n" , timestr, header-> ts. tv_usec, header-> len) ;
}
if ( res == - 1 ) {
printf ( "Error reading the packets: %s\n" , pcap_geterr ( adhandle) ) ;
return - 1 ;
}
pcap_close ( adhandle) ;
return 0 ;
}
为什么我们使用 pcap_next_ex()
而不是以前的
pcap_next()
?因为 pcap_next()
有一些缺点。首先,它的效率很低,因为它隐藏了回调方法,但仍然依赖于
pcap_dispatch()
。其次,它不能检测EOF
,所以当从文件中收集数据包时,它不是很有用。
还请注意,pcap_next_ex()
对成功、超时、错误和EOF
条件返回不同的值。
6、过滤流量
Npcap(以及
libpcap)提供的最强大的功能之一是过滤引擎。它提供了一种非常有效的方式来接收网络流量的子集,并且(通常)与
Npcap 提供的捕获机制集成。用于过滤数据包的函数是
pcap_compile()
和 pcap_setfilter()
。
pcap_compile()
接受一个包含高级布尔(过滤器)表达式的字符串,并生成一个低级别的字节码,该字节码可由数据包驱动程序中的文件过滤器引擎解释。布尔表达式的语法可以在本文档的过滤表达式语法部分中找到。
pcap_setfilter()
在内核驱动中把一个过滤器与一个捕获会话联系起来。一旦调用
pcap_setfilter()
,相关的过滤器将被应用于所有来自网络的数据包,所有符合要求的数据包(即布尔表达式评估为真的数据包)将被实际拷贝到应用程序中。
下面的代码展示了如何编译和设置一个过滤器。注意,我们必须从描述适配器的
pcap_if
结构中获取网络掩码,因为由
pcap_compile()
创建的一些过滤器需要它。
在这个代码片段中传递给 pcap_compile()
的过滤器是“ip and
tcp”,这意味着“只保留 IPv4 和 TCP 的数据包并将它们传递给应用程序”。
if ( d-> addresses != NULL )
netmask= ( ( struct sockaddr_in * ) ( d-> addresses-> netmask) ) -> sin_addr. S_un. S_addr;
else
netmask= 0xffffff ;
if ( pcap_compile ( adhandle, & fcode, "ip and tcp" , 1 , netmask) < 0 )
{
fprintf ( stderr ,
"\nUnable to compile the packet filter. Check the syntax.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
if ( pcap_setfilter ( adhandle, & fcode) < 0 )
{
fprintf ( stderr , "\nError setting the filter.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
上面的代码将会在下面的“解读数据包”这一节种使用到。
7、解读数据包
现在我们能够捕获和过滤网络流量,我们希望将我们的知识用于一个简单的“现实世界”应用程序。
在本节中,我们将从之前的小节中获取代码,并使用这些部分来构建一个更有用的程序。当前程序的主要目的是展示如何解析和解释捕获的数据包的协议头。由此生成的应用程序,称为
UDPdump,打印我们网络上 UDP 流量的摘要。
我们选择了解析和显示UDP协议,因为它比其他协议(如TCP)更容易获得,因此是一个很好的初始例子。让我们看一下代码。
# include <pcap.h>
# include <Winsock2.h>
# include <time.h>
# include <tchar.h>
BOOL LoadNpcapDlls ( )
{
_TCHAR npcap_dir[ 512 ] ;
UINT len;
len = GetSystemDirectory ( npcap_dir, 480 ) ;
if ( ! len) {
fprintf ( stderr , "Error in GetSystemDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
_tcscat_s ( npcap_dir, 512 , _T ( "\\Npcap" ) ) ;
if ( SetDllDirectory ( npcap_dir) == 0 ) {
fprintf ( stderr , "Error in SetDllDirectory: %x" , GetLastError ( ) ) ;
return FALSE;
}
return TRUE;
}
typedef struct ip_address {
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
} ip_address;
typedef struct ip_header {
u_char ver_ihl;
u_char tos;
u_short tlen;
u_short identification;
u_short flags_fo;
u_char ttl;
u_char proto;
u_short crc;
ip_address saddr;
ip_address daddr;
u_int op_pad;
} ip_header;
typedef struct udp_header {
u_short sport;
u_short dport;
u_short len;
u_short crc;
} udp_header;
void packet_handler ( u_char* param,
const struct pcap_pkthdr * header,
const u_char* pkt_data) ;
int main ( )
{
pcap_if_t * alldevs;
pcap_if_t * d;
int inum;
int i = 0 ;
pcap_t * adhandle;
char errbuf[ PCAP_ERRBUF_SIZE] ;
u_int netmask;
char packet_filter[ ] = "ip and udp" ;
struct bpf_program fcode;
if ( ! LoadNpcapDlls ( ) )
{
fprintf ( stderr , "Couldn't load Npcap\n" ) ;
exit ( 1 ) ;
}
if ( pcap_findalldevs_ex ( PCAP_SRC_IF_STRING,
NULL , & alldevs, errbuf) == - 1 )
{
fprintf ( stderr , "Error in pcap_findalldevs: %s\n" , errbuf) ;
exit ( 1 ) ;
}
for ( d = alldevs; d; d = d-> next)
{
printf ( "%d. %s" , ++ i, d-> name) ;
if ( d-> description)
printf ( " (%s)\n" , d-> description) ;
else
printf ( " (No description available)\n" ) ;
}
if ( i == 0 )
{
printf ( "\nNo interfaces found! Make sure Npcap is installed.\n" ) ;
return - 1 ;
}
printf ( "Enter the interface number (1-%d):" , i) ;
scanf_s ( "%d" , & inum) ;
if ( inum < 1 || inum > i)
{
printf ( "\nInterface number out of range.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
for ( d = alldevs, i = 0 ; i < inum - 1 ; d = d-> next, i++ ) ;
if ( ( adhandle = pcap_open ( d-> name,
65536 ,
PCAP_OPENFLAG_PROMISCUOUS,
1000 ,
NULL ,
errbuf
) ) == NULL )
{
fprintf ( stderr ,
"\nUnable to open the adapter. %s is not supported by Npcap\n" ,
d-> name) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
if ( pcap_datalink ( adhandle) != DLT_EN10MB)
{
fprintf ( stderr , "\nThis program works only on Ethernet networks.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
if ( d-> addresses != NULL )
netmask = ( ( struct sockaddr_in * ) ( d-> addresses-> netmask) ) -> sin_addr. S_un. S_addr;
else
netmask = 0xffffff ;
if ( pcap_compile ( adhandle, & fcode, packet_filter, 1 , netmask) < 0 )
{
fprintf ( stderr , "\nUnable to compile the packet filter. Check the syntax.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
if ( pcap_setfilter ( adhandle, & fcode) < 0 )
{
fprintf ( stderr , "\nError setting the filter.\n" ) ;
pcap_freealldevs ( alldevs) ;
return - 1 ;
}
printf ( "\nlistening on %s...\n" , d-> description) ;
pcap_freealldevs ( alldevs) ;
pcap_loop ( adhandle, 0 , packet_handler, NULL ) ;
return 0 ;
}
void packet_handler ( u_char* param,
const struct pcap_pkthdr * header,
const u_char* pkt_data)
{
struct tm ltime;
char timestr[ 16 ] ;
ip_header* ih;
udp_header* uh;
u_int ip_len;
u_short sport, dport;
time_t local_tv_sec;
( VOID) ( param) ;
local_tv_sec = header-> ts. tv_sec;
localtime_s ( & ltime, & local_tv_sec) ;
strftime ( timestr, sizeof timestr, "%H:%M:%S" , & ltime) ;
printf ( "%s.%.6d len:%d " , timestr, header-> ts. tv_usec, header-> len) ;
ih = ( ip_header* ) ( pkt_data +
14 ) ;
ip_len = ( ih-> ver_ihl & 0xf ) * 4 ;
uh = ( udp_header* ) ( ( u_char* ) ih + ip_len) ;
sport = ntohs ( uh-> sport) ;
dport = ntohs ( uh-> dport) ;
printf ( "%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n" ,
ih-> saddr. byte1,
ih-> saddr. byte2,
ih-> saddr. byte3,
ih-> saddr. byte4,
sport,
ih-> daddr. byte1,
ih-> daddr. byte2,
ih-> daddr. byte3,
ih-> daddr. byte4,
dport) ;
}
运行如下:
首先,我们将过滤器设置为“ip and udp”。通过这种方式,我们可以确定
packet_handler()
将只接收 IPv4 上的 UDP
数据包:这简化了解析并提高了程序的效率。
我们还创建了几个描述 IP 和 UDP
标头的结构。packet_handler()
使用这些结构来正确定位各种标头字段。
packet_handler()
虽然仅限于单个协议解析器(UDP over
IPv4),但它显示了 tcpdump/WinDump
等复杂的“嗅探器”如何解码网络流量。由于我们对 MAC
标头不感兴趣,因此我们跳过它。为简单起见,在开始捕获之前,我们使用
pcap_datalink()
检查 MAC
层,以确保我们正在处理以太网网络。这样我们就可以确定 MAC 报头正好是 14
个字节。
IP 标头位于 MAC 标头之后。我们将从 IP 标头中提取 IP
源地址和目标地址。
到达 UDP 报头有点复杂,因为 IP 报头没有固定的长度。因此,我们使用 IP
头的长度字段来知道它的大小。一旦我们知道 UDP
标头的位置,我们就提取源端口和目标端口。
习题与思考题
1、WINPCAP(Npcap)是否能实现服务质量的控制?
答:不能。WinPcap可以独立地通过主机协议发送和接受数据,如同TCP-IP。这就意味着WinPcap不能阻止、过滤或操纵同一机器上的其他应用程序的通讯:它仅仅能简单地"监视"在网络上传输的数据包。所以,它不能提供类似网络流量控制、服务质量调度和个人防火墙之类的支持,因而不能实现服务质量的控制。
参考:
1、https://npcap.com/guide/npcap-tutorial.html