简介:这是一份基于Visual C++开发的网卡数据包嗅探与抓包程序源代码,面向网络编程学习者、协议分析初学者以及希望研究抓包工具内部实现的开发者,适合在课程设计或毕业设计中作为基础工程来学习借鉴。压缩包内共44个文件、整体大小约78KB,以16个头文件和8个C++源文件为主,并配有dsw/dsp工程文件、rc界面资源、bmp与ico图标,以及编译生成的可执行程序、Packet32.dll和vxd驱动文件等,既能查看源码,也可直接运行验证抓包效果。资源已有777人学习/下载,具备一定的参考热度。项目主要程序围绕GetPacket展开,包含主框架、文件列表视图、过滤设置对话框和Packet32接口封装等模块,覆盖了网卡设备打开、数据包捕获、过滤规则处理以及界面刷新显示等关键环节;头文件中还给出IP、TCP等协议结构定义,方便继续做报文解析与数据包分析。对于想动手实现简易嗅探器、研究Windows驱动级抓包原理或完成相关网络实验的同学,这份源代码能提供完整的工程结构和可直接借鉴的VC++网络编程思路。
1. 为什么还在拆一份 VC++ 6.0 的 Sniff 源码
一份用 VC++ 6.0 写的网络抓包程序,放在今天看,依然有拆解价值。这个GetPacket项目不是套接字层的小打小闹,而是直接通过Packet32.dll绕过协议栈,从网卡驱动层取原始数据帧,能看见 MAC 地址、IP 分片、TCP 标志位,甚至能自己构造过滤规则。对想深入了解抓包原理的 C/C++ 开发者来说,这个源码比 WinPcap 封装后的调用示例更接近底层——你能看到 VxD、DLL、MFC 界面三者怎么协作。资源包里zpacket.vxd和Packet32.c同时出现,说明它保留了 98/2000 时代网络驱动接口的完整实现,这类代码现在已经很难找到完整工程了。适合需要做协议解析、网络监控、教学实验的人当原型参考,也适合想搞懂DeviceIoControl下发抓包命令的读者。
2. 从 zpacket.vxd 到 Packet32.dll:网卡抓包的底层链路
2.1 用户态与内核态的抓包分工
普通 Socket 编程只能收到内核协议栈处理完的数据,而嗅探器必须在数据进入协议栈之前把它复制一份。这份源码采用了两层结构:用户态的Packet32.dll封装 API,内核态由zpacket.vxd完成网卡驱动绑定、混杂模式设置、环形缓冲区管理。zpacket.vxd是 Windows 9x 时代的虚拟设备驱动,通过DeviceIoControl与用户态通信。
代码清单里DEVIOCTL.H和NTDDNDIS.H揭示了通信方式:前者定义了 IOCTL 控制码,后者提供了 NDIS(Network Driver Interface Specification)结构体。常见做法是先用PacketOpenAdapter打开设备,再调用PacketSetHwFilter设置混杂模式,最后PacketReceive进入等待状态。整个链路里,每一层都有各自的缓冲策略,这也是后面分析抓包丢失问题的关键。
2.2 Packet32.dll 的 API 边界与数据结构
资源包里的Packet32.h声明了这套库的对外接口。和 WinPcap 不同,这个实现非常精简,核心入口只有几个:
// Packet32.h 关键函数(摘录) LPADAPTER PacketOpenAdapter(char *AdapterName); BOOLEAN PacketSetHwFilter(LPADAPTER Adapter, ULONG Filter); BOOLEAN PacketSetBuff(LPADAPTER Adapter, int dim); BOOLEAN PacketReceive(LPADAPTER Adapter, BOOLEAN flush, LPPACKET Packet); BOOLEAN PacketSendPacket(LPADAPTER Adapter, LPPACKET Packet, BOOLEAN Sync);调用顺序是PacketOpenAdapter拿到句柄,PacketSetBuff设置内核缓冲,然后循环PacketReceive。每个PACKET结构里包含了bpf_stat(统计信息)和ulBytesReceived,可以从PacketReceive返回后读取实际字节数。PacketSetHwFilter的参数NDIS_PACKET_TYPE_PROMISCUOUS让网卡接收所有经过的数据帧,这就实现了混杂模式。
在这个源码里,GetPacket.cpp的主文档类负责管理适配器生命周期,而Packet32.c则直接拿 C 代码实现 DLL 的底层逻辑,两者共用packoff.h/packon.h做结构体字节对齐控制,避免因为有其他编译选项而导致布局错位。
3. GetPacket 主流程:抓包线程、回调与 UI 联动
3.1 MFC 文档视图结构下的抓包入口
工程是基于 MFC 的 SDI 架构,GetPacketView.cpp是界面入口,GetPacketListView.cpp负责把抓到的包列成列表,GetPacketDoc.cpp维护数据文档。这种设计在现在的网络工具里不常见,新的图形界面框架喜欢把逻辑全放在一个类里,但 MFC 的 Doc/View 分离在抓包场景中反而有优势——视图只管刷新,文档全局持有数据包队列,抓包线程写队列,UI 线程定时读取。
启动抓包的操作在视图里触发,然后调用文档对象的抓包方法:
// GetPacketView.cpp 中启动抓包的入口 void CGetPacketView::OnStartSniff() { CGetPacketDoc* pDoc = GetDocument(); if (pDoc && !pDoc->m_bSniffing) { pDoc->OpenAdapter(m_strAdapterName); // 打开网卡 pDoc->StartCaptureThread(); // 启动抓包线程 } }这段代码的重要逻辑是OpenAdapter与StartCaptureThread分开。抓包线程一旦启动就不能反复创建,开关只允许点击一次,后面通过m_bSniffing标志位防重入。m_strAdapterName来自一个设备选择对话框,实际来源于底层枚举出的网卡名称字符串,形如\\.\Packet.dll这样的旧的 NDIS 路径。
3.2 抓包循环与数据分发
抓包线程的核心是一个死循环,这条循环会持续阻塞在PacketReceive上。每收到一包就包一层自定义结构,塞进文档类的CPtrList队列。协议解码并不在抓包线程做,而是延迟到 UI 线程查询列表时再做,这样能减少抓包线程的额外开销。
// GetPacketDoc.cpp 抓包线程函数(精简) UINT CGetPacketDoc::CaptureThread(LPVOID pParam) { CGetPacketDoc* pDoc = (CGetPacketDoc*)pParam; char* buffer = new char[BUF_SIZE]; // 自定义抓包缓冲 PACKET packet; PacketInitPacket(&packet, buffer, BUF_SIZE); while (!pDoc->m_bStop) { if (PacketReceive(pDoc->m_lpAdapter, TRUE, &packet)) { if (packet.ulBytesReceived > 0) { pDoc->AddPacket(packet.ulBytesReceived, (BYTE*)packet.pBuffer); } } else { Sleep(1); // 防止忙等导致 UI 卡死 } } delete[] buffer; return 0; }这里有个细节:PacketReceive的第二个参数flush传的是TRUE。这个参数决定内核驱动是否在调用返回前清空缓冲区,如果不传TRUE,缓冲区数据会以更高效的方式批量提交,但单个包延迟会变大。实时抓包时传TRUE,离线分析时传FALSE,这个边界值得记住。
AddPacket内部会做一次memcpy把数据从临时缓冲拷入文档队列,因为下一轮循环会复用buffer。如果没有这步拷贝,前面的包内容会被覆盖。这个拷贝开销在千兆网卡全速抓包时不可忽略,也是很多抓包工具优先使用内存映射的原因之一。
3.3 过滤对话框 FileterDlg 与参数传递
FileterDlg.cpp实现了一个简单的过滤设置窗口。和 Wireshark 的 BPF 语法不同,它用的是枚举型条件,比如按协议类型、端口号过滤。这个对话框不是在内核层过滤,而是在应用层读包之后、显示之前过滤,因此过滤后抓到的包仍然占用内存缓冲,只是不显示出来。
// FileterDlg.cpp 读取用户选择的过滤条件 void CFileterDlg::OnOK() { if (IsDlgButtonChecked(IDC_CHECK_TCP)) { m_dwFilter |= FILTER_TCP; } if (IsDlgButtonChecked(IDC_CHECK_UDP)) { m_dwFilter |= FILTER_UDP; } // 从编辑框取端口号 CString strPort; GetDlgItemText(IDC_EDIT_PORT, strPort); m_nPort = _ttoi(strPort); CDialog::OnOK(); }过滤条件用一个位掩码加一个整型端口获得,传到文档类后,解码函数里直接判断标志位。这种实现的好处是简单直接,坏处是每个包都要遍历一次过滤条件,性能下降明显。后续如果做了字节码级过滤器,就不需要在用户态逐包判断了。
4. protocol.h / ipaddr.h:以太网帧与 IP/TCP 头解析
4.1 帧头结构定义与字节序陷阱
protocol.h里定义了一套手工对齐的网络协议结构。它没有使用<winsock2.h>的标准结构,而是自己重新写了EtherHdr、IPHdr、TCPHdr、UDPHdr。原因是为了在没有#pragma pack的编译环境下保证结构体布局可控。
// protocol.h 中的以太网帧头定义 #pragma pack(push, 1) typedef struct _EtherHdr { BYTE dest[6]; // 目的 MAC BYTE src[6]; // 源 MAC WORD type; // 上层协议类型 0x0800=IP } EtherHdr, *PEtherHdr; #pragma pack(pop)packoff.h和packon.h在这个工程里的作用就是替代#pragma pack,它们本质上是在不同编译器之间统一字节对齐开关。WORD type是网络字节序的,比如 0x0800 在内存里是00 08,直接比较时会发现type不等于0x0800,必须用ntohs转换。这个错误在初学者改源码时频繁出现,而且表现得很隐蔽——所有 IP 包都被误判成未知协议。
4.2 IP 头和 TCP 头偏移计算
从以太网帧走到 TCP 端口号,要跳过两层头。常规计算如下:
// protocol.h 中解析 IP 头后的端口获取逻辑(示意) EtherHdr* pEth = (EtherHdr*)pPacketData; if (ntohs(pEth->type) != 0x0800) return; IPHdr* pIpHdr = (IPHdr*)(pPacketData + sizeof(EtherHdr)); int ipHeaderLen = (pIpHdr->ver_ihl & 0x0F) * 4; if (pIpHdr->proto == IPPROTO_TCP) { TCPHdr* pTcp = (TCPHdr*)((BYTE*)pIpHdr + ipHeaderLen); int srcPort = ntohs(pTcp->src_port); int dstPort = ntohs(pTcp->dst_port); }这里的关键点是ver_ihl拆开来的低四位IHL是以 4 字节为单位的,所以乘 4 才是真正的 IP 头长度。不能直接拿sizeof(IPHdr)跳转,因为有 Options 存在时头部会变长。protocol.h用了一个联合体或者位域来拆这个字节,确保能正确拿到长度。
4.3 ipaddr.cpp 的地址格式化和掩码计算
ipaddr.cpp负责把二进制 IP 转成点分十进制的字符串。这段逻辑现在看起来平淡无奇,但它把 4 字节直接转为CString,没有使用inet_ntoa,因为后者会依赖运行时库的静态缓冲,多线程调用时会互相覆盖。这个源码里给出的方案是:
// ipaddr.cpp 自定义 IP 转字符串 CString IPAddrToString(DWORD ipAddr) { BYTE* p = (BYTE*)&ipAddr; CString str; str.Format(_T("%d.%d.%d.%d"), p[0], p[1], p[2], p[3]); return str; }虽然这段实现完全绕过了字节序,自然匹配主机序的存储布局,但它比inet_ntoa更可控。
ipaddr.h里还有一些网络掩码计算的函数。用于判断eth->src是否属于同一子网,直接在应用层做小范围的主机识别。它没有调GetAdaptersInfo,而是在程序初始化时指定本机 IP 和子网掩码,然后硬算。这个方法在网段切换时会有 bug,因为不会自动更新。当代改造时可以直接替换为GetAdaptersAddresses动态获取。
4.4 应用层过滤规则与解析的协作
FileterDlg设置的条件在这里被读取。最终列表视图GetPacketListView.cpp在插入每一行时,会调用DecodeAndFilter,它返回布尔值决定是否加入显示列表。这样做的好处是保持原始数据队列不丢包,同时界面能调整显示过滤条件而不用重新抓包。
这个设计在工程上很实用:抓包线程只负责把数据存下来,过滤是显示时做的。缺点是长时间运行内存会持续增长。真实商用工具会做环形缓冲,只保留最近 N 包。在改造这个源码时,可以把文档类的CPtrList改成容量有限的std::deque,当超过 10 万包时从队头弹出,避免内存溢出。
5. 抓包效率、丢包与兼容性:老源码的现代改造
5.1 Packet32 的缓冲机制与丢包边界
Packet32.c里最值得看的是内核缓冲区的实现。代码清单中出现了PACKON.H和PACKOFF.H,这两个头配合zpacket.vxd使用。这个驱动把收到的包存放在内核环形缓冲,用户态调用PacketReceive时返回缓冲中的数据包个数。
丢包的根源不只是网卡处理不过来的问题——用户态线程读取不够快时,环形缓冲被覆盖才是最直接的丢包原因。改造思路有两条,一是调大缓冲区:
// 设置 8MB 内核缓冲 PacketSetBuff(m_lpAdapter, 8 * 1024 * 1024);二是把Sleep(1)改成WaitForSingleObject等待信号量,等PacketReceive返回时才处理,而不是空转循环轮询。
5.2 Release 目录里的 exe 和 DLL 的兼容性
资源包的Release目录里放着编译好的GetPacket.exe、zpacket.vxd和Packet32.dll。这个组合只能在 32 位 Windows 98/2000/XP 上运行。在 64 位 Windows 10/11 上会加载失败,因为 VxD 驱动没有签名且架构不支持。建议的验证方法是开一台 Windows XP 虚拟机,把zpacket.vxd放到系统目录并手工启动 VxD,再运行 exe。现代 Windows 需要使用 WinPcap/Npcap 替代底层驱动,但保留源码里的协议解析逻辑和 MFC 界面结构。
# 在 Windows XP 虚拟机中手动注册 VxD(示例) copy zpacket.vxd C:\WINDOWS\SYSTEM32\ copy Packet32.dll C:\WINDOWS\SYSTEM32\ regsvr32 Packet32.dllzpacket.vxd不是 COM 组件,regsvr32通常无效。正确方式是通过DeviceIoControl动态加载,或者由Packet32.dll在PacketOpenAdapter内部自动CreateFile打开\\.\ZPACKET设备。因此在 XP 上只要 DLL 路径正确即可,不需要额外注册。芯片组集成网卡不一定能被老驱动识别,这时需要改用 Npcap 的兼容模式。
5.3 验证抓包结果与调试技巧
运行抓包程序后,用浏览器访问一个 HTTP 网站,列表里出现源 MAC、目的 MAC、TCP 端口号和长度,说明抓包链路通了。如果列表一直为空,先从网卡选择对话框确认选中的不是虚拟网卡,再确认是否有其他软件抢占了网卡独占模式。zc里最常见的问题是编译器默认字节对齐导致EtherHdr大小为 14 变成了 16,帧头偏移错位,解析出的 IP 头完全是乱的。此时把protocol.h中所有结构体用#pragma pack(1)包住即可。
另一种验证方式是构造已知数据帧。用本机自己发一个 UDP 包到广播地址,看抓到的源 MAC 是不是本机网卡。有些驱动收不到本机发送的数据,这是正常的,因为网卡如果启用了 TX offload 和 receive filtering,部分回环流量不经过抓包点。可以加一个PacketSetHwFilter的参数把过滤器改为NDIS_PACKET_TYPE_ALL_LOCAL,把本机接收的所有流量都算进来,再看有没有输出。
本文还有配套的精品资源,点击获取