☰
VC++网卡抓包源码解析:Packet32驱动实现与协议分析实战
2026/10/1 11:34:16 网站建设 项目流程

简介:这是一份面向网络编程初学者与安全技术爱好者的 VC++ 网络抓包程序源代码,基于 WinPcap 的 Packet32 驱动接口实现网卡数据包嗅探与截取,可用于学习底层协议解析、网卡混杂模式设置及数据包过滤等核心技能。压缩包共 44 个文件,以 16 个 .h 头文件与 8 个 .cpp 源文件为主体,涵盖协议定义、IP 地址处理、过滤对话框与主框架等模块,另含 dsw/dsp 工程文件、vxd 虚拟驱动、dll 动态库、ico/bmp 图标位图资源及 readme 说明,整体约 78KB,结构紧凑、便于直接编译调试。目前已有 780 人学习下载。读者可从中获取完整的抓包工程源码,理解 Packet32 驱动调用流程、网卡数据捕获与过滤逻辑,并借助现成工程快速搭建自己的网络嗅探实验环境,适合作为课程设计、毕业设计或底层网络编程入门的参考实例。

1. 从一份 VC++ 抓包源码说起:网卡数据包截获到底怎么落地

手上这份vc++ sniff截取网卡数据包,网络抓包程序,源代码.zip,是我最近翻出来重读的一套老工程。它做的事很直接:在 Windows 上通过 Packet32 驱动接口打开物理网卡,把流经网卡的数据帧原样捞上来,再按协议头逐层解析。压缩包里既有 MFC 界面工程GetPacket.dsw,也有底层驱动封装Packet32,还有Packet32.dll、zpacket.vxd这类运行时依赖,属于一套能编译、能跑、能改的完整抓包程序源码。

它解决的是「我想自己写一个抓包工具,而不是只会用现成软件」这个诉求。适合两类人:一是学网络协议、想亲眼看到以太网帧长什么样的学生和新人;二是需要做私有协议分析、内网流量排查,又不方便引入大型框架的工程师。关键词里c语言 抓包、vc++、sniff、网卡数据包这几个词,基本就是它的全部技术画像。下面我按「它是什么 → 怎么编译跑起来 → 怎么改过滤和解析 → 坑在哪」的顺序拆一遍。

2. Packet32 驱动模型与工程结构:抓包程序的骨架长什么样

2.1 为什么是 Packet32 而不是原始套接字

很多人第一反应是用 Winsock 的SOCK_RAW抓包,但原始套接字在 Windows 上有明显边界:它拿到的是去掉链路层之后的 IP 层数据,以太网帧头、ARP 帧这些链路层内容根本看不到,而且不同系统版本对原始套接字的限制越来越紧。这份源码走的是另一条路——Packet32,它是一层 NDIS 协议驱动,直接挂在网卡驱动之上,能拿到完整的以太网帧。

Packet32 的工作模型可以这样理解:应用层调用PacketOpenAdapter打开网卡,拿到一个句柄;然后PacketSetHwFilter设置硬件过滤模式,比如NDIS_PACKET_TYPE_PROMISCUOUS就是常说的混杂模式,让网卡不再只收发给自己的帧;接着PacketSetBuff分配内核缓冲区,PacketSetReadTimeout设超时,最后用PacketReceivePacket把数据批量读上来。这套接口在Packet32.h里声明,实现在Packet32.c,编译产物就是Packet32.dll。

提示:混杂模式只是让网卡把「不是发给本机」的帧也交给驱动,能不能真正收到,还取决于你所在网络是不是交换网络。交换环境下别人的单播流量不会泛洪到你的端口,这一点后面避坑章节会细说。

2.2 工程文件逐个点名

压缩包里的文件不是随便堆的,按职责能分成四组,我整理成一张表方便对照:

分组代表文件职责
MFC 界面层GetPacket.dsw、MainFrm.cpp、GetPacketView.cpp、GetPacketListView.cpp主窗口、列表控件、菜单响应
抓包逻辑层GetPacket.cpp、GetPacketDoc.cpp、GetPacket.h打开网卡、收包线程、数据分发
过滤与解析FileterDlg.cpp、FileterDlg.h、protocol.h、ipaddr.cpp过滤条件对话框、协议结构体、IP 地址处理
驱动与依赖Packet32目录、Packet32.dll、zpacket.vxd、PACKOFF.H、NTDDNDIS.H底层驱动接口、NDIS 常量定义

protocol.h是理解解析逻辑的入口,里面大概率定义了以太网头、IP 头、TCP/UDP 头的结构体。ipaddr.cpp负责点分十进制和网络字节序之间的转换。FileterDlg对应界面上那个过滤设置对话框,用来填 IP、端口之类的条件。zpacket.vxd是给老版本 Windows 9x 用的虚拟设备驱动,现在基本用不上,但说明这套代码年代较早,跨版本兼容性要留意。

2.3 一次收包的完整调用链

把调用链理清楚,改代码时才知道该动哪一层。典型流程是这样的:

// 打开网卡,获取适配器句柄 LPADAPTER lpAdapter = PacketOpenAdapter(AdapterName); if (lpAdapter == NULL) { // 打开失败,通常是权限不足或网卡名不对 return; } // 设置混杂模式,抓取所有流经网卡的帧 PacketSetHwFilter(lpAdapter, NDIS_PACKET_TYPE_PROMISCUOUS); // 分配接收缓冲区,这里给 0x100000 即 1MB PacketSetBuff(lpAdapter, 0x100000); // 设置读超时,单位毫秒,避免线程死等 PacketSetReadTimeout(lpAdapter, 1000); // 循环收包 LPPACKET lpPacket = PacketAllocatePacket(); PacketInitPacket(lpPacket, buffer, sizeof(buffer)); while (bRunning) { if (PacketReceivePacket(lpAdapter, lpPacket, TRUE)) { // 解析 buffer 中的以太网帧 ParseEthernetFrame((unsigned char*)buffer, lpPacket->ulBytesReceived); } }

逻辑说明:PacketOpenAdapter的参数是网卡设备名,格式类似\\.\Packet_{GUID},不是我们平时看到的「本地连接」这种友好名,需要通过PacketGetAdapterNames枚举出来。PacketSetBuff的缓冲区大小直接影响高流量下的丢包率,太小会丢,太大占内核内存。PacketReceivePacket第三个参数为TRUE表示同步等待,配合超时使用。解析部分从缓冲区起始位置按以太网头 14 字节、IP 头 20 字节、TCP 头 20 字节这样逐层偏移。

参数说明:NDIS_PACKET_TYPE_PROMISCUOUS是混杂模式常量,定义在NTDDNDIS.H里;如果只想收广播和组播,可以换成NDIS_PACKET_TYPE_BROADCAST | NDIS_PACKET_TYPE_MULTICAST。缓冲区大小我一般按链路速率估:百兆网卡给 256KB 到 512KB,千兆网卡建议 1MB 以上,否则突发流量下PacketReceivePacket返回的包会明显变少。

3. 编译与运行:从 dsw 到能抓到第一个包

3.1 环境准备与工程打开

这套工程是 VC++ 6.0 时代的.dsw工作区格式,用 VS2017 及以上打开会触发工程升级向导。我的建议是:如果只是想跑起来看效果,用 VS2010 到 VS2013 这一档最省事,升级改动小;如果非要用新版 VS,升级后重点检查Packet32子工程的字符集和运行库设置。

打开步骤:先解压,用 VS 打开GetPacket.dsw,解决方案资源管理器里会看到GetPacket和Packet32两个工程。Packet32是驱动封装库,必须先编译它生成Packet32.dll和对应的导入库,否则主工程链接会报找不到符号。

# 如果习惯命令行编译,可以用 msbuild 指定配置 msbuild Packet32\Packet32.dsp /p:Configuration=Release /p:Platform=Win32 msbuild GetPacket.dsp /p:Configuration=Release /p:Platform=Win32

逻辑说明:先编Packet32再编主工程,顺序不能反。/p:Configuration=Release指定发布配置,调试阶段可以换成Debug方便断点。参数Platform=Win32表示 32 位目标,这套老代码基本都是 32 位,强行编 64 位会遇到指针长度和驱动接口不匹配的问题。

3.2 运行时依赖的摆放

编译成功后,Release目录下会有GetPacket.exe。但直接双击往往起不来,因为Packet32.dll和zpacket.vxd需要放到 exe 同目录,或者注册到系统。Packet32.dll是必须的,zpacket.vxd只在 Windows 9x 下有意义,现代系统可以忽略。

# 把驱动 dll 拷到 exe 同目录,最省事的做法 copy Packet32\Release\Packet32.dll Release\ # 确认 exe 依赖是否齐全 dumpbin /dependents Release\GetPacket.exe

逻辑说明:dumpbin /dependents会列出 exe 依赖的所有 dll,如果Packet32.dll显示为缺失,说明没放对位置。参数/dependents是 dumpbin 的依赖查看开关,比用 Depends 工具轻量。这一步做完,以管理员身份运行GetPacket.exe,界面上应该能列出本机网卡。

3.3 选网卡、开混杂、看第一个包

程序跑起来后,操作顺序是:在网卡下拉框里选中要抓的适配器,点「开始」或类似按钮,列表控件里就会滚动出现数据包。第一次跑,建议先抓 ARP 或者 ping 产生的 ICMP,因为这两种包特征明显、容易辨认。

判断是否真的抓到了:看列表里有没有出现源 MAC、目的 MAC、协议类型这几列。如果协议类型显示0x0806就是 ARP,0x0800就是 IP。抓不到任何包时,先确认是不是管理员权限,再确认网卡选对没有,最后看混杂模式有没有生效。

注意:Windows 下打开 Packet32 适配器需要管理员权限,普通用户运行会PacketOpenAdapter返回 NULL。这不是代码 bug,是驱动层的权限要求。

4. 过滤与协议解析:让抓到的包能看懂

4.1 过滤逻辑放在哪一层

抓包程序最怕的是「什么都抓,列表刷得飞快,真正想看的包被淹没」。这份源码里FileterDlg提供了过滤条件输入,过滤逻辑一般放在收包线程解析之后、入列表之前。过滤分两层:一层是硬件过滤,通过PacketSetHwFilter设置,只能按广播、组播、混杂这种粗粒度来;另一层是软件过滤,在收到帧之后按 IP、端口、协议号自己判断。

// 软件过滤示例:只保留指定 IP 和端口的 TCP 包 bool MatchFilter(unsigned char* frame, unsigned long len, const FilterRule& rule) { // 以太网头 14 字节,类型字段在 12-13 字节 unsigned short ethType = (frame[12] << 8) | frame[13]; if (ethType != 0x0800) return false; // 非 IP 包直接丢弃 // IP 头从第 14 字节开始,协议号在偏移 9 unsigned char* ip = frame + 14; unsigned char proto = ip[9]; if (proto != 6) return false; // 非 TCP 丢弃 // 源 IP 在偏移 12,目的 IP 在偏移 16 unsigned int srcIp = *(unsigned int*)(ip + 12); unsigned int dstIp = *(unsigned int*)(ip + 16); if (srcIp != rule.ip && dstIp != rule.ip) return false; // TCP 头从 IP 头之后开始,长度由 IP 头 IHL 字段决定 int ipHdrLen = (ip[0] & 0x0F) * 4; unsigned char* tcp = ip + ipHdrLen; unsigned short srcPort = (tcp[0] << 8) | tcp[1]; unsigned short dstPort = (tcp[2] << 8) | tcp[3]; return (srcPort == rule.port || dstPort == rule.port); }

逻辑说明:这段代码演示了从以太网帧逐层剥到 TCP 端口的过程。ethType判断是不是 IP 包,proto判断是不是 TCP,ipHdrLen用 IP 头第一个字节的低 4 位乘以 4 得到实际头长,因为 IP 头可能有选项字段,不能死写 20 字节。参数rule是从FileterDlg收集来的过滤条件。

参数说明:0x0800是 IPv4 的以太网类型,0x0806是 ARP,0x86DD是 IPv6。协议号6是 TCP,17是 UDP,1是 ICMP。这些常量在protocol.h里通常有定义,改代码时优先用宏而不是魔数。

4.2 协议头结构体与字节序

protocol.h里定义的结构体是解析的基础。写抓包解析最容易翻车的地方是字节序:网络字节序是大端,x86 是小端,直接读多字节字段会得到反的值。ipaddr.cpp里的转换函数就是干这个的。

// 网络字节序转主机字节序,端口和 IP 都要过一遍 unsigned short ntohs_custom(unsigned short netshort) { return (netshort >> 8) | (netshort << 8); } // 把 4 字节 IP 转成点分十进制字符串 void IpToString(unsigned int ip, char* out) { sprintf(out, "%d.%d.%d.%d", ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); }

逻辑说明:ntohs_custom做 16 位字节交换,端口号必须过这一步,否则 80 端口会显示成 20480。IpToString按小端顺序取字节,因为从帧里读出来的 IP 已经是网络序,在 x86 上按内存顺序取正好是反的,所以从低字节开始拼。

参数说明:如果直接用系统提供的ntohs、ntohl,需要包含winsock2.h,注意和windows.h的包含顺序,先包含winsock2.h再包含windows.h,否则会有一堆重定义错误,这是 VC++ 网络编程的经典坑。

4.3 列表控件的刷新与性能

GetPacketListView.cpp负责把解析结果塞进列表控件。MFC 的CListCtrl在插入大量行时性能很差,几千行就会卡。常见做法是:收包线程只负责把包存进一个队列,界面线程定时批量刷新,而不是每来一个包就InsertItem一次。

// 收包线程只入队,不碰界面 void PacketThread() { while (bRunning) { if (PacketReceivePacket(lpAdapter, lpPacket, TRUE)) { PacketRecord rec; ParseFrame((unsigned char*)buffer, lpPacket->ulBytesReceived, rec); { CSingleLock lock(&m_queueLock, TRUE); m_packetQueue.push_back(rec); } } } } // 界面定时器里批量取出刷新 void OnTimer(UINT nIDEvent) { CSingleLock lock(&m_queueLock, TRUE); while (!m_packetQueue.empty()) { PacketRecord& rec = m_packetQueue.front(); m_listCtrl.InsertItem(0, rec.srcMac); m_listCtrl.SetItemText(0, 1, rec.dstMac); m_packetQueue.pop_front(); } }

逻辑说明:收包线程和界面线程通过加锁队列解耦,避免跨线程直接操作控件导致崩溃或卡顿。CSingleLock是 MFC 的锁封装,配合CCriticalSection使用。参数上,定时器间隔设 200 到 500 毫秒比较合适,太短刷新频繁,太长看起来像卡死。

提示:跨线程操作 MFC 控件是抓包程序崩溃的高发区,血泪经验是界面更新一律走消息或定时器,绝不在收包线程里直接SetItemText。

5. 避坑与排查:这份老源码最容易翻车的地方

5.1 现象:程序能跑但一个包都抓不到

原因:最常见是权限问题,PacketOpenAdapter需要管理员权限;其次是网卡名传错,PacketGetAdapterNames返回的是设备名不是友好名;还有一种情况是选中的网卡没有实际流量,比如选了个虚拟网卡或已断开的无线网卡。

解决:右键以管理员身份运行;在代码里打印枚举出来的适配器名,确认选中的是活跃网卡;先用 ping 制造流量再观察。如果还是不行,用PacketSetHwFilter设成NDIS_PACKET_TYPE_ALL_LOCAL试试,排除混杂模式设置失败的可能。

5.2 现象:交换网络下抓不到别人的通信

原因:这是对混杂模式最常见的误解。混杂模式只让网卡不丢弃「目的 MAC 不是自己」的帧,但在交换网络中,交换机只会把单播帧转发到目标端口,你的端口根本收不到别人的单播流量,网卡再混杂也没用。

解决:能抓到的只有三类——发给本机的、广播的、组播的。想抓全网流量,需要在交换机上配置端口镜像,或者用集线器这种共享介质设备。这不是代码能解决的,属于网络拓扑层面的限制,别在这上面浪费时间调代码。

5.3 现象:高流量下大量丢包

原因:PacketSetBuff分配的缓冲区太小,或者收包线程处理太慢,导致内核缓冲区溢出。另外PacketReceivePacket如果设成非阻塞轮询,CPU 占用高但效率未必好。

解决:把缓冲区调大,千兆环境给到 2MB 甚至 4MB;收包线程里解析逻辑尽量轻,重活丢给后续处理;用PacketSetReadTimeout配合同步等待,让驱动在没数据时挂起线程而不是空转。丢包率可以用「发送端计数 vs 接收端计数」来量化验证。

5.4 现象:VS 高版本编译报一堆类型错误

原因:老代码用了很多 VC++ 6.0 的隐式类型转换和过时的 API,新版编译器默认更严格,char*到LPCTSTR的转换、sprintf的安全警告都会变成错误。

解决:在工程属性里把字符集设为「未设置」或「多字节字符集」,关闭 SDL 检查(/sdl-),把sprintf换成sprintf_s或加_CRT_SECURE_NO_WARNINGS宏。升级向导跑完后,重点看Packet32子工程的预处理器定义有没有丢。

5.5 现象:解析出来的端口和 IP 是乱码

原因:字节序没处理,或者结构体对齐方式和实际帧不一致。C 编译器默认会对结构体做对齐填充,而网络帧是紧凑排列的,用结构体直接映射帧内存会错位。

解决:解析时用指针加偏移的方式逐字段读,不要图省事用结构体强转;端口和 IP 一律过ntohs/ntohl;如果非要用结构体,加#pragma pack(1)取消对齐。这个坑我在好几个抓包项目里都踩过,偏移量算错一个字节,后面全乱。

6. 进阶改造:把这份源码变成自己的协议分析工具

跑通只是起点,真正有价值的是按自己的需求改造。我一般会从三个方向动手,这里把具体做法和验证方法写清楚。

第一个方向是加协议解析插件。protocol.h里现在大概率只覆盖了以太网、IP、TCP、UDP 这几层,想解析自定义协议,可以在ParseFrame之后加一个分发函数,按端口号或魔数判断协议类型,再调用对应的解析器。验证方法是构造一段已知内容的测试流量,看解析结果和预期是否一致。

// 按端口分发到自定义协议解析器 void DispatchProtocol(unsigned char* payload, int len, unsigned short dstPort) { switch (dstPort) { case 8080: ParseMyProtocol(payload, len); // 自定义协议 break; case 1883: ParseMqtt(payload, len); // MQTT break; default: DumpHex(payload, len); // 未知协议先十六进制打印 break; } }

逻辑说明:DispatchProtocol在 TCP 载荷解析之后调用,payload指向应用层数据起始位置,len是载荷长度。参数dstPort用来做初步分类,但更可靠的方式是看载荷开头的魔数,因为端口可以改,魔数不会骗人。

第二个方向是加落盘功能。抓包工具不落盘,关掉就没了,排查问题时很被动。可以在收包线程里加一个写文件分支,把原始帧按pcap格式写出去,这样还能用 Wireshark 打开做二次分析。pcap文件头是 24 字节,每个包前面加 16 字节的包头,记录时间戳和长度。验证方法是写完用 Wireshark 打开,能正常解析就说明格式对了。

第三个方向是加统计视图。比起一行行看包,按协议、按 IP 对统计流量更直观。可以在GetPacketListView旁边加一个统计页,用std::map按源 IP 累加字节数,定时刷新。这个改造对定位「谁在占带宽」特别有用。

最后说一个我自己的习惯:每次改完解析逻辑,我都会先用一段固定的测试流量跑一遍,把结果和 Wireshark 的解析对照,确认偏移和字节序都对得上,再拿去抓真实流量。从那以后我每次动协议解析代码,都强制走一遍这个对照流程,省了很多事后排查的功夫。希望这份源码和上面的拆解,能帮你把抓包这件事真正落到自己手里。

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

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

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

立即咨询