简介华中科技大学2018年计算机网络安全实验源码面向网络协议分析与安全方向的学生及实验者完整实现了基于Winpcap的数据包抓取与分析并进一步通过ARP欺骗完成中间人攻击可直接运行或对照学习也是课程设计与网络攻防实验的可选参考。资源共27个文件以8个C头文件和7个C源文件为主体覆盖抓包线程、ARP欺骗线程、主窗口逻辑等核心模块另有2个Qt界面文件及配套pro/qrc工程配置5张png与2张jpg图片辅助展示界面与效果整个压缩包约293KB结构紧凑。目前已有2814人学习浏览适合用来理解嗅探器工作原理、ARP协议细节以及Qt框架下网络抓包工具的界面组织。通过这份源码不仅能掌握Winpcap的初始化、数据包回调解析与过滤流程还能看到从底层数据包解析到上层攻击演示的完整链路对课设与安全入门均有直接帮助。1. 基于 Winpcap 的网络嗅探器实验源码拆解与完整复现用 Winpcap 写一个数据分析器嗅探器是网络方向课程实验里绕不开的经典题它要求你绕过系统协议栈直接从网卡取原始帧再按以太网、IP、TCP/UDP 的层次把二进制拆成可读报告。换句话说拆的不是“抓包”而是“字节偏移”。这份源码包涵盖设备枚举、混杂模式抓包、协议头解析和常见过滤器适合三类人第一次接触 Winpcap 的新手、需要交课程设计报告的学生、想自研轻量抓包工具的开发者。它解决的核心问题是当网卡把原始帧交给你时你该按什么顺序、在哪些偏移上取字段以及环境坑都出在哪。2. Winpcap 抓包原理与环境先搞懂驱动和 API 的调用顺序2.1 为什么普通 socket 拿不到完整数据包用 socket 编程收数据recv() 拿到的永远是应用层载荷操作系统协议栈早已把以太网帧头、IP 头、TCP/UDP 头全部剥掉。这对常规通信是好事对嗅探却是致命限制。Winpcap 的思路是在网卡驱动与 TCP/IP 协议栈之间插入一个内核态过滤层网卡每收到一帧先复制一份交给用户态程序再把原始帧继续交给协议栈处理。因此你用 pcap_next_ex 拿到的 pkt_data是包含链路层头部的完整数据包哪怕它并非发给本机只要网卡在混杂模式下也会被捕获。这里最关键的一个认知是抓包程序的“全知视角”不是靠应用层轮询实现的而是靠驱动在更低位置做旁路复制。驱动对你来说是黑匣子但 API 行为固定只要按设备枚举→打开→循环读包的顺序走就能稳定拿到帧。另一个常被忽略的点是混杂模式pcap_open_live 的第二个参数传 1 时网卡才会接收目的地址不是自己的帧传 0 的话你只能抓到本机收发与广播包效果接近普通 socket 外的有限流量。很多实验做到一半发现“只能看到自己”问题往往就出在这。2.2 设备枚举、打开与实时循环抓包先别急着写解析函数第一步是把一个可用的 pcap_t 句柄拿到手。常见做法是先用 pcap_findalldevs_ex 枚举本机所有网卡打印设备名再让程序选择其中一个打开。我把这段枚举代码放在下面#include pcap.h #include stdio.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, ws2_32.lib) int main() { pcap_if_t *alldevs NULL; pcap_if_t *dev NULL; char errbuf[PCAP_ERRBUF_SIZE] { 0 }; // 枚举本机所有网络接口PCAP_SRC_IF_STRING 表示从本机获取 if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, alldevs, errbuf) ! 0) { printf(枚举网卡失败: %s\n, errbuf); return -1; } int index 0; for (dev alldevs; dev ! NULL; dev dev-next) { printf(接口编号 %d: %s\n, index, dev-name); if (dev-description) { printf( 描述: %s\n, dev-description); } } // 这里你应该根据打印结果记录想抓包的编号再传入下面的打开逻辑 pcap_freealldevs(alldevs); return 0; }pcap_findalldevs_ex 返回 0 表示成功非 0 时错误文本会写入 errbuf。dev-name 通常是“\Device\NPF_{GUID}”这类驱动路径不是普通网卡名别拿它去匹配系统的中文接口名dev-description 才是人可读的描述比如“Realtek PCIe GbE Family Controller”。这段代码跑通后你已经完成了抓包环境的第一层验证驱动装没装好、权限够不够几乎都能在这里暴露出来。接下来是打开设备并进入实时循环。这里我用 pcap_open_live 打开网卡再用 pcap_next_ex 一帧一帧读pcap_t *fp NULL; // 第二参数 65536 是 snaplen第三参数 1 开启混杂模式第四参数 1000 是读超时毫秒数 fp pcap_open_live(dev-name, 65536, 1, 1000, errbuf); if (fp NULL) { printf(打开网卡失败: %s\n, errbuf); return -1; } struct pcap_pkthdr *header NULL; const unsigned char *pkt_data NULL; int ret 0; while ((ret pcap_next_ex(fp, header, pkt_data)) 0) { if (ret 0) { // 超时无包不处理 continue; } printf(抓到 %d 字节时间戳 %ld抓取长度 %d\n, header-len, (long)header-ts.tv_sec, header-caplen); } if (ret -1) { printf(抓包出错: %s\n, pcap_geterr(fp)); } pcap_close(fp);pcap_open_live 的第一个参数是设备名65536 表示每个帧最多截前 64 KB对常规以太网帧够用promisc1 打开混杂模式timeout1000 表示每 1000 毫秒返回一次。这里有个细节header-len 是线上真实长度header-caplen 是实际复制到用户态的字节数。当 snaplen 小于帧长度或遭遇驱动截断时caplen 会小于 len后面解析时必须用 caplen 做边界否则越界读内存是早晚的事。pcap_next_ex 的返回码也容易看错1 表示成功读到一包0 表示超时无包-1 表示出错-2 表示读取离线文件已到末尾。很多人把 0 当成失败导致循环提前退出事实上 ret0 时直接 continue 即可。2.3 编译链接配置与运行环境源码包里的代码不会自己带上 pcap.h 和库文件工程必须额外指定 WinPcap Developers Pack 的目录。最省事的做法是在 VC6/VS 工程属性里配置预处理宏与附加目录头文件组合一般是#include pcap.h #include packet32.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, Packet.lib)pcap.h 声明 pcap 系列 APIpacket32.h 提供 PacketRequest 这类网卡底层操作接口实验里打印本机 MAC 或查询网卡描述时会用到所以两个库都链上。Developers Pack 解压后通常有一个 WpdPack 目录把 include 与 lib 分别填入项目的附加目录即可。运行时目标机器上必须安装 WinPcap 或 Npcap。当前主流做法是装 Npcap并在安装界面勾选“WinPcap API 兼容模式”这样原有 pcap_xxx 代码不用改就能跑。老版本 WinPcap 4.1.3 在 Windows 10/11 上存在驱动签名问题装完可能显示“设备无法启动”我把这个问题放到第 4 章避坑部分细讲。另外要注意工程位数工程是 x64 就链 x64 的 lib不要混用否则打开抓包句柄时会出现诡异的 LNK2019。工程配置里还有两个容易踩的编译开关预处理定义要加上 WIN32、WPCAPWinpcap 官方示例通常自带运行时库建议先用默认的 Debug 配置/MTd 或 /MDd跑通再考虑改发布配置。很多同学卡在“编译不过”上排除完头文件路径后发现是预处理宏缺失。3. 协议解析从以太网帧头到 TCP/UDP 端口3.1 以太网帧头解析与类型分发抓到数据包后第一层拆的是以太网帧头。以太网帧头固定 14 字节6 字节目的 MAC、6 字节源 MAC、2 字节类型字段。类型字段决定上层是谁0x0800 是 IPv40x0806 是 ARP0x86DD 是 IPv6。在主循环里拿到 pkt_data 后可以先做一个最小长度保护再交给解析函数while (1) { int ret pcap_next_ex(fp, header, pkt_data); if (ret ! 1) { continue; } if (header-caplen 14) { ParseEthernet(pkt_data, header-caplen); } }ParseEthernet 的实现void ParseEthernet(const unsigned char *pkt, int len) { if (len 14) { printf(帧长度不足可能被截断\n); return; } // 前 6 字节目的 MAC接着 6 字节源 MAC printf(目的 MAC: %02X-%02X-%02X-%02X-%02X-%02X\n, pkt[0], pkt[1], pkt[2], pkt[3], pkt[4], pkt[5]); printf(源 MAC: %02X-%02X-%02X-%02X-%02X-%02X\n, pkt[6], pkt[7], pkt[8], pkt[9], pkt[10], pkt[11]); // 类型字段是大端序高字节在前 unsigned short type (unsigned short)((pkt[12] 8) | pkt[13]); printf(以太网类型: 0x%04X\n, type); switch (type) { case 0x0800: // 数据从第 14 字节开始长度要减去以太网头 14 字节 ParseIPv4(pkt 14, len - 14); break; case 0x0806: printf(ARP 报文按帧打印 MAC/IP 即可\n); break; case 0x86DD: printf(IPv6 报文本实验可不做完整解析\n); break; default: printf(未知协议类型\n); } }注意 MAC 地址打印要按字节逐个输出不要尝试把 6 个字节拼成一个 u64 再打印那样字节序会乱。以太类型字段是网络字节序大端我习惯手动拼(pkt[12] 8) | pkt[13]可行且不受对齐问题影响如果你用ntohs(*(unsigned short *)(pkt 12))也等价但非对齐访存在某些平台上有隐患。3.2 从 IP 头提取协议、地址与总长度以太网头解析完接着处理 IP 头。IPv4 头的第一个字节很关键高四位是版本号低四位是 IHLInternet Header Length单位是 4 字节所以常见取值 0x45 表示 IPv4、头长 5×420 字节。协议字段在偏移 9源 IP 在偏移 12目的 IP 在偏移 16总长度在偏移 2。解析函数如下void ParseIPv4(const unsigned char *ip, int len) { if (len 20) { printf(IPv4 头不足 20 字节\n); return; } int ver (ip[0] 4) 0x0F; int ihl (ip[0] 0x0F) * 4; if (ver ! 4 || ihl 20) { printf(非法 IPv4 报文ver%d ihl%d\n, ver, ihl); return; } unsigned short total_len (unsigned short)((ip[2] 8) | ip[3]); unsigned char proto ip[9]; unsigned char ttl ip[8]; printf(IP 版本: %d, 头长: %d, 总长度: %d\n, ver, ihl, total_len); printf(TTL: %d\n, ttl); // IP 地址是点分十进制直接按字节打印 printf(源 IP: %d.%d.%d.%d\n, ip[12], ip[13], ip[14], ip[15]); printf(目的 IP: %d.%d.%d.%d\n, ip[16], ip[17], ip[18], ip[19]); // 如果长度超过实际捕获长度按捕获长度截断 int cap_len total_len; if (cap_len len) { cap_len len; } if (proto 6) { ParseTCP(ip ihl, cap_len - ihl); } else if (proto 17) { ParseUDP(ip ihl, cap_len - ihl); } else if (proto 1) { printf(ICMP 报文本程序只打印基础字段\n); } else { printf(协议号 %d跳过上层解析\n, proto); } }这里最容易翻车的是把 ihl 当成字节数直接用。ihl 字段的语义是“4 字节的倍数”所以必须乘以 4。另一个问题是 total_len 可能大于实际捕获长度 caplen尤其是在 snaplen 较小或网卡驱动截断时用 min(total_len, len) 限制解析边界能避免后面越界读。IP 头的协议号值得记住6TCP17UDP1ICMP。3.3 TCP/UDP 头解析端口、序列号与数据偏移走到这一步已经能从字节流里看到“谁在和谁通信”。TCP 头最小 20 字节前两个字段分别是源端口和目的端口各 2 字节序列号占 4 字节第 12 字节的高四位是数据偏移单位 4 字节第 13 字节的低 6 位是 URG/ACK/PSH/RST/SYN/FIN 标志。解析代码void ParseTCP(const unsigned char *tcp, int len) { if (len 20) { printf(TCP 头长度不足\n); return; } unsigned short sport (unsigned short)((tcp[0] 8) | tcp[1]); unsigned short dport (unsigned short)((tcp[2] 8) | tcp[3]); unsigned int seq ((unsigned int)tcp[4] 24) | ((unsigned int)tcp[5] 16) | ((unsigned int)tcp[6] 8) | (unsigned int)tcp[7]; int data_off (tcp[12] 4) * 4; unsigned char flags tcp[13]; printf(TCP %d - %d, 序列号 %u, TCP头长 %d\n, sport, dport, seq, data_off); printf(标志: %s%s%s%s%s%s\n, (flags 0x02) ? SYN : , (flags 0x10) ? ACK : , (flags 0x01) ? FIN : , (flags 0x04) ? RST : , (flags 0x08) ? PSH : , (flags 0x20) ? URG : ); }TCP 端口 80/443 通常暗示 HTTP/HTTPS端口 53 多半是 DNS但只凭端口判断协议并不严谨实验报告里注明“按端口猜测”即可。UDP 头更简单固定 8 字节void ParseUDP(const unsigned char *udp, int len) { if (len 8) { printf(UDP 头不足 8 字节\n); return; } unsigned short sport (unsigned short)((udp[0] 8) | udp[1]); unsigned short dport (unsigned short)((udp[2] 8) | udp[3]); unsigned short udp_len (unsigned short)((udp[4] 8) | udp[5]); printf(UDP %d - %d, UDP长度 %d\n, sport, dport, udp_len); if (sport 53 || dport 53) { printf(该数据包疑似 DNS 报文\n); } }端口、长度、总长度这些多字节字段全部是网络字节序在 x86 小端机器上不能直接解引用打印手动移位拼接是最直观的写法也能避免结构体对齐的坑。经过这三层解析分析器已经能还原出“谁在某个时间点向谁发起连接、使用什么协议、目标端口是多少”的完整链路实验核心功能至此跑通。4. 避坑盘点抓包抓不到、解析错乱的五个常见原因这一章是这份实验里真正耗时间的部分。协议解析本身是刻板的偏移计算但环境与运行时的坑会让一个逻辑正确的程序“看起来完全不对”。下面五条都是实际跑实验时的血泪经验按现象、原因、解决三个层面说清楚。4.1 混杂模式失效只抓得到本机流量现象程序能跑但抓到的基本都是自己本机发出的包同网段其他机器的流量一概看不到。原因第一pcap_open_live 的第二参数没有显式传 1第二实验环境在交换网络中交换机不会把其他主机的帧复制到你的端口第三部分无线网卡驱动即使设置了混杂模式也不生效它会把非本机帧直接丢弃。解决先把 pcap_open_live 的 promisc 参数写死为 1再尝试用有线网卡而不是 Wi-Fi 抓包。如果网络环境不允许跨主机抓包就在本机上用 ping、访问网页产生流量并把验证目标改为“能抓到本机回环与广播的 ARP/ICMP 帧”一样能完成协议解析的验收。4.2 过滤器写错包数量与预期不符甚至为空现象设置了tcp port 80之后一个包都抓不到或者设置 ip 地址条件后抓到一堆无关流量。原因BPF 过滤器语法对空格、关键字大小写敏感常见错误是把 host 写成 ip host把 and/or 写成 和 ||另一个原因是 pcap_compile 的第三个参数 netmask 填了 0导致部分地址匹配失效。解决先不加过滤器跑一次确认网卡确实有流量再改成最简单的表达式比如tcp或udp port 53逐步加条件。netmask 参数可以先用 0xFFFFFF00如果你知道本机掩码是 24 位就填它。pcap_compile 返回 0 才代表表达式编译成功否则拿 pcap_geterr(fp) 打印具体错误信息。4.3 字节序混乱MAC 地址反了、端口打印成几千上万的大数现象源 MAC 和目的 MAC 打印出来像是“反的”端口号明明应该是 80却打印成 20480 之类的数。原因网络字节序是大端x86 是小端直接用*(unsigned short *)pkt取端口读出来是反的MAC 地址打印时可能把 pkt[0..5] 和 pkt[6..11] 搞混。解决所有多字节字段统一用手动移位拼接(pkt[0] 8) | pkt[1]这种写法不依赖 host 字节序。MAC 地址每个字节单独打印%02X不要拼成 64 位整数再格式化。吃一堑长一智把字节序检查做成一个固定的打印宏后续所有偏移都能复用。4.4 截断包越界解析到一半程序崩溃或显示长度不足现象程序跑一会儿之后崩溃或者某些包打印“长度不足”直接跳过尤其是抓大包时。原因header-caplen 小于 header-len。snaplen 设置过小或者网卡驱动/中间层截断了包也可能某些帧本身长度异常比如 runt frame。解决每一层解析函数入口都做长度判断这是必选的防线。另外在所有总长度字段参与边界计算时使用cap_len min(total_len, len)限制解析上限避免拿线上长度去访问实际捕获不到的缓冲区。从那以后我写每个解析函数第一行都是长度检查之后再读字段。4.5 链接失败或运行时报找不到 wpcap.dll现象编译报 LNK2019找不到 pcap_open_live或者程序生成后一运行弹窗提示“由于找不到 wpcap.dll无法继续执行代码”。原因lib 路径没加、工程位数与 lib 位数不一致、运行机器没有安装 WinPcap/Npcap 驱动三选一。解决工程属性里确认附加库目录指向 WpdPack/lib并保证 x86/x64 对齐运行机器装 Npcap 时勾选 WinPcap API 兼容模式。老版 WinPcap 4.1.3 在 Win10/11 上驱动签名容易失败现象是设备管理器的 NPF 设备带黄色感叹号这时卸载重装 Npcap 几乎能解决绝大多数玄学问题。如果上面五条还覆盖不了你的情况我的习惯是把程序拆成三截单独验证先只跑设备枚举确认网卡列表再只跑打开与循环抓包确认能出包最后才加解析函数。三段各自能跑通组合起来出错就只可能出在长度交接上排查范围会小很多。5. 让分析器更像工具加过滤器、落盘保存再验证一遍实验验收时单纯的实时打印并不够友好我一般会再加三件事BPF 过滤、pcap 文件落盘、以及用 Wireshark 做交叉验证。加过滤器的代码很简单重点在表达式。想在抓包阶段就只关心 HTTP 和 DNS可以这样struct bpf_program bpf; char filter[] tcp port 80 or udp port 53; if (pcap_compile(fp, bpf, filter, 1, 0xFFFFFF00) 0 pcap_setfilter(fp, bpf) 0) { printf(过滤器已生效\n); } else { printf(过滤器失败: %s\n, pcap_geterr(fp)); }pcap_compile 的第四个参数 optimize 填 1第五个参数是网络掩码拿不准就用 0xFFFFFF00。这个过滤逻辑放在循环之前驱动在内核层就把不匹配的包丢掉了程序省去大量无效解析也减少丢包可能。落盘保存更适合“先抓后分析”的实验流程。用 pcap_dump_open 打开输出文件每次收到包时用 pcap_dump 写进去pcap_dumper_t *dumper pcap_dump_open(fp, capture.pcap); if (dumper ! NULL) { pcap_dump((unsigned char *)dumper, header, pkt_data); } // 最后 pcap_dump_close(dumper)这样生成的文件就是标准 pcap 格式Wireshark 可以直接打开。验证方法是我最推荐的收尾动作先在本机 ping 一个地址自己的分析器应能打印出 ICMP 报文和对应的 ARP 请求同时把 capture.pcap 丢进 Wireshark对比前 14 字节的 MAC 与类型字段确认手工解析没有偏移算错。这一步能把“看起来差不多”提升到“确实每字节都对”。从那以后我每次做这类抓包实验都强制走一遍固定流程枚举网卡 → 抓本机环比流量验证环境 → 写协议解析 → 落盘对比 Wireshark。这套顺序帮我省掉了大量反复排查的时间也是这份源码最值得跟着做一遍的原因。希望帮到你。本文还有配套的精品资源点击获取