ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MFC+WinPcap/Npcap手写网络嗅探器与协议解析实战

MFC+WinPcap/Npcap手写网络嗅探器与协议解析实战 简介这是一套基于 Visual Studio、MFC 与 WinPcap 编写的网络嗅探器完整工程面向正在学习网络协议分析、抓包编程和 Windows 桌面开发的初、中级读者也可供安全测试与网络排障人员参考。项目提供较友好的图形界面支持 IPv4、IPv6、ARP、ICMP、TCP、UDP、HTTP 等常见协议的捕获与解析适合用来理解从网卡抓包、协议识别到界面展示的完整链路。压缩包为 rar 格式共 22 个文件约 1.92MB包含 7 个头文件、4 个 cpp 源文件以及解决方案、工程配置、图标和界面资源等结构紧凑便于在 VS 中打开并对照学习。已有 5406 人学习或下载。读者可获得可运行的 MFC 工程源码研究 WinPcap 调用、抓包流程、协议字段解析和界面交互实现并在此基础上做二次开发或课程设计。1. 手写嗅探器的真正价值不在抓包那一行手写一个 MFC WinPcap 网络嗅探器sniffer最值钱的地方不是那行抓包调用而是你被迫在字节层面搞懂了以太网帧、IP 头、TCP 握手到底长什么样。我第一次把自制的 sniffer 跑在实验室一台机器的虚拟网卡上抓到 ARP 广播的那一下才彻底明白界面上那些漂亮表格全是人一行行解析出来的没有任何魔法。这篇写给两种人一种是想用 MFC 做带界面的抓包工具、又不想被现成组件糊住的开发者另一种是刚啃完协议教材、需要把课本图和真实字节对上号的学习者。下面按选型、骨架、解析、避坑、收尾五步把真机上踩过的东西摊开讲能直接抄的部分我都标了。所有演示只在自己有权限的本机或实验网段里跑这条底线先立住。2. 选型这一步别省MFC WinPcap 还值不值得当底座2.1 界面层先定死MFC 的适用边界在哪我一般会先问一句这个工具最终交付形态是什么。如果答案是「一个 exe双击就能跑不需要用户装运行时界面就是列表加详情两栏」MFC 依然是个省事的选择。静态链接 CRT 之后单文件几十兆启动快和 Win32 API 贴得近抓包线程、消息循环、窗口句柄这些概念天然对得上。反过来如果你要做树形协议栈、要深浅色主题、要高分屏下 200% 缩放不糊MFC 就得自己补一堆自绘逻辑代码量会追上用 Qt 的收益。新手常误判的一点是以为 MFC 老就等于难维护。实际上 MFC 的坑集中在两类一是字符集二是消息机制。字符集在 VS 工程属性里选「使用 Unicode 字符集」之后CString 和 std::string 之间的转换要显式走 CT2A / CA2T很多人图省事直接强转中文路径和中文协议名立刻变问号。消息机制则是抓包线程不能碰控件必须 PostMessage这一点后面单独讲。熟手看的是另一层MFC 的消息泵和 WinPcap 的回调天然是两个执行上下文pcap_loop 跑在哪个线程、回调里能不能 Sleep、界面重绘会不会把抓包拖慢这些决定了架构。我见过不少 Demo 把 pcap_loop 直接写在按钮响应函数里点一下开始整个窗口就白了这不是 MFC 的问题是把阻塞调用放在 UI 线程的经典翻车。所以界面层我给个判断工具型、单窗口、团队 C 背景、交付要轻选 MFC要跨平台、要复杂控件、要快速迭代 UI选 Qt别硬扛。这个结论不看情怀只看你后续三个月要不要改界面。2.2 抓包层是重头戏WinPcap 和 Npcap 的四个实测差异项目名叫 WinPcap但真要在新机器上跑起来绕不开 Npcap。两者 API 层面高度兼容源码迁移量通常只有工程配置那几行但行为差异会直接决定你的工具能不能用。第一是系统兼容。WinPcap 最后一个版本停在很早期驱动签名和 NDIS 版本都跟不上现在的系统在较新的系统上经常是安装程序跑完了、设备管理器里也有虚拟网卡但 pcap_findalldevs_ex 返回空列表或者打开时直接报错。这不是代码写错是驱动没加载。Npcap 在这一层的支持是一直在维护的新系统上装完就能枚举到设备。第二是回环抓包。WinPcap 抓不到 127.0.0.1 上的流量这对调试本地服务是硬伤。你想验证自己写的客户端和服务端在本机通信时的报文格式结果抓不到包只能两台机器对着测。Npcap 在安装时勾选回环支持就能枚举出一个回环设备抓本机流量。这一条对学习协议的人价值极大因为不用凑两台机器。第三是链路层类型。这个坑最隐蔽不同网卡返回的 pcap_datalink 不一样以太网是 DLT_EN10MB回环设备在部分实现里是别的类型无线网卡也可能有差异。你按以太网固定 14 字节去解析一旦换了设备整个偏移全错抓出来的端口号是乱码。正确做法是打开适配器之后立刻判断链路层类型不是 DLT_EN10MB 就在界面上明确提示「当前设备非以太网链路仅显示原始字节」而不是硬解。第四是分发形态。Npcap 有自己的安装器和 redist 版本如果你的工具要给别人用得考虑对方机器上有没有装驱动、装的是哪个版本、要不要静默安装。WinPcap 时代这个问题反而简单因为大家装的都是同一个老包。这一点不是技术问题是交付问题但会在最后一刻咬你。提示不要在同一台机器上同时装 WinPcap 和 Npcap驱动层会打架表现是枚举到重复设备或者打开句柄失败。切换前先卸干净再重启。2.3 三套组合放一起比一张表看完取舍维度MFC WinPcapMFC NpcapQt Npcap新系统可用性差驱动常装不上好好源码迁移成本基线低改库路径即可中界面全重写回环抓包不支持支持安装时勾选支持交付体积最小小需带驱动安装器大带 Qt 运行库界面扩展性一般需自绘一般强适合人群老项目维护、教学新项目首选要长期迭代的团队表格看完结论很清楚如果你是照着这个项目学原理MFC WinPcap 的代码结构完全够用把库换成 Npcap 照样编译如果你是要交付一个能长期用的工具直接按 MFC Npcap 起步省掉后面一堆兼容性扯皮。我自己的做法是保留 WinPcap 的 API 调用习惯工程里只换链接的库和头文件路径这样教学和实用两头都不耽误。3. 骨架先跑通环境配置、网卡枚举与抓包线程3.1 VS 工程里最容易配错的三处第一处是头文件顺序。WinPcap 的扩展接口比如按字符串枚举设备需要两个宏在包含头文件之前定义漏了就会提示函数未声明新手很容易以为是库没链上跑去反复检查附加依赖项。// stdafx.h / pch.h 中必须写在 #include pcap.h 之前 #define WPCAP // 启用 WinPcap 扩展 API #define HAVE_REMOTE // 启用 pcap_findalldevs_ex 等远程/字符串源接口 #include pcap.h #pragma comment(lib, wpcap.lib) // 抓包主库 #pragma comment(lib, Packet.lib) // 驱动侧辅助库Npcap 下路径不同这段的关键在宏的位置不在 lib。WPCAP 和 HAVE_REMOTE 决定了 pcap.h 里展开哪些声明写反了编译器就找不到符号。Packet.lib 是老版本驱动访问用的换成 Npcap 之后库目录变了需要在项目属性里把附加库目录指向 Npcap 的 SDK 目录否则链接期报 LNK2019。第二处是平台位数。抓包驱动是分位数的x86 的 exe 配 x64 的库编译能过运行直接崩在 pcap_open 上。工程属性里的平台和实际链接的 lib 目录必须一致这一点在从别人那儿拷工程时特别容易错。第三处是字符集。前面提过选 Unicode 之后所有 pcap 返回的 char* 错误信息都要转换才能显示。错误缓冲区 errbuf 是 PCAP_ERRBUF_SIZE 大小别自己拍脑袋定 256。3.2 枚举网卡并打开适配器把失败原因写进日志枚举这步的价值不只是拿设备列表而是把所有失败原因暴露出来。很多人写 Demo 只看函数返回值失败了就一个弹窗「打开失败」然后没下文排查全靠猜。pcap_if_t* alldevs nullptr; char errbuf[PCAP_ERRBUF_SIZE] { 0 }; // 第二个参数是远程认证信息本机抓包传 nullptr if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, nullptr, alldevs, errbuf) -1) { // errbuf 里是具体原因驱动没装、权限不足、服务未启动 CString msg; msg.Format(_T(枚举网卡失败: %s), CA2T(errbuf)); AfxMessageBox(msg); return; } // 遍历时优先取带地址的设备没有地址的通常是驱动虚拟出来的空壳 for (pcap_if_t* d alldevs; d ! nullptr; d d-next) { CString name(d-name); CString desc d-description ? CString(d-description) : _T((无描述)); m_listDev.AddString(desc _T( | ) name); }这段里有两个细节值得说。一是 d-description 可能为空老驱动不填这个字段不判空直接转 CString 会崩。二是设备名d-name才是后面打开用的标识描述只是给人看的别把描述传进 pcap_open。枚举完之后记得在合适的位置 pcap_freealldevs否则反复点「刷新」会泄漏。打开适配器用 pcap_open参数分别是设备名、抓包长度snaplen、混杂模式标志、读超时毫秒、认证信息、错误缓冲。// snaplen 设 65535 表示抓完整帧只想看头部可以设 96能显著降低拷贝开销 // 混杂模式传 1 只对共享网络段有效交换网络和无线网卡上基本抓不到别人的流量 pcap_t* adhandle pcap_open(devName, 65535, PCAP_OPENFLAG_PROMISCUOUS, 1000, nullptr, errbuf); if (adhandle nullptr) { // 常见原因驱动未加载、被安全软件拦截、设备已被其他程序独占 return FALSE; }snaplen 这个参数新手最容易随手写个 65535 然后不管。它在高流量下是真会掉包的因为每个包都要完整拷到用户态。只做协议分析的话 96 字节够看到四五层头抓包效率能差出好几倍。读超时 1000 毫秒的意思是驱动最多攒 1 秒就返回一批这个值影响的是响应延迟不是丢包。3.3 抓包线程与界面线程用环形缓冲加自定义消息解开死锁pcap_loop 是阻塞的放在 UI 线程里点一下就假死。标准做法是开一个工作线程跑循环回调里不碰任何控件把数据投给界面线程。投递方式有两种。简单的是 PostMessage 带上堆上的数据指针界面线程收到后负责 delete好处是解耦彻底坏处是高流量下消息队列会爆界面处理不过来。稳一点的是回调里把数据压进一个固定容量的环形缓冲界面用定时器按 100 毫秒批量取取的时候加个临界区。后者在千兆环境下明显更稳因为消息数量被定时器限流了。// 工作线程入口pParam 传对话框 this 指针 UINT CMySnifferDlg::SniffThread(LPVOID pParam) { CMySnifferDlg* pThis (CMySnifferDlg*)pParam; // cnt 传 -1 表示一直抓到 pcap_breakloop 被调用为止 pcap_loop(pThis-m_adhandle, -1, packet_handler, (u_char*)pThis); return 0; } // 回调运行在抓包线程这里只做解析入队绝不直接操作控件 void packet_handler(u_char* param, const struct pcap_pkthdr* header, const u_char* pkt_data) { CMySnifferDlg* pThis (CMySnifferDlg*)param; if (header-caplen 14) return; // 截断帧直接丢别硬解 pThis-PushToRingBuffer(pkt_data, header); // 内部加锁 // 通知界面有新数据消息只带一个计数不带指针避免生命周期问题 ::PostMessage(pThis-m_hWnd, WM_PACKET_ARRIVED, 0, 0); }这里的 msg 参数传 0界面线程收到后自己去环形缓冲取指针不跨线程谁分配谁释放的问题就不存在了。停止抓包调 pcap_breakloop它会让 pcap_loop 尽快返回然后在线程结束时 pcap_close。顺序反了会崩因为回调还在跑就关了句柄。注意pcap_breakloop 不保证立即返回它只是设置一个标志循环会在下一次超时或收包后退出。所以你在停止时如果马上 pcap_close大概率踩到野指针。等线程 WaitForSingleObject 返回之后再关。4. 解析层怎么写从以太网帧还原出五元组4.1 逐层剥头14 字节起步的偏移算法解析的核心就一件事算出每层的起始偏移和头部长度然后用 ntohs / ntohl 把网络字节序转回主机序。写死偏移是最常见的错误来源因为 IP 有选项时头部不是 20 字节TCP 有选项时也不是 20 字节VLAN 帧还会整体后移 4 字节。void ParsePacket(const u_char* pkt, u_int caplen, u_int wirelen) { if (caplen 14) return; // 以太网头目的 MAC 6 源 MAC 6 类型 2 u_short ethType ntohs(*(const u_short*)(pkt 12)); u_int off 14; // 802.1Q VLAN外层类型 0x8100内层真实类型在 16偏移整体后移 4 if (ethType 0x8100 caplen 18) { ethType ntohs(*(const u_short*)(pkt 16)); off 4; } if (ethType ! 0x0800) return; // 只解 IPv4ARP 和 IPv6 另走分支 if (caplen off 20) return; const u_char* ip pkt off; u_int ihl (ip[0] 0x0F) * 4; // 首部长度以 4 字节为单位最小值 20 if (ihl 20 || caplen off ihl) return; u_short ipTotalLen ntohs(*(const u_short*)(ip 2)); u_char proto ip[9]; // IP 总长减去 IP 头得到载荷长度用它而不是 caplen因为 caplen 可能被 snaplen 截断 u_int payload (ipTotalLen ihl) ? (ipTotalLen - ihl) : 0; if (payload 0) return; if (proto ! 6 || caplen off ihl 20) return; // 只看 TCP且要够最小头 const u_char* tcp ip ihl; u_short sport ntohs(*(const u_short*)(tcp)); u_short dport ntohs(*(const u_short*)(tcp 2)); u_int seq ntohl(*(const u_int*)(tcp 4)); u_char flags tcp[13]; u_int tcpHl (tcp[12] 4) * 4; // TCP 头长度同样以 4 字节为单位 if (tcpHl 20) return; u_int dataLen (payload tcpHl) ? (payload - tcpHl) : 0; // 到这里五元组齐了sport / dport / proto加上 ip 头里的源目地址 }这段最值得盯的是三个地方。一是 caplen 和 wirelen 的区别header 里 caplen 是实际抓到的字节数wirelen 是线上真实长度snaplen 设小了 caplen 会小于 wirelen这时候你按 wirelen 去算载荷长度没问题但绝不能用 wirelen 去访问 pkt 数组越界读就崩了。二是 ihl 和 tcpHl 都可能是 0 或超过实际长度必须校验我见过构造的畸形包直接把 tcpHl 算成 60一读就越界。三是所有多字节字段都要 ntohs / ntohl漏一个端口号就变成 12345 这种莫名其妙的数字。4.2 过滤表达式与内核态过滤把无用流量挡在回调之外抓到全部流量再在回调里 if 判断协议是很浪费的做法。WinPcap 支持 BPF 过滤表达式编译后塞进内核不匹配的包根本不会拷到用户态。这在千兆网卡上差别非常明显。struct bpf_program fcode; // 只关心本机 80 端口相关的 TCP外加所有 ARP 方便观察地址解析 const char* filter tcp port 80 or arp; // 第三个参数 1 表示编译后立即生效第四个参数是子网掩码可为 0 if (pcap_compile(adhandle, fcode, filter, 1, 0) 0) { // 表达式语法错会返回 -1错误信息用 pcap_geterr 取 return FALSE; } if (pcap_setfilter(adhandle, fcode) 0) return FALSE; pcap_freecode(fcode); // 设置成功后立刻释放否则重复设置会泄漏过滤表达式有几个新手常踩的语法坑端口写 port 80 而不是 port 80抓某个网段用 net 192.168.1.0/24方向和逻辑组合要用 and / or / not不是 和 ||。写错了编译期就报错好在 pcap_geterr 会给出具体位置。另一个容易忽略的点是过滤粒度。如果你要做的是会话统计过滤掉 ARP 和 ICMP 会让统计看起来「少了点什么」因为地址解析失败往往就是 ARP 的问题。我一般先不过滤跑一遍看全貌再根据分析目标收窄表达式。4.3 界面呈现列表虚拟化与详情十六进制视图解析结果最终要落在控件上。MFC 的 CListCtrl 在几千行时还行上到几万行就开始拖。解决办法是虚拟列表控件的 LVN_GETDISPINFO 里按需返回某一行的文本数据本身存在自己的 vector 里控件只负责显示可见的那几十行。这么改完之后十万条记录的滚动是流畅的。详情区我一般用两个视图并排上面是协议树文本层级下面是十六进制加 ASCII 的对照。十六进制视图不复杂每行 16 字节左边偏移中间 hex右边可打印字符其余用点号占位。这个视图是排查错位的利器——当解析出来的端口明显不对时直接看原始字节对着 RFC 的字段布局数一遍问题基本就定位了。还有一点是时间戳。header 里的 ts 是 timeval秒加微秒别只显示秒抓包分析里毫秒级差异很关键比如判断重传和乱序。我习惯显示成时分秒加六位小数列表里对齐排好看起来清爽也好比对。5. 避坑与排查五个真机翻车现场5.1 抓不到包三类现象的排查顺序现象一界面上一个包都不来但网卡列表正常。原因通常是过滤表达式写得太窄或者干脆编译失败被忽略了返回值。解决是先清空过滤器跑一遍确认有流量之后再逐步加条件同时检查 pcap_compile 的返回值别把 -1 当成功。现象二能抓到广播和 ARP但抓不到本机访问外网的 TCP。原因是混杂模式在交换网络下不管用你只可能看到本机收发的流量如果连本机的都没有多半是选错了适配器选中了虚拟网卡或已断开的无线。解决是把网卡描述和地址都列出来带 IP 的那个才是你要的。现象三装完驱动重启后依然枚举为空。原因是驱动没真正加载设备管理器里能看到叹号或者被安全软件拦了。解决是把安全软件临时放行卸载后重装装完必须重启别跳过这一步。5.2 界面卡死与崩溃线程边界上的坑现象点「开始抓包」窗口立刻无响应拖都拖不动。原因很明确pcap_loop 跑在 UI 线程。解决是移到工作线程回调里只入队。这个坑几乎每个手写抓包工具的人都踩过一次包括我。现象抓包跑一会儿偶发崩溃dump 里指向 CString 构造。原因是回调里直接拼 CString 更新控件跨线程访问了 MFC 对象。MFC 的大多数对象有线程亲和性必须回到创建它的线程去用。解决办法就是前面说的 PostMessage 加自己取数据绝不在工作线程里构造界面对象。现象点「停止」后概率性崩溃。原因是停止时 pcap_close 在线程还没退出时就执行了。解决是 breakloop 之后等线程句柄超时设个两三秒没退就再等确认退出再 close。5.3 解析错位与乱码字节序、截断长度和字符集现象端口号显示成 80 但源地址变成 0.0.0.0或者端口是 20480 这种明显不对的数。原因是没有做字节序转换或者用 caplen 当总长去算偏移。解决是把所有 ntohs / ntohl 补全并且统一用 IP 头里的总长字段做边界判断。现象抓到的载荷里有中文显示成方块或问号。原因是 char 数组直接转 CString 走了错误的代码页。解决是明确源数据编码HTTP 头里的 charset 或按 UTF-8 尝试用 MultiByteToWideChar 显式指定代码页转换失败就回退成十六进制显示别猜。现象换了台机器或换了无线网卡所有字段全乱。原因是链路层类型不是以太网你还在按 14 字节偏移解。解决是打开后检查 pcap_datalink不是 DLT_EN10MB 就只显示原始字节并在状态栏提示。注意调试解析逻辑时把 pcap 存成文件再离线读比反复在线抓效率高得多。用 pcap_dump 落盘改完代码再 pcap_open_offline 重放同一份数据可以反复验证不用每次重新复现现场。6. 最后一公里让它从玩具变成能用的判断依据跑通抓包和解析只是及格线真正决定这个工具好不好用的是三件事性能、留存、结论。性能上把 snaplen 从 65535 降到 96 到 128环形缓冲容量按每秒包数估界面刷新用 100 毫秒定时器而不是收到就刷这三条改完千兆环境下基本不丢包也不卡顿。要更稳还可以调 pcap_setbuff 加大内核缓冲代价是内存占用按机器配。留存上一定要有导出 pcap 的能力。自己写的解析再全也比不上成熟分析工具的深度遇到疑难问题时把原始包存下来换个工具二次分析是最省时间的路径。加上时间戳和抓包时的过滤器条件一起记下来回看的时候才知道上下文。结论上列表里别只堆原始包按五元组聚合出一个会话视图显示每个会话的包数、字节数、起止时间、TCP 标志位分布。判断「这个连接是不是有问题」靠的是会话级的统计不是单包。我自己最常用的一列是标志位序列SYN 之后连续多个 SYN 就是没连上出现了 RST 就是被拒这些一眼就能看出来比逐包翻快得多。回过头看我在这个项目上最大的教训不是哪个 API 用错而是早期图快把所有逻辑塞进一个文件、所有调用放在 UI 线程等到要加会话统计和过滤功能时改动成本翻了三倍。从那以后我每次起手写这类工具都强制先把线程边界、数据结构和停止流程这三件事定下来再写第一行解析代码哪怕多花半天后面省的是好几天。这套骨架本身不难难的是别在细节上想当然希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表