简介这是一份面向网络编程初学者与安全技术爱好者的 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:ConfigurationRelease /p:PlatformWin32 msbuild GetPacket.dsp /p:ConfigurationRelease /p:PlatformWin32逻辑说明先编Packet32再编主工程顺序不能反。/p:ConfigurationRelease指定发布配置调试阶段可以换成Debug方便断点。参数PlatformWin32表示 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就是 ARP0x0800就是 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判断是不是 TCPipHdrLen用 IP 头第一个字节的低 4 位乘以 4 得到实际头长因为 IP 头可能有选项字段不能死写 20 字节。参数rule是从FileterDlg收集来的过滤条件。参数说明0x0800是 IPv4 的以太网类型0x0806是 ARP0x86DD是 IPv6。协议号6是 TCP17是 UDP1是 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 的解析对照确认偏移和字节序都对得上再拿去抓真实流量。从那以后我每次动协议解析代码都强制走一遍这个对照流程省了很多事后排查的功夫。希望这份源码和上面的拆解能帮你把抓包这件事真正落到自己手里。本文还有配套的精品资源点击获取