多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

VC++网卡抓包实战:基于Npcap的Sniff源码解析与避坑指南

VC++网卡抓包实战:基于Npcap的Sniff源码解析与避坑指南 简介这是一份面向网络编程初学者与安全方向开发者的 VC 网络抓包程序源代码基于 WinPcap 的 Packet32 驱动接口实现网卡数据包嗅探与截取可用于学习底层协议解析、网卡混杂模式设置及数据包过滤等核心技能。压缩包共 44 个文件以 16 个 .h 头文件与 8 个 .cpp 源文件为主体涵盖协议定义、IP 地址处理、过滤对话框与主框架等模块另含 dsw/dsp 工程文件、vxd 虚拟驱动、dll 动态库、ico/bmp 图标位图资源及 readme 说明整体约 78KB结构紧凑便于阅读与二次开发。目前已有 780 人学习下载。读者可从中获得一套可直接编译运行的抓包工程理解 Packet32 驱动调用、DEVIOCTL 设备控制与 NTDDNDIS 网卡接口的配合方式并借助过滤对话框实现按条件筛选数据包是研究 Windows 平台网络嗅探原理的实用参考。1. 从一份 vc sniff 抓包源码说起网卡数据包到底怎么被截下来的很多人第一次拿到「vc sniff 截取网卡数据包」这类源代码压缩包时心态是矛盾的一边觉得抓包这事 Wireshark 已经做得够好了一边又想知道它底层到底怎么把流经网卡的帧抠出来。我当年也是这么入坑的拆开一份典型的 vc 抓包源码后发现核心不过百来行——打开网卡、设混杂模式、收原始帧、解析协议头。真正难的不是代码而是权限、驱动、缓冲区这些看不见的边界。这篇笔记就顺着这份源码的思路把 vc 下基于 WinPcap/Npcap 的网卡抓包从环境、编码、参数到排错讲透适合想自己写一个轻量抓包工具、或者要把抓包能力嵌进现有 vc 工程的开发者。读完你能独立跑通一个最小可用的抓包程序也知道哪些参数一改就翻车。2. vc 抓包的技术底座为什么绕不开 Npcap 这层2.1 从网卡到用户态抓包程序站在哪一层要理解 vc sniff 的源码结构先得搞清楚数据包从网线到你的recv回调中间隔了几层。网卡收到帧后正常路径是驱动交给协议栈协议栈再分发给 socket。但抓包要的是「所有帧」包括不是发给本机的、协议栈准备丢弃的所以必须有一个东西在协议栈之前把帧复制一份出来。Windows 上干这活的就是 NpcapWinPcap 的继任者它本质是一个 NDIS 协议驱动加一个用户态 DLL。你的 vc 程序链接的是wpcap.lib运行时加载wpcap.dll通过它向内核驱动下命令打开某块网卡、设置过滤规则、把捕获的帧通过共享缓冲区送回用户态。源码里那些pcap_open_live、pcap_loop调用全是在跟这层驱动对话。理解这一点很关键——你的程序本身没有「截取」能力它只是驱动的一个客户端。所以权限、驱动版本、网卡类型这些问题都会直接反映到你的抓包结果上。2.2 选 WinPcap 还是 Npcap一份源码的兼容性判断老源码里常见#include pcap.h加wpcap.lib这是 WinPcap 时代的写法。WinPcap 已经停止维护在 Win10 之后的系统上装不上或者装上了也不稳定。现在正确的做法是换 Npcap它提供兼容模式头文件和库名基本不变但安装时要勾选「WinPcap API-compatible Mode」否则你编译出来的 exe 在别人机器上会因为找不到wpcap.dll直接崩。判断一份源码该用哪个看三点头文件包含的是pcap.h还是pcap/pcap.h链接库写的是wpcap.lib还是Packet.lib有没有用到pcap_setmode这类 Npcap 扩展。多数教学源码停留在 WinPcap 写法直接拿来编译能过但部署时要装 Npcap 并开兼容模式。我一般会建议把工程里的库依赖统一改成 Npcap SDK 里的版本避免新旧 DLL 混用导致的玄学崩溃。2.3 环境搭建VS2017 下把 Npcap SDK 接进 vc 工程热词里「vs2017 vc 安装」「vc 运行库」出现频率很高说明不少人是卡在环境这一步。下面是我在 VS2017 上跑通一份 sniff 源码的标准流程VS2019/2022 同理。第一步装 Npcap 运行库安装时务必勾选 WinPcap 兼容模式。第二步下载 Npcap SDK解压后得到Include和Lib两个目录。第三步在 VS 工程里配置# 工程属性 - C/C - 常规 - 附加包含目录 你的路径\npcap-sdk\Include # 工程属性 - 链接器 - 常规 - 附加库目录 你的路径\npcap-sdk\Lib\x64 # 工程属性 - 链接器 - 输入 - 附加依赖项 wpcap.lib ws2_32.lib这里ws2_32.lib容易被漏掉因为源码里解析 IP 头时会用到inet_ntoa之类的 Winsock 函数不链接就报一堆未解析外部符号。平台要选 x64因为现在 Npcap 默认装的是 64 位驱动你编个 32 位 exe 去调 64 位 DLL 会直接失败。配置完先别急着跑把源码里写死的网卡名换成动态枚举否则换台机器就抓不到东西。3. 把源码跑起来一个最小可用的 vc 抓包程序3.1 枚举网卡并打开源码里最该先看懂的一段几乎所有 sniff 源码开头都有一段「列出所有网卡让用户选」的逻辑这段是理解整个程序结构的钥匙。核心 API 是pcap_findalldevs它返回一个链表每个节点描述一块网卡。下面是我从源码里抽出来、又补了错误处理的版本#include pcap.h #include stdio.h int main() { pcap_if_t *alldevs; pcap_if_t *d; char errbuf[PCAP_ERRBUF_SIZE]; int i 0; // 枚举本机所有可抓包的网卡 if (pcap_findalldevs(alldevs, errbuf) -1) { fprintf(stderr, 枚举网卡失败: %s\n, errbuf); return 1; } for (d alldevs; d ! NULL; d d-next) { // 打印序号、网卡名和描述方便用户选择 printf(%d. %s, i, d-name); if (d-description) printf( (%s)\n, d-description); else printf( (无描述)\n); } if (i 0) { printf(没有找到可用网卡检查 Npcap 是否安装\n); return 1; } pcap_freealldevs(alldevs); return 0; }逻辑说明pcap_findalldevs把网卡信息填进alldevs链表d-name是形如\Device\NPF_{GUID}的设备名后面pcap_open_live就靠它定位网卡。参数说明errbuf是错误缓冲区大小固定PCAP_ERRBUF_SIZE任何 pcap 调用失败都会往里写原因排错时第一时间看它。这段跑通说明环境和驱动都正常接下来才是真正的抓包。3.2 打开网卡、设混杂模式、抓第一个包拿到设备名后用pcap_open_live打开它。这一步的参数直接决定你能抓到什么是整份源码里最值得抠的地方pcap_t *adhandle; int snaplen 65536; // 捕获长度够装下整个帧 int promisc 1; // 混杂模式抓所有经过网卡的帧 int timeout 1000; // 毫秒读超时 adhandle pcap_open_live(d-name, snaplen, promisc, timeout, errbuf); if (adhandle NULL) { fprintf(stderr, 打开网卡失败: %s\n, errbuf); pcap_freealldevs(alldevs); return 1; }snaplen设小了会截断帧解析 IP 头没问题但要看完整 payload 就得给足65536 是常用上限。promisc置 1 是抓包程序的灵魂它让网卡接收所有帧而不只是发给自己的但注意在交换网络里你只能看到本机所在广播域和本机相关的流量别指望抓到整个局域网——这是新手最常见的误解。timeout影响pcap_next_ex的返回节奏设太小 CPU 空转设太大实时性差1000 毫秒是个平衡点。打开成功后用pcap_loop或pcap_next_ex收包。pcap_loop是回调式源码里常见void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { // header-len 是帧原始长度header-caplen 是实际捕获长度 printf(抓到一帧长度 %d 字节\n, header-len); } // 主循环里调用cnt0 表示无限抓 pcap_loop(adhandle, 0, packet_handler, NULL);header-len和header-caplen的区别要记牢前者是帧在网线上的真实长度后者是实际存进缓冲区的长度。如果caplen len说明snaplen设小了帧被截断解析高层协议会出错。3.3 加一个 BPF 过滤别让无关流量淹没你裸抓包会把 ARP、广播、各种后台流量全收进来真正有用的没几条。源码里通常会用pcap_compile加pcap_setfilter挂一个 BPF 过滤表达式这是抓包程序从「能用」到「好用」的分水岭struct bpf_program fcode; char filter[] tcp port 80 or tcp port 443; // 把过滤表达式编译成内核能执行的 BPF 字节码 if (pcap_compile(adhandle, fcode, filter, 1, 0xffffff) 0) { fprintf(stderr, 过滤表达式编译失败\n); return 1; } // 下发到驱动之后只有匹配的帧才会送到用户态 if (pcap_setfilter(adhandle, fcode) 0) { fprintf(stderr, 设置过滤器失败\n); return 1; } pcap_freecode(fcode);参数说明filter是标准 BPF 语法tcp port 80、host 192.168.1.1、udp and port 53都能写组合起来很灵活。pcap_compile的第四个参数optimize置 1 会优化字节码第五个netmask一般填0xffffff。过滤在驱动层执行比你在用户态if判断高效得多流量大时差别非常明显。我一般会先不加过滤跑一遍看流量构成再根据目标写表达式避免过滤写错导致「什么都抓不到」还以为程序坏了。4. 解析数据包从以太网头一路剥到 TCP payload4.1 以太网头、IP 头、TCP 头的偏移关系抓到帧只是拿到一串字节要看出内容得按协议栈逐层解析。以太网头固定 14 字节6 字节目的 MAC 6 字节源 MAC 2 字节类型类型字段0x0800表示上层是 IP。IP 头长度不固定靠首部长度字段算通常是 20 字节。TCP 头也是变长的靠数据偏移字段算。下面这段是源码里解析逻辑的典型写法// 以太网头 typedef struct eth_header { u_char dst_mac[6]; u_char src_mac[6]; u_short type; } eth_header; // 解析入口 void parse_packet(const u_char *pkt_data, u_int caplen) { eth_header *eth (eth_header *)pkt_data; if (ntohs(eth-type) ! 0x0800) return; // 只处理 IPv4 // IP 头从以太网头之后开始 u_char *ip_start (u_char *)(pkt_data 14); u_char ip_header_len (ip_start[0] 0x0F) * 4; // 首部长度以 4 字节为单位 // TCP 头从 IP 头之后开始 u_char *tcp_start ip_start ip_header_len; u_char tcp_header_len ((tcp_start[12] 0xF0) 4) * 4; // payload 起点 u_char *payload tcp_start tcp_header_len; printf(payload 长度约 %d 字节\n, (int)(pkt_data caplen - payload)); }逻辑说明ntohs做网络字节序到主机字节序的转换Windows 上大小端和网络序相反不做转换读出来的类型值是错的。IP 首部长度字段只占低 4 位所以要 0x0F再乘 4。TCP 数据偏移占高 4 位要右移 4 位再乘 4。参数说明caplen是实际捕获长度算 payload 长度时要用它而不是header-len否则截断的帧会算出负数。4.2 用结构体对齐的坑为什么你的解析结果总是错位vc 默认按成员大小对齐结构体你定义一个包含u_char和u_short的结构体编译器可能插入填充字节导致sizeof和你预期的不一样指针偏移全乱。这是血泪经验里排前三的坑。解决办法有两个一是解析时别用结构体直接按字节偏移取值像上面那样二是非要用结构体就加#pragma pack(1)关掉对齐#pragma pack(push, 1) typedef struct ip_header { u_char ver_ihl; u_char tos; u_short total_len; u_short id; u_short flags_fo; u_char ttl; u_char proto; u_short checksum; u_char src_ip[4]; u_char dst_ip[4]; } ip_header; #pragma pack(pop)#pragma pack(push, 1)让结构体按 1 字节对齐pop恢复默认。这样sizeof(ip_header)才是 20。我一般建议新手先用字节偏移写解析等完全理解布局了再用结构体能少踩很多「明明代码没错但结果就是不对」的坑。4.3 把抓到的包存成 pcap 文件方便事后用 Wireshark 复盘自己写的解析器再全也不如 Wireshark 好用所以抓包程序通常会把原始帧存成 pcap 格式事后用 Wireshark 打开分析。pcap_dump系列 API 就是干这个的pcap_dumper_t *dumper pcap_dump_open(adhandle, capture.pcap); if (dumper NULL) { fprintf(stderr, 创建 pcap 文件失败\n); return 1; } // 在回调里把每一帧写进去 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { pcap_dump((u_char *)param, header, pkt_data); } // 主循环里把 dumper 作为 param 传进去 pcap_loop(adhandle, 0, packet_handler, (u_char *)dumper);参数说明pcap_dump_open的第二个参数是文件名后缀.pcap是 Wireshark 认的格式。pcap_dump的param就是回调透传进来的 dumper 指针。抓完记得pcap_dump_close(dumper)否则缓冲区里的帧可能没落盘。这个功能在排查偶发问题时特别有用现场抓一段存下来回去慢慢看比盯着实时输出强得多。5. 避坑与排查vc 抓包最常见的五个翻车现场5.1 现象程序能编译运行报「找不到 wpcap.dll」原因只装了 Npcap SDK 没装运行库或者装了但没勾 WinPcap 兼容模式系统里没有wpcap.dll。解决去 Npcap 官网装运行库安装向导里明确勾选兼容模式装完重启。如果还不行检查 exe 是 32 位还是 64 位和 DLL 位数必须一致。5.2 现象枚举网卡返回空列表或者只有一块「回环」原因没用管理员权限运行。抓包要访问驱动普通用户权限不够pcap_findalldevs会静默返回空。解决右键 exe 以管理员身份运行或者在工程属性里把清单文件设成requireAdministrator。这个坑新手几乎必踩因为程序不报错只是「什么都抓不到」。5.3 现象能抓到包但全是本机发出的收不到别人的原因对混杂模式的理解有偏差。在交换网络里交换机只把帧转发到目的端口你的网卡即使开了混杂模式也收不到不属于本机端口的流量。解决认清边界——单机抓包只能看到本机相关流量和广播。要抓整个网段的流量得用端口镜像或者集线器这不是软件能解决的。5.4 现象抓到的帧长度对但解析出来全是乱码原因结构体对齐问题或者忘了做字节序转换。ntohs/ntohl漏掉一个读出来的端口号、长度字段就是反的。解决解析前先确认结构体加了#pragma pack(1)所有多字节字段读出来都过一遍ntohs或ntohl。用 Wireshark 打开同一份 pcap 对比能快速定位是哪一层错了。5.5 现象跑一段时间内存暴涨或者丢包严重原因回调里做了耗时操作比如每抓一帧就printf或者写文件同步刷盘导致驱动缓冲区溢出丢包或者pcap_loop的cnt参数设错无限循环里又不断分配内存。解决回调里只做最轻量的处理把帧丢进队列由另一个线程慢慢解析写文件用pcap_dump而不是自己fwrite它内部有缓冲。丢包严重时先调大驱动的缓冲区Npcap 支持pcap_setbuff。6. 进阶把抓包能力嵌进现有 vc 工程的两个实用技巧第一个技巧是异步抓包。pcap_loop是阻塞的直接放在主线程会让界面卡死。我一般开一个工作线程专门跑pcap_loop回调里把帧拷进一个线程安全队列主线程定时取出来解析和显示。队列要设上限满了就丢最旧的避免内存无限增长。线程退出时用pcap_breakloop打断pcap_loop再pcap_close顺序反了会崩。第二个技巧是动态改过滤规则。运行中想换过滤条件不能直接pcap_setfilter得先pcap_breakloop停掉循环重新pcap_compile和pcap_setfilter再重启pcap_loop。下面是我常用的封装void restart_capture(pcap_t *adhandle, const char *new_filter) { struct bpf_program fcode; pcap_breakloop(adhandle); // 先停 if (pcap_compile(adhandle, fcode, new_filter, 1, 0xffffff) 0) { fprintf(stderr, 新过滤规则编译失败保持原规则\n); return; } pcap_setfilter(adhandle, fcode); pcap_freecode(fcode); // 重启循环回调不变 pcap_loop(adhandle, 0, packet_handler, NULL); }参数说明new_filter是新的 BPF 表达式编译失败时不要setfilter否则会把原来的规则也弄丢。这个封装在需要按用户输入实时切换抓包目标的工具里很实用。验证抓包程序是否正确我有个笨但有效的习惯同时开 Wireshark 抓同一块网卡跑一段相同操作对比两边抓到的帧数和协议分布。数量级对得上、关键帧都在基本就没问题。差异大就回头查snaplen、过滤规则和混杂模式。这套对比法帮我定位过好几次「以为程序对了其实漏了一半包」的问题。写抓包程序最忌讳想当然多拿现成工具做参照比盯着自己的代码猜要快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表