尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VC++网络抓包程序开发:从WinPcap/Npcap到协议解析

VC++网络抓包程序开发:从WinPcap/Npcap到协议解析 简介这是一份基于Visual C开发的控制台与MFC混合框架网络抓包程序面向C/C网络编程学习者和安全入门者用于解决网卡数据包截取与分析入门难的问题。代码封装了Packet32驱动接口实现底层抓包、协议过滤与简单报文解析。资源共44个文件以16个头文件和8个C源文件为核心配合dsp/dsw工程文件、bmp/ico界面资源以及可执行文件GetPacket.exe和Packet32.dll开发者可直接编译运行并对照学习。压缩包仅78KB轻量便于快速部署。已有777人学习下载。通过阅读源码可理解NDIS协议驱动调用、Packet32 API抓包流程、FilterDlg协议过滤对话框实现以及ListView报文列表展示等关键模块适合想要快速上手Sniffer原理与VC网络编程的读者。1. 用 VC 写网络抓包程序为什么值得自己造轮子当 Wireshark 轻轻松松就能抓包时也许你会问标题里这个“vc sniff截取网卡数据包网络抓包程序,源代码.zip”还有什么意义在真实的交付场景里抓包往往不是最终目的产品要按自定义规则过滤流量要把 Ethernet、IP、TCP 头直接映射到结构体还要把抓包模块嵌进没有图形界面的 Windows 服务里。Wireshark 面对这些需求一点忙都帮不上。标题里的项目正是这类自研抓包工具的常见起点用 VC 调用 WinPcap/Npcap 的底层 API枚举网卡、开启混杂模式、循环抓帧、解析协议头并落盘成 pcap。这篇博文就顺着这条路径讲从原理选型到 VC 工程调试每一步都给出可复现的命令和代码。适合有 C 基础、打算动手写网络监控或协议分析工具的工程师5 年以上经验可以直接跳到第 3 章看参数和坑。2. 网卡数据包捕获原理为什么 WinPcap/Npcap 是 VC 抓包程序的根基2.1 对比 SOCK_RAW 和 WinPcap/Npcap看清 sniff 应该站在哪一层很多人一听到“sniff”就想到用 SOCK_RAW 套接字。这个方向没错但在 Windows 上限制很多。Windows 的原始套接字只能接收发往本机的 TCP/UDP 包以及本地发出的 ICMP 包无法接收网络上其他主机的 IPv4 流量即便开启了 SIO_RCVALL也拿不到以太网帧头数据链路层的 ARP、RARP、STP 这类帧完全看不见。这决定了它不适合做一个通用网络抓包程序。WinPcap 和它的继任者 Npcap 则完全不同它们基于 NDIS 驱动在网卡驱动收到帧、还没交给 TCP/IP 协议栈之前就先复制一份给用户态应用。用户态拿到的数据是完整的帧包括前导码以外的所有字节。所以我在 VC 里写抓包程序优先选 Npcap SDK而不是硬啃 Win Socket。下表是我在日常方案选型时常用的对比对比项SOCK_RAWWinPcap / Npcap可捕获层次网络层以上数据链路层到应用层混杂模式支持受驱动和协议栈限制网卡驱动支持时直接生效无线网卡流量很难拿到其他终端帧需要开启 Npcap 的 802.11 监控模式过滤器全靠自己写协议判断内核级 BPF 过滤性能高VC 工程集成只需 ws2_32.lib需要 wpcap.lib Packet.lib从表里能看出WinPcap/Npcap 的“捕获层次”优势是原始套接字无法弥补的。有人会问既然 Npcap 更强为什么还常看到 WinPcap 的旧代码因为 API 完全兼容Npcap 的 SDK 头文件仍然沿用 pcap_ 前缀老代码改一行库名就能跑起来。真正的坑在于环境Npcap 默认不安装 WinPcap 兼容模式需要安装时勾选“Install Npcap in WinPcap API-compatible Mode”否则一些旧程序无法通过 pcap_findalldevs 找到设备。我在调试“网络抓包程序源代码”这类项目时第一件事就是检查 Npcap 服务是否启动命令是sc query npcap输出结果是RUNNING就正常否则需要执行net start npcap。这个检查比反复看代码更有效因为大部分枚举失败问题都出在驱动没起来。2.2 混杂模式与监视模式网络抓包程序必须分清楚的两件事混杂模式promiscuous mode让网卡把链路上经过的帧都收下来而不是只收目标 MAC 是本机的。pcap_open_live的 promisc 参数置 1就开启了混杂模式。但在交换机环境下你的网卡能收到的仅限本机收发和广播帧要抓其他主机流量需要在交换机上做端口镜像这不是抓包程序能解决的。在 Npcap 上如果网卡是无线型号混杂模式不一定能捕获邻居终端的流量因为 802.11 抓包需要的是“监听模式”monitor mode。pcap_set_rfmon可以尝试开启监听模式但前提是网卡驱动和无线模块支持。这个细节在 Wireshark 文档里也不明确5 年以上工程师也常踩。所以这里用网络抓包软件最常见的组合有线网卡 混杂模式最可靠。另一个跟混杂模式容易混淆的是 Npcap 的“回环流量支持”。默认 Npcap 抓不到本机发往本机的 loopback TCP 包必须在安装时勾选 Support Loopback Traffic。很多 VC 抓包程序在本地测试 HTTP 客户端时什么都抓不到就是这个原因。验证方式很简单netstat -an | findstr 8080如果看到127.0.0.1:8080是本机进程监听但抓包程序看不到回环包先检查 Npcap 安装选项而不是怀疑代码。2.3 在 VC 工程里接入 Npcap SDK三个注意点一次配好先明确 SDK 的目录。我一般把 Npcap SDK 解压到C:\Npcap\sdk然后配置 VC 的包含目录和库目录。如果是 VC6需要注意版本兼容性。VC6 的编译器对比较新的 Windows SDK 头文件支持不佳包含目录里不能混入新头文件否则会报Cannot open include file: winapifamily.h。解决办法是安装 Windows Server 2003 R2 SDK并把它的 include 目录放在 VC6 自带目录之后。在 VC6 的 IDE 里配置步骤如下打开Project - Settings - C/C在 Preprocessor 的 Additional include directories 填C:\Npcap\sdk\inc。切到Link页Input 的 Additional library path 填C:\Npcap\sdk\libObject/Library modules 里加wpcap.lib Packet.lib ws2_32.lib。在 C/C 的 Preprocessor definitions 里加上HAVE_REMOTE否则用不了远程抓包相关 API。如果习惯用命令行编译等价命令是cl /nologo /O2 /I C:\Npcap\sdk\inc sniff.cpp /link /LIBPATH:C:\Npcap\sdk\lib wpcap.lib Packet.lib ws2_32.lib /OUT:sniff.exe这条命令要求先运行vcvars32.bat设置 VC6 环境变量通常位于C:\Program Files\Microsoft Visual Studio\VC98\Bin下。命令里的/O2是速度优化抓包循环里不要开/O1体积优先或/Od禁用优化否则数据包处理热路径的性能差距明显。关于“vc 6 运行效率”这个问题我还想多说一句。VC6 的运行时库对 Windows 10/11 的兼容性一般但编译出来的抓包 exe 未必慢。真正的性能瓶颈出现在用户态缓冲区的拷贝以及频繁的pcap_next_ex调用。解决办法不是换编译器而是调整 Npcap 的内核缓冲区大小和最小拷贝阈值这部分在第 5 章展开。3. 用 VC 写通一个最小编译的网络抓包程序3.1 枚举网卡并选择设备pcap_findalldevs 的返回码和错误缓冲抓包程序的第一步是拿到可用的网卡列表。pcap_findalldevs返回一个pcap_if_t链表每个节点里最有用的字段是name和description。注意name是驱动层的设备路径格式类似\Device\NPF_{GUID}不要硬编码每一台机器的 GUID 都不同。下面这段代码可以列出所有设备并选择第一个#include pcap.h #include stdio.h #include stdlib.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, Packet.lib) #pragma comment(lib, ws2_32.lib) int main() { pcap_if_t* alldevs NULL; pcap_if_t* dev; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(alldevs, errbuf) -1) { printf(枚举网卡失败: %s\n, errbuf); return 1; } int devCount 0; for (dev alldevs; dev; dev dev-next) { printf(%d: %s %s\n, devCount, dev-name, dev-description ? dev-description : ); } if (devCount 0) { printf(未发现网卡设备请检查 Npcap 驱动是否安装\n); return 1; } dev alldevs; // 默认取第一个设备 pcap_t* handle pcap_open_live( dev-name, 65536, 1, 1000, errbuf ); if (handle NULL) { printf(打开网卡失败: %s\n, errbuf); pcap_freealldevs(alldevs); return 1; } // 抓包循环见 3.3 pcap_close(handle); pcap_freealldevs(alldevs); return 0; }代码逻辑很直接先调用pcap_findalldevs填充链表然后遍历打印最后用pcap_open_live打开第一个设备。这里没有写循环是为了先展示设备枚举的行为。pcap_open_live返回的句柄是后续所有抓包调用的前提。3.2 抓包循环里 pcap_next_ex 的四个返回值别再只判断空指针很多人第一次写抓包循环会用pcap_next这个函数在读取超时时返回 NULL无法区分“超时”和“出错”。pcap_next_ex的返回值更明确下面是完整的判断逻辑1成功读取一个包header和pkt_data有效。0读取超时没有包到达可以刷新界面或继续循环。-1发生错误用pcap_geterr(handle)获取具体原因。-2正本数据包读取完毕这种情况只在离线读取 pcap 文件时出现实时抓包不会遇到。这个状态机不搞清楚程序会在高流量下频繁误判。比如很多示例代码把返回值小于 0 当作错误退出但在某些旧版本驱动上超时可能返回负值导致程序意外终止。所以正确的循环结构应该是沿用我上面的分支写法把0和-1分开处理。3.3 完整代码示例从打开网卡到打印帧长度把上面两部分拼起来就是一个能跑的最小编译程序。它的循环体非常短核心代码可以这样写while (1) { struct pcap_pkthdr* header; const u_char* pkt_data; int res pcap_next_ex(handle, header, pkt_data); if (res 1) { printf(帧长度: %u, 捕获长度: %u, 时间戳: %u.%06u\n, (unsigned)header-len, (unsigned)header-caplen, (unsigned)header-ts.tv_sec, (unsigned)header-ts.tv_usec); // 在这里调用后续的协议解析函数 } else if (res 0) { // 超时什么都不做 continue; } else if (res -1) { printf(抓包错误: %s\n, pcap_geterr(handle)); break; } else { break; } }这段循环用了pcap_next_ex的拉模型每调用一次拿一个包。如果你的抓包程序是单线程、需要控制退出时机这种写法最直观。header-len是原始帧长度header-caplen是实际捕获到的字节数如果 snaplen 配置得比帧长小caplen会小于len解析时只能用caplen防止越界。3.4 编译、链接和运行参数表解决误配置带来的运行效率问题编译这个最小程序时最容易踩的坑是把snaplen设成 1514。这个值只够标准以太网帧但遇到带有 802.1Q VLAN 标签、甚至 Jumbo Frame 的流量时帧头后面的数据会被截断。我统一设置成 65536和 libpcap 官方建议一致不会损失任何字段。下面的参数表可以贴在代码旁边方便调参参数类型说明推荐值dev-namechar*来自pcap_findalldevs的设备名不要硬编码每台机器不同snaplenint单帧最大捕获字节数65536promiscint是否开启混杂模式1 表示开启read_timeoutint读取超时毫秒数1000 适合交互0 表示无限等待errbufchar*错误信息缓冲长度用PCAP_ERRBUF_SIZE关于运行效率这里有个常见误解。把 read_timeout 设成 0 在某些程序里会提升响应速度但在pcap_next_ex的循环里0 会让驱动一直等待下一个包CPU 占用率反而下降但实时性变差。如果需要低延迟抓包建议设一个 100 到 200 毫秒的阈值既不会空转也不会让 UI 卡顿。4. 解析以太网帧和 IP/TCP 头并写入 pcap 文件4.1 以太网帧头的强制转换与字节序问题抓包拿到的pkt_data是一段原始内存最常见的方法是直接强制转换成结构体指针。以太网帧头是固定的 14 字节目标 MAC 6 字节、源 MAC 6 字节、EtherType 2 字节。问题出在内存对齐VC6 默认按 8 字节对齐结构体直接定义会多出填充字节必须用#pragma pack(push, 1)取消对齐。#pragma pack(push, 1) struct EthernetHeader { u_char destMac[6]; u_char srcMac[6]; u_short etherType; }; #pragma pack(pop)然后是字节序。EtherType 字段在大端序的网络字节序下存储在 x86 机器上必须用ntohs转换否则0x0800会读成0x0008协议判断全部失败。下面这段代码可以区分最常见的 ARP 和 IPv4const EthernetHeader* eth (const EthernetHeader*)pkt_data; unsigned short etherType ntohs(eth-etherType); if (etherType 0x0806) { printf(ARP 帧\n); } else if (etherType 0x0800) { printf(IPv4 帧\n); }这里有个容易忽略的点不是所有以太网帧都带 802.1Q VLAN 标签。如果交换网络里启用了 VLANEtherType 需要向前偏移 4 字节也就是先判断0x8100再读取真正的类型。实战中最好写一个跳过 VLAN 标签的辅助函数否则在 trunk 端口上抓不到协议。4.2 IP 头长度和协议字段的判定注意捕获长度和总长度的差别解析 IPv4 头时字段特别容易算错。IP 首部长度是verIhl字节的低 4 位单位是 4 字节所以真正的偏移是(verIhl 0x0F) * 4。总长度则是整个 IP 包的长度包括头和载荷需要ntohs。还有一个边界条件header-caplen可能小于totalLen因为一个包可能被分片或者被 snaplen 截断。解析时始终用caplen判断可读范围不能用totalLen。这里定义一个紧凑的 IP 头结构体#pragma pack(push, 1) struct IpHeader { u_char verIhl; u_char tos; u_short totalLen; u_short id; u_short fragOffset; u_char ttl; u_char protocol; u_short checksum; u_int srcAddr; u_int dstAddr; }; #pragma pack(pop)解析函数可以这样写void parse_ipv4(const u_char* data, int caplen) { if (caplen (int)(sizeof(EthernetHeader) sizeof(IpHeader))) { return; } const IpHeader* ip (const IpHeader*)(data sizeof(EthernetHeader)); int ihl (ip-verIhl 0x0F) * 4; if (caplen (int)(sizeof(EthernetHeader) ihl)) { return; } unsigned long src ntohl(ip-srcAddr); unsigned long dst ntohl(ip-dstAddr); printf(src%lu.%lu.%lu.%lu dst%lu.%lu.%lu.%lu protocol%d\n, (src 24) 0xFF, (src 16) 0xFF, (src 8) 0xFF, src 0xFF, (dst 24) 0xFF, (dst 16) 0xFF, (dst 8) 0xFF, dst 0xFF, ip-protocol); }注意ntohl的返回值是 32 位无符号整数打印前按字节移位是为了避免直接使用inet_ntoa带来的静态缓冲区覆盖问题。protocol是上层协议编号6 是 TCP17 是 UDP1 是 ICMP。如果你只关心 TCP 端口需要继续从data sizeof(EthernetHeader) ihl位置读取 TCP 头但 TCP 头长度不固定要再读数据偏移字段才能算准源端口和目的端口。4.3 设置 BPF 过滤器和保存抓包结果抓包程序跑起来后流量往往是巨量的把所有包都解析一遍既不现实也没必要。用 BPF 过滤器在内核态先做筛选是最好的做法。pcap_compile负责把字符串表达式编译成驱动认识的指令pcap_setfilter再把它绑定到 handle 上。struct bpf_program fcode; if (pcap_compile(handle, fcode, tcp port 80, 1, PCAP_NETMASK_UNKNOWN) 0) { printf(过滤器编译失败: %s\n, pcap_geterr(handle)); return; } if (pcap_setfilter(handle, fcode) 0) { printf(过滤器设置失败: %s\n, pcap_geterr(handle)); return; } pcap_freecode(fcode);这里pcap_compile的 optimize 参数传 1表示让编译器优化过滤指令。最后一个参数 netmask 在实时抓包时通常用PCAP_NETMASK_UNKNOWN只有离线文件才需要显式传掩码。常用表达式可以看下表BPF 表达式含义tcp port 80HTTP 数据的 TCP 流量host 192.168.1.1只抓来自或发往该主机的包udp and port 53DNS 查询和响应not arp排除 ARP 请求/应答保存抓包结果用 pcap 库自带的 dump 接口。先pcap_dump_open创建一个 pcap 文件然后在原来打印的循环里替换成pcap_dumppcap_dumper_t* dumper pcap_dump_open(handle, capture.pcap); if (dumper NULL) { printf(无法创建 pcap 文件: %s\n, pcap_geterr(handle)); return; } // 抓包循环内替代 printf pcap_dump((u_char*)dumper, header, pkt_data); // 循环结束后 pcap_dump_close(dumper);pcap_dump会同时写入 pcap 全局头、数据包头和完整帧数据生成的文件可以直接用 Wireshark 打开。要注意 dumper 必须在 handle 关闭之前关闭否则缓冲区里的尾部数据会丢失。另外pcap 文件的链路类型取决于 handle 打开时网卡的datalink有线网卡通常是DLT_EN10MB保存前可以用pcap_datalink(handle)确认。5. VC 工程调试、源码保护和验证抓包结果5.1 抓包引擎的进阶参数pcap_create 与 pcap_setmintocopy很多人在pcap_open_live一步到位以后就放弃了调优。实际上pcap_open_live隐藏了内核缓冲区大小等关键设置默认缓冲区容易在大流量下丢包。更好的做法是换成pcap_create加pcap_activate的分步模式pcap_t* handle pcap_create(dev-name, errbuf); pcap_set_snaplen(handle, 65536); pcap_set_promisc(handle, 1); pcap_set_timeout(handle, 1000); pcap_set_buffer_size(handle, 8 * 1024 * 1024); // 内核缓冲区 8MB pcap_activate(handle);如果用的是 Npcap 而不是老 WinPcap还可以多设一个pcap_setmintocopy把内核到用户态的拷贝阈值提高到 4096 字节。这样小包会先在驱动里攒一批再一次性交到用户态减少了系统调用次数吞吐明显提升。代价是单个包到达用户态的延迟变大了。做实时交互工具时不建议开这个参数做后台统计采集器非常值得。5.2 调试 DLL 时当前不会命中断点的几种检查顺序把抓包核心封装成 DLL 以后VC 的调试往往会弹出“当前不会命中断点”。我通常会按下面的顺序排查先看符号文件再查工作目录最后确认有没有附加到错误的进程。如果你的 DLL 是动态加载的断点命不中的最常见原因是调试器尚未加载该 DLL 的符号需要在模块窗口强制加载 PDB。检查项具体操作符号路径VC6 工具菜单下 Options - Debug - Symbols确保 DLL 的 PDB 路径已加入工作目录Project Settings - Debug - Working Directory 设为 exe 实际目录附加进程如果由第三方 EXE 加载 DLL使用 Debug - Attach to Process工程配置确保 Debug 和 Release 都使用 Program Database (/Zi)且关闭“Edit and Continue”还有一个容易被忽略的点Release 版 DLL 做了函数内联和代码重排断点在某个源文件行上可能不会命中。遇到这种情况需要把该函数的#pragma optimize(, off)临时加上调完再去掉。这比反复重建工程快得多。5.3 用 Wireshark 和 tshark 对照验证抓包程序抓包程序写完以后不能用“好像抓到了包”当验收标准。我一般会同时开 Wireshark 监听同一网卡再用自己的程序保存一份 pcap最后用tshark对比两个文件的包序列。tshark是 Wireshark 自带的命令行工具很适合做这种不带界面的校验tshark -r capture.pcap -T fields -e frame.len -e ip.src -e ip.dst | head -20这条命令读取capture.pcap只输出帧长度、源 IP 和目的 IP 这 3 个字段。你可以用粘贴板复制前 20 行再和 Wireshark 上同一时段的包做比对。长度完全一致并不代表解析正确但至少能证明抓包循环和落盘逻辑没有丢帧。要验证 IP 头解析可以在程序里把源 IP 打印出来再对照 tshark 的ip.src字段。一旦发现某个包的源 IP 对不上优先检查字节序和结构体偏移。最后提一个本地测试的配置如果要在本机抓连接127.0.0.1的包Npcap 必须支持 loopback 流量并且设备列表里会多出一个 “Npcap Loopback Adapter”。程序抓这个设备能拿到回环包但以太网头是全 0 或虚拟地址不要在层二解析上纠结。跑通这条链路就说明从网卡枚举到抓包循环的整个路径都是通的。本文还有配套的精品资源点击获取
返回列表