☰
基于WinPcap的网络数据包嗅探与分析工具实现
2026/10/20 7:01:52 网站建设 项目流程

简介:这份资源是华中科技大学2018年计算机网络安全实验的完整源码,面向正在学习网络安全、网络协议分析或准备相关课程实验的高校学生与开发者,帮助理解数据包抓取、协议解析以及ARP欺骗实现中间人攻击的完整流程。压缩包共27个文件,约293KB,以C++源码为主体,包含7个cpp与8个h文件,分别承载抓包线程、发包线程、ARP攻击线程及协议解析等核心逻辑;另有2个ui界面文件、1个pro工程文件与1个qrc资源文件,配合5张png与2张jpg界面截图,便于快速还原工程结构与运行效果。目前已有2814人学习下载,说明该实验方案在同类课程中具有较高的参考价值。读者可从中获得基于WinPcap的数据分析器(嗅探器)实现思路、ARP攻击与中间人攻击的具体编码方式,以及多线程抓包与界面交互的工程组织方法,适合作为课程实验对照、二次开发或网络安全入门实践的参考素材。

1. 从抓包到看懂:一个网络安全实验项目的真实打开方式

很多人对网络安全实验的印象停留在抓包工具点点按钮,但真正让你从零实现一个数据包分析器时,事情就没那么轻松了。这个项目就是基于 WinPcap 实现一个网络数据包嗅探与分析工具,属于计算机网络安全课程中典型的动手实验。它要解决的问题很具体:把网卡上流过的原始数据帧抓下来,逐层解析以太网头、IP 头、TCP/UDP 头,最终把端口、协议类型、载荷内容以可读的形式呈现出来。适合正在做网络安全实验的学生、想理解协议栈底层实现的开发者,以及需要二次开发抓包工具的人。源码结构清晰,依赖 WinPcap 开发包,在 Windows 平台配合 Visual Studio 即可编译运行。

2. 环境搭建与 WinPcap 开发包配置:把编译跑通是第一关

2.1 开发环境选型与依赖说明

这个项目是典型的 Windows 平台 C/C++ 工程,核心依赖是 WinPcap 开发包。WinPcap 提供了用户态直接访问网络驱动层的接口,绕过操作系统的协议栈,拿到最原始的链路层帧。常见做法是安装 WinPcap 运行库(提供 wpcap.dll 和 Packet.dll),再把开发包里的头文件和 lib 文件挂到工程里。

具体需要的东西:

  • Visual Studio(2015 及以上都行,建议用 2019 或 2022)
  • WinPcap 4.1.3 运行库安装包
  • WinPcap 4.1.3 Developer's Pack(开发包,含 pcap.h、wpcap.lib、Packet.lib)
  • 一台有物理网卡的 Windows 机器(虚拟机桥接模式也可以)

安装完运行库后,系统目录下会出现C:\Windows\System32\wpcap.dll和Packet.dll。开发包解压后,把Include目录下的头文件路径加到工程的附加包含目录,把Lib目录下的wpcap.lib和Packet.lib加到附加依赖项。

提示:如果你用的是 64 位编译目标,务必确认链接的是开发包里 x64 目录下的 lib 文件,32 位和 64 位混用会直接报 LNK2019 无法解析的外部符号。

2.2 工程配置的完整步骤

在 Visual Studio 里新建一个空项目,把源码文件加进去,然后按下面的顺序配置:

# 第一步:确认 WinPcap 运行库已安装 # 在命令行执行以下命令,能列出网卡说明运行库正常 # 这一步不是编译必须,但能提前验证环境
// 第二步:在代码中验证能否枚举网卡 // 这是环境是否配置成功的最小验证代码 #include <pcap.h> #include <stdio.h> int main() { pcap_if_t *alldevs; pcap_if_t *d; char errbuf[PCAP_ERRBUF_SIZE]; // 获取本机所有网卡列表 if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) { fprintf(stderr, "枚举网卡失败: %s\n", errbuf); return 1; } int i = 0; for (d = alldevs; d != NULL; d = d->next) { printf("%d. %s\n", ++i, d->name); if (d->description) printf(" 描述: %s\n", d->description); } if (i == 0) { printf("没有找到可用网卡,确认是否以管理员权限运行\n"); return 1; } pcap_freealldevs(alldevs); return 0; }

这段代码的作用是调用pcap_findalldevs_ex枚举本机所有网络适配器。参数PCAP_SRC_IF_STRING表示获取的是可打开的接口列表,errbuf用来接收错误信息。如果返回 -1,说明 WinPcap 运行库没装好或者权限不够。枚举成功后,每个pcap_if_t节点包含网卡名称和描述,后续抓包就是拿这个名称去打开设备。

编译时如果报无法打开源文件 pcap.h,检查附加包含目录是否指向了开发包的Include文件夹。如果报链接错误,检查附加库目录和附加依赖项是否都配了。这两个问题是新手最容易翻车的地方,我见过太多人卡在这一步。

2.3 权限与运行注意事项

WinPcap 抓包需要管理员权限,因为要直接访问网络驱动。在 Visual Studio 里调试时,需要用管理员身份启动 VS,否则pcap_open会返回失败。另外,如果机器上装了 Npcap(WinPcap 的后续兼容版本),两者可能冲突,建议只保留一个。

还有一个容易忽略的点:Windows 防火墙或者某些安全软件可能会拦截抓包行为,导致能打开网卡但抓不到包。遇到这种情况,先把安全软件临时关掉验证一下。

3. 核心模块拆解:从抓包到协议解析的完整链路

3.1 抓包主循环与回调机制

整个嗅探器的核心是一个抓包循环。WinPcap 提供了两种模式:一种是pcap_loop或pcap_dispatch配合回调函数,另一种是pcap_next_ex主动逐个获取。这个项目通常用回调方式,因为结构更清晰。

// 抓包主流程:打开设备 -> 设置过滤 -> 进入循环 pcap_t *adhandle; char errbuf[PCAP_ERRBUF_SIZE]; // 打开选定的网卡,65536 是抓包缓冲区大小,1 表示混杂模式,1000 是超时毫秒 adhandle = pcap_open(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf); if (adhandle == NULL) { fprintf(stderr, "打开网卡失败: %s\n", errbuf); return 1; } // 编译过滤规则,只抓 IP 包,避免抓到大量无关的 ARP 等 struct bpf_program fcode; if (pcap_compile(adhandle, &fcode, "ip", 1, 0xffffff) < 0) { fprintf(stderr, "编译过滤规则失败\n"); return 1; } // 设置过滤器 if (pcap_setfilter(adhandle, &fcode) < 0) { fprintf(stderr, "设置过滤器失败\n"); return 1; } // 进入抓包循环,0 表示无限循环,直到出错或手动停止 // packet_handler 是每抓到一个包就调用的回调函数 pcap_loop(adhandle, 0, packet_handler, NULL);

pcap_open的参数需要重点理解:第一个是网卡名称,第二个是抓包缓冲区大小(snapshot length),设成 65536 能保证完整捕获整个帧。第三个参数PCAP_OPENFLAG_PROMISCUOUS表示混杂模式,能抓到经过网卡但不发往本机的包。第四个是超时时间,影响pcap_loop的响应速度。pcap_compile里的过滤表达式"ip"表示只抓 IP 协议包,这个表达式语法和 tcpdump 一致,可以改成"tcp port 80"之类的更精细规则。

3.2 以太网帧与 IP 头解析

回调函数收到的是原始字节流,需要按协议层次逐层剥离。先看以太网帧头,再看 IP 头。

// 以太网帧头结构,14 字节 typedef struct ether_header { u_char ether_dhost[6]; // 目的 MAC 地址 u_char ether_shost[6]; // 源 MAC 地址 u_short ether_type; // 上层协议类型,0x0800 表示 IP } ether_header; // IP 头结构,至少 20 字节 typedef struct ip_header { u_char ver_ihl; // 版本号(4位) + 头长度(4位) u_char tos; // 服务类型 u_short tlen; // 总长度 u_short identification; // 标识 u_short flags_fo; // 标志 + 片偏移 u_char ttl; // 生存时间 u_char proto; // 上层协议,6 表示 TCP,17 表示 UDP u_short crc; // 头校验和 u_int saddr; // 源 IP 地址 u_int daddr; // 目的 IP 地址 } ip_header; // 回调函数中的解析逻辑 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { ether_header *eth; ip_header *ip; u_int ip_len; // 跳过以太网帧头,定位到 IP 头 eth = (ether_header *)pkt_data; if (ntohs(eth->ether_type) != 0x0800) { return; // 不是 IP 包,直接忽略 } // 定位 IP 头,偏移量为以太网帧头长度 ip = (ip_header *)(pkt_data + 14); // 计算 IP 头实际长度:ver_ihl 低 4 位乘以 4 ip_len = (ip->ver_ihl & 0x0f) * 4; // 打印源 IP 和目的 IP struct in_addr saddr, daddr; saddr.s_addr = ip->saddr; daddr.s_addr = ip->daddr; printf("源IP: %s -> 目的IP: %s 协议: %d 长度: %d\n", inet_ntoa(saddr), inet_ntoa(daddr), ip->proto, ntohs(ip->tlen)); }

这里有几个关键点。ether_type需要用ntohs转成主机字节序再比较,因为网络字节序是大端。IP 头的ver_ihl字段高 4 位是版本号,低 4 位是头长度,单位是 4 字节,所以实际头长度是(ver_ihl & 0x0f) * 4。saddr和daddr是 32 位无符号整数,用inet_ntoa转成点分十进制字符串。proto字段区分上层协议,6 是 TCP,17 是 UDP,1 是 ICMP。

3.3 TCP/UDP 头解析与载荷提取

拿到 IP 头之后,根据proto字段决定下一步解析 TCP 还是 UDP。TCP 头固定 20 字节(不含选项),UDP 头固定 8 字节。

// TCP 头结构,固定 20 字节 typedef struct tcp_header { u_short th_sport; // 源端口 u_short th_dport; // 目的端口 u_int th_seq; // 序列号 u_int th_ack; // 确认号 u_char th_offx2; // 数据偏移(4位) + 保留(4位) u_char th_flags; // 标志位 SYN/ACK/FIN 等 u_short th_win; // 窗口大小 u_short th_sum; // 校验和 u_short th_urp; // 紧急指针 } tcp_header; // 在 packet_handler 中继续解析 if (ip->proto == 6) { // TCP tcp_header *tcp = (tcp_header *)((u_char *)ip + ip_len); u_short sport = ntohs(tcp->th_sport); u_short dport = ntohs(tcp->th_dport); // TCP 头长度:th_offx2 高 4 位乘以 4 int tcp_hdr_len = ((tcp->th_offx2 & 0xf0) >> 4) * 4; // 载荷起始位置 = IP 头起始 + IP 头长度 + TCP 头长度 u_char *payload = (u_char *)ip + ip_len + tcp_hdr_len; int payload_len = ntohs(ip->tlen) - ip_len - tcp_hdr_len; printf(" TCP %u -> %u 载荷长度: %d\n", sport, dport, payload_len); // 如果载荷是 HTTP 明文,可以直接看到请求内容 if (payload_len > 0 && (sport == 80 || dport == 80)) { printf(" HTTP 数据: %.100s\n", payload); } } else if (ip->proto == 17) { // UDP // UDP 头只有 8 字节,结构更简单 u_short *udp = (u_short *)((u_char *)ip + ip_len); u_short sport = ntohs(udp[0]); u_short dport = ntohs(udp[1]); printf(" UDP %u -> %u\n", sport, dport); }

TCP 头的th_offx2字段高 4 位是数据偏移,表示 TCP 头长度,单位同样是 4 字节。载荷的起始位置需要把 IP 头长度和 TCP 头长度都加上。payload_len用 IP 总长度减去两个头长度得到。如果抓的是 HTTP 流量且没有加密,载荷里就是明文的 HTTP 请求或响应,可以直接打印出来看。

UDP 头更简单,前 4 个字节就是源端口和目的端口,各占 2 字节。注意这里用u_short指针加下标的方式访问,因为 UDP 头结构固定且简单,不需要单独定义结构体。

3.4 过滤规则的灵活运用

WinPcap 的过滤表达式基于 BPF 语法,功能很强大。合理使用过滤规则能大幅减少无关包,让分析更聚焦。

过滤表达式含义适用场景
ip只抓 IP 包排除 ARP、STP 等链路层协议
tcp只抓 TCP 包分析 HTTP、HTTPS 等
udp只抓 UDP 包分析 DNS、视频流等
tcp port 80只抓 80 端口 TCP专注 HTTP 流量
host 192.168.1.1只抓与该 IP 相关的包追踪特定主机
src port 53只抓源端口 53 的包分析 DNS 响应

设置过滤规则的代码在前面已经展示过,核心就是pcap_compile编译表达式,pcap_setfilter应用规则。表达式字符串可以做成命令行参数或者配置文件,方便运行时切换。

4. 避坑与常见问题:那些让你抓不到包的原因

4.1 枚举不到网卡或打开失败

现象:程序运行后pcap_findalldevs_ex返回空列表,或者pcap_open返回 NULL。

原因:最常见的是没有以管理员权限运行。WinPcap 需要访问网络驱动层,普通用户权限不够。其次是 WinPcap 运行库没装或者版本不匹配。

解决:用管理员身份启动 Visual Studio 或编译后的 exe。确认C:\Windows\System32下存在wpcap.dll和Packet.dll。如果装了 Npcap,尝试卸载后重装 WinPcap,或者反过来只保留一个。

4.2 能打开网卡但抓不到任何包

现象:pcap_open成功,但pcap_loop一直不触发回调。

原因:可能是过滤规则太严格,把所有包都过滤掉了。也可能是选错了网卡,比如选了虚拟网卡或者没有流量的网卡。还有一种情况是安全软件拦截了抓包操作。

解决:先把过滤规则改成空字符串或者"ip"测试。确认选的是有实际流量的物理网卡。临时关闭安全软件验证。另外,混杂模式在某些虚拟化环境下不生效,可以尝试去掉PCAP_OPENFLAG_PROMISCUOUS标志。

4.3 解析出的 IP 地址或端口是乱码

现象:打印出来的 IP 地址像0.0.0.0或者端口号是很大的数字。

原因:字节序没有转换。网络字节序是大端,x86 机器是小端,不转换就会读反。

解决:所有多字节字段都要用ntohs(16 位)或ntohl(32 位)转成主机字节序。IP 地址用inet_ntoa转换,不要直接打印整数。端口号必须ntohs之后再打印。

4.4 载荷内容打印出来是乱码

现象:HTTP 载荷打印出来一堆不可读字符。

原因:抓到的可能是 HTTPS 流量,载荷是加密的。也可能是二进制协议,本来就不是文本。

解决:先确认端口号,443 是 HTTPS,载荷加密无法直接读。80 端口才是 HTTP 明文。如果是二进制协议,需要按协议格式解析,不能当字符串打印。打印时限制长度,避免刷屏。

4.5 程序运行一段时间后崩溃

现象:抓包循环跑几分钟后程序异常退出。

原因:回调函数里可能有内存泄漏,或者打印速度跟不上抓包速度导致缓冲区溢出。也可能是pcap_loop的 cnt 参数设成了有限值,循环结束后继续访问已释放的资源。

解决:检查回调函数里是否有动态分配内存没释放。减少回调里的打印操作,或者把数据先存到队列里异步处理。pcap_loop的 cnt 参数设成 0 表示无限循环,设成其他值要注意循环结束后的清理逻辑。

5. 二次开发与功能扩展:从实验代码到实用工具

5.1 增加协议识别与统计功能

原始项目只解析了 IP、TCP、UDP 三层,实际使用中往往需要识别更多协议。一个实用的扩展是增加协议统计,比如统计一段时间内各协议的包数量和字节数。

// 简单的协议统计结构 typedef struct { unsigned long tcp_count; unsigned long udp_count; unsigned long icmp_count; unsigned long other_count; unsigned long total_bytes; } proto_stats; proto_stats stats = {0}; // 在 packet_handler 中更新统计 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { stats.total_bytes += header->len; ip_header *ip = (ip_header *)(pkt_data + 14); switch (ip->proto) { case 6: stats.tcp_count++; break; case 17: stats.udp_count++; break; case 1: stats.icmp_count++; break; default: stats.other_count++; break; } } // 在主循环中定期打印统计 // 可以另开一个线程,或者用 pcap_loop 的 cnt 参数分批处理

这个扩展的关键是把统计逻辑和解析逻辑解耦。header->len是抓到的包的实际长度,累加就能得到总流量。协议计数用switch分发,简单直接。如果要做得更细,可以按五元组(源 IP、目的 IP、源端口、目的端口、协议)做哈希统计,找出流量最大的连接。

5.2 把抓到的包保存为 pcap 文件

WinPcap 提供了pcap_dump系列函数,可以把抓到的包直接存成标准 pcap 格式,方便后续用 Wireshark 分析。

pcap_dumper_t *dumpfile; // 在打开网卡之后,创建 dump 文件 dumpfile = pcap_dump_open(adhandle, "capture.pcap"); if (dumpfile == NULL) { fprintf(stderr, "创建 dump 文件失败\n"); return 1; } // 在回调函数中写入 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { // param 参数可以传入 dumpfile pcap_dump((u_char *)dumpfile, header, pkt_data); // ... 其他解析逻辑 } // 循环结束后关闭 pcap_dump_close(dumpfile);

pcap_dump_open的第二个参数是输出文件名,后缀用.pcap方便 Wireshark 识别。pcap_dump在回调里调用,把原始包数据写入文件。注意param参数可以传入dumpfile,这样就不需要全局变量。存下来的文件可以直接用 Wireshark 打开,做更深入的分析。

5.3 改造为带界面的工具

命令行版本适合实验,但实际使用中带界面的工具更友好。常见的做法是用 MFC 或者 Qt 做前端,把抓包逻辑放在独立线程里,通过消息队列把解析结果传给界面刷新。

改造时的关键点:抓包线程不能阻塞界面线程,所以pcap_loop要放在工作线程里。界面刷新频率要控制,不能每抓一个包就刷新一次,可以攒一批数据再更新。网卡列表用下拉框展示,过滤规则用输入框让用户自定义。这些改造不涉及核心抓包逻辑,主要是工程结构的调整。

5.4 我踩过的一个坑

有一次我帮人调试这个实验,代码看起来完全正确,但就是抓不到任何包。排查了半天,最后发现是网卡选错了——代码里默认选了列表第一个网卡,而那台机器上第一个是虚拟网卡,根本没有流量。从那以后我每次做抓包相关的开发,都强制在打开网卡之前把网卡列表和描述打印出来,让用户明确选择,而不是默认选第一个。这个习惯帮我省了很多来回排查的时间。希望这个经验对你有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询