计算机网络实验二

winpcap 编程

实验目的

  • 了解 winpcap 的架构
  • 学习 winpcap 编程

实验原理

WinPcap是一个基于Win32平台的,用于捕获网络数据包并进行分析的开源库。

大多数网络应用程序通过被广泛使用的操作系统元件来访问网络,比如sockets。这是一种简单的实现方式,因为操作系统已经妥善处理了底层具体实现细节(比如协议处理,封装数据包等等),并且提供了一个与读写文件类似的,令人熟悉的接口。

然而,有些时候,这种“简单的方式”并不能满足任务的需求,因为有些应用程序需要直接访问网络中的数据包。也就是说,那些应用程序需要访问原始数据包,即没有被操作系统利用网络协议处理过的数据包。

WinPcap产生的目的,就是为Win32应用程序提供这种访问方式;WinPcap提供了以下功能:

  1. 捕获原始数据包,无论它是发往某台机器的,还是在其他设备(共享媒介)上进行交换的
  2. 在数据包发送给某应用程序前,根据用户指定的规则过滤数据包
  3. 将原始数据包通过网络发送出去
  4. 收集并统计网络流量信息

以上这些功能需要借助安装在Win32内核中的网络设备驱动程序才能实现,再加上几个动态链接库DLL。

所有这些功能都能通过一个强大的编程接口来表现出来,易于开发,并能在不同的操作系统上使用。

WinPcap可以被用来制作网络分析、监控工具。一些基于WinPcap的典型应用有:

  1. 网络与协议分析器 (network and protocol analyzers)
  2. 网络监视器 (network monitors)
  3. 网络流量记录器 (traffic loggers)
  4. 网络流量发生器 (traffic generators)
  5. 用户级网桥及路由 (user-level bridges and routers)
  6. 网络入侵检测系统 (network intrusion detection systems (NIDS))
  7. 网络扫描器 (network scanners)
  8. 安全工具 (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结构的链接列表,每个结构都包含一个连接的适配器的全面信息。特别是,字段namedescription分别包含了相应设备的名称和可读的描述。

下面的代码检索适配器列表并显示在屏幕上,如果没有找到适配器,则打印一个错误。

#ifdef _MSC_VER
/*
 * we do not want the warnings about the old deprecated and unsecure CRT functions
 * since these examples can be compiled under *nix as well
 */
#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];

    /* Retrieve the device list from the local machine */
    if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING,
        NULL /* auth is not needed */,
        &alldevs, errbuf) == -1)
    {
        fprintf(stderr,
            "Error in pcap_findalldevs_ex: %s\n",
            errbuf);
        exit(1);
    }

    /* Print the list */
    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;
    }

    /* We don't need any more the device list. Free it */
    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() 返回的每个条目都会调用该函数。

/* Print all the available information on the given interface */
void ifprint(pcap_if_t *d)
{
  pcap_addr_t *a;
  char ip6str[128];

  /* Name */
  printf("%s\n",d->name);

  /* Description */
  if (d->description)
    printf("\tDescription: %s\n",d->description);

  /* Loopback Address*/
  printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");

  /* IP addresses */
  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


// Function prototypes
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
	/* Load Npcap and its functions. */
	if (!LoadNpcapDlls())
	{
		fprintf(stderr, "Couldn't load Npcap\n");
		exit(1);
	}
#endif

	/* Retrieve the device list */
	if (pcap_findalldevs(&alldevs, errbuf) == -1)
	{
		fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
		exit(1);
	}

	/* Scan the list printing every entry */
	for (d = alldevs;d;d = d->next)
	{
		ifprint(d);
	}

	/* Free the device list */
	pcap_freealldevs(alldevs);

	return 0;
}



/* Print all the available information on the given interface */
void ifprint(pcap_if_t* d)
{
	pcap_addr_t* a;
	char ip6str[128];

	/* Name */
	printf("%s\n", d->name);

	/* Description */
	if (d->description)
		printf("\tDescription: %s\n", d->description);

	/* Loopback Address*/
	printf("\tLoopback: %s\n", (d->flags & PCAP_IF_LOOPBACK) ? "yes" : "no");

	/* IP addresses */
	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__ /* Cygnus doesn't have IPv6 */
			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");
}

/* From tcptraceroute, convert a numeric IP address to a string */
#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__ /* Cygnus doesn't have IPv6 */
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 /* __MINGW32__ */

运行结果:

然后我们看一下这个 WIFI6 适配器,发现它的地址和通过 powershell 查看到的 ipv4 地址是一样的:

结果分析:

npcap 获取了 6 块逻辑网卡的高级信息:包含设备描述、IP地址、子网掩码、广播地址等。

4、打开适配器并捕获数据包

现在我们已经看到了如何获得一个适配器来玩(对,play,所谓的 toy programs),让我们开始真正的工作,打开一个适配器并捕获一些流量。在这一课中,我们将编写一个程序,打印出流经适配器的每个数据包的一些信息。

打开一个捕获设备的函数是 pcap_open()。参数 snaplenflagsto_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
/*
 * we do not want the warnings about the old deprecated and unsecure CRT functions
 * since these examples can be compiled under *nix as well
 */
#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

/* prototype of the packet handler */
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
	/* Load Npcap and its functions. */
	if (!LoadNpcapDlls())
	{
		fprintf(stderr, "Couldn't load Npcap\n");
		exit(1);
	}
#endif

	/* Retrieve the device list */
	if (pcap_findalldevs(&alldevs, errbuf) == -1)
	{
		fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
		exit(1);
	}

	/* Print the list */
	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");
		/* Free the device list */
		pcap_freealldevs(alldevs);
		return -1;
	}

	/* Jump to the selected adapter */
	for (d = alldevs, i = 0; i < inum - 1;d = d->next, i++);

	/* Open the device */
	/* Open the adapter */
	if ((adhandle = pcap_open_live(d->name,	// name of the device
		65536,			// portion of the packet to capture. 
					   // 65536 grants that the whole packet will be captured on all the MACs.
		1,				// promiscuous mode (nonzero means promiscuous)
		1000,			// read timeout
		errbuf			// error buffer
	)) == NULL)
	{
		fprintf(stderr, "\nUnable to open the adapter. %s is not supported by Npcap\n", d->name);
		/* Free the device list */
		pcap_freealldevs(alldevs);
		return -1;
	}

	printf("\nlistening on %s...\n", d->description);

	/* At this point, we don't need any more the device list. Free it */
	pcap_freealldevs(alldevs);

	/* start the capture */
	pcap_loop(adhandle, 0, packet_handler, NULL);

	pcap_close(adhandle);
	return 0;
}


/* Callback function invoked by libpcap for every incoming packet */
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;

	/*
	 * unused parameters
	 */
	(VOID)(param);
	(VOID)(pkt_data);

	/* convert the timestamp to readable format */
	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
/*
 * we do not want the warnings about the old deprecated and unsecure CRT functions
 * since these examples can be compiled under *nix as well
 */
#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
	/* Load Npcap and its functions. */
	if (!LoadNpcapDlls())
	{
		fprintf(stderr, "Couldn't load Npcap\n");
		exit(1);
	}
#endif

	/* Retrieve the device list */
	if (pcap_findalldevs(&alldevs, errbuf) == -1)
	{
		fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
		return -1;
	}

	/* Print the list */
	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");
		/* Free the device list */
		pcap_freealldevs(alldevs);
		return -1;
	}

	/* Jump to the selected adapter */
	for (d = alldevs, i = 0; i < inum - 1;d = d->next, i++);

	/* Open the adapter */
	if ((adhandle = pcap_open_live(d->name,	// name of the device
		65536,			// portion of the packet to capture. 
					   // 65536 grants that the whole packet will be captured on all the MACs.
		1,				// promiscuous mode (nonzero means promiscuous)
		1000,			// read timeout
		errbuf			// error buffer
	)) == NULL)
	{
		fprintf(stderr, "\nUnable to open the adapter. %s is not supported by Npcap\n", d->name);
		/* Free the device list */
		pcap_freealldevs(alldevs);
		return -1;
	}

	printf("\nlistening on %s...\n", d->description);

	/* At this point, we don't need any more the device list. Free it */
	pcap_freealldevs(alldevs);

	/* Retrieve the packets */
	while ((res = pcap_next_ex(adhandle, &header, &pkt_data)) >= 0) {

		if (res == 0)
			/* Timeout elapsed */
			continue;

		/* convert the timestamp to readable format */
		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)
  /* Retrieve the mask of the first address of the interface */
  netmask=((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr;
else
  /* If the interface is without an address
   * we suppose to be in a C class network */
  netmask=0xffffff; 


//compile the filter
if (pcap_compile(adhandle, &fcode, "ip and tcp", 1, netmask) < 0)
{
  fprintf(stderr,
    "\nUnable to compile the packet filter. Check the syntax.\n");
  /* Free the device list */
  pcap_freealldevs(alldevs);
  return -1;
}

//set the filter
if (pcap_setfilter(adhandle, &fcode) < 0)
{
  fprintf(stderr,"\nError setting the filter.\n");
  /* Free the device list */
  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;
}


/* 4 bytes IP address */
typedef struct ip_address {
    u_char byte1;
    u_char byte2;
    u_char byte3;
    u_char byte4;
}ip_address;

/* IPv4 header */
typedef struct ip_header {
    u_char  ver_ihl; // Version (4 bits) + IP header length (4 bits)
    u_char  tos;     // Type of service 
    u_short tlen;    // Total length 
    u_short identification; // Identification
    u_short flags_fo; // Flags (3 bits) + Fragment offset (13 bits)
    u_char  ttl;      // Time to live
    u_char  proto;    // Protocol
    u_short crc;      // Header checksum
    ip_address  saddr; // Source address
    ip_address  daddr; // Destination address
    u_int  op_pad;     // Option + Padding
}ip_header;

/* UDP header*/
typedef struct udp_header {
    u_short sport; // Source port
    u_short dport; // Destination port
    u_short len;   // Datagram length
    u_short crc;   // Checksum
}udp_header;

/* prototype of the packet handler */
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;

    /* Load Npcap and its functions. */
    if (!LoadNpcapDlls())
    {
        fprintf(stderr, "Couldn't load Npcap\n");
        exit(1);
    }

    /* Retrieve the device list */
    if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING,
        NULL, &alldevs, errbuf) == -1)
    {
        fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
        exit(1);
    }

    /* Print the list */
    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");
        /* Free the device list */
        pcap_freealldevs(alldevs);
        return -1;
    }

    /* Jump to the selected adapter */
    for (d = alldevs, i = 0; i < inum - 1;d = d->next, i++);

    /* Open the adapter */
    if ((adhandle = pcap_open(d->name, // name of the device
        65536, // portion of the packet to capture. 
               // 65536 grants that the whole packet
               // will be captured on all the MACs.
        PCAP_OPENFLAG_PROMISCUOUS, // promiscuous mode
        1000, // read timeout
        NULL, // remote authentication
        errbuf // error buffer
    )) == NULL)
    {
        fprintf(stderr,
            "\nUnable to open the adapter. %s is not supported by Npcap\n",
            d->name);
        /* Free the device list */
        pcap_freealldevs(alldevs);
        return -1;
    }

    /* Check the link layer. We support only Ethernet for simplicity. */
    if (pcap_datalink(adhandle) != DLT_EN10MB)
    {
        fprintf(stderr, "\nThis program works only on Ethernet networks.\n");
        /* Free the device list */
        pcap_freealldevs(alldevs);
        return -1;
    }

    if (d->addresses != NULL)
        /* Retrieve the mask of the first address of the interface */
        netmask = ((struct sockaddr_in*)(d->addresses->netmask))->sin_addr.S_un.S_addr;
    else
        /* If the interface is without addresses
         * we suppose to be in a C class network */
        netmask = 0xffffff;


    //compile the filter
    if (pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) < 0)
    {
        fprintf(stderr, "\nUnable to compile the packet filter. Check the syntax.\n");
        /* Free the device list */
        pcap_freealldevs(alldevs);
        return -1;
    }

    //set the filter
    if (pcap_setfilter(adhandle, &fcode) < 0)
    {
        fprintf(stderr, "\nError setting the filter.\n");
        /* Free the device list */
        pcap_freealldevs(alldevs);
        return -1;
    }

    printf("\nlistening on %s...\n", d->description);

    /* At this point, we don't need any more the device list. Free it */
    pcap_freealldevs(alldevs);

    /* start the capture */
    pcap_loop(adhandle, 0, packet_handler, NULL);

    return 0;
}

/* Callback function invoked by libpcap for every incoming packet */
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;

    /*
     * Unused variable
     */
    (VOID)(param);

    /* convert the timestamp to readable format */
    local_tv_sec = header->ts.tv_sec;
    localtime_s(&ltime, &local_tv_sec);
    strftime(timestr, sizeof timestr, "%H:%M:%S", &ltime);

    /* print timestamp and length of the packet */
    printf("%s.%.6d len:%d ", timestr, header->ts.tv_usec, header->len);

    /* retireve the position of the ip header */
    ih = (ip_header*)(pkt_data +
        14); //length of ethernet header

      /* retireve the position of the udp header */
    ip_len = (ih->ver_ihl & 0xf) * 4;
    uh = (udp_header*)((u_char*)ih + ip_len);

    /* convert from network byte order to host byte order */
    sport = ntohs(uh->sport);
    dport = ntohs(uh->dport);

    /* print ip addresses and udp ports */
    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


计算机网络实验二
http://fanyfull.github.io/2022/05/31/计算机网络实验二/
作者
Fany Full
发布于
2022年5月31日
许可协议