ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VC++6.0网络抓包源码解析:从Packet32.dll到协议解析

VC++6.0网络抓包源码解析:从Packet32.dll到协议解析 简介这是一份基于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 控制码后者提供了 NDISNetwork 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; // 上层协议类型 0x0800IP } 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把本机接收的所有流量都算进来再看有没有输出。本文还有配套的精品资源点击获取
返回列表