
简介华中科技大学计算机网络安全实验的完整C源码基于WinPcap库实现数据包嗅探与分析工具并集成ARP欺骗模块演示中间人攻击。面向高校网络空间安全专业学生及对协议分析感兴趣的开发者适合作为网络安全课程设计、毕业设计或综合实验的参考实现。压缩包共27个文件包含8个头文件与7个cpp源文件搭配2个Qt界面文件及png/jpg图形资源项目结构清晰capthread与sendpktthread分别负责抓包与发包protocol.h和arphead.h定义TCP/IP与ARP报文格式arpcheatdialog提供欺骗交互界面各模块职责明确便于阅读。整体大小仅293KB轻量便于加载运行。已有2814人学习源码对WinPcap初始化、设备枚举、数据包逐层解析、ARP缓存欺骗等核心环节均有实现读者可借此深入理解网络嗅探与中间人攻击原理并二次开发扩展检测功能同时可作为实验报告配套材料对照源码梳理思路与关键代码逻辑。1. 基于WinPcap的数据包嗅探器把网卡黑匣子拆开给你看的网络安全实验当我第一次在自己的笔记本上把这份基于 WinPcap 的嗅探器编译跑起来看着浏览器发出的 HTTP 请求在十六进制区一行行滚出来我才真正理解网络协议栈是什么——不是什么抽象的分层矩形而是一串串比特在网卡、驱动、协议头之间流动。这份来自某高校计算机网络安全课程的实验源码目标很直接在 Windows 平台上用 C 语言实现一个数据包分析器嗅探器把网卡收到的原始帧逐层拆成以太网、IP、TCP/UDP 字段按 BPF 规则过滤流量并把抓到的报文落盘保存成通用格式。这套源码最适合三类人正在做网络实验或课程设计的学生想快速跑通 WinPcap 抓包全流程的开发者以及在不引入重型分析框架的前提下想理解“流量分析怎么落地”的入门者。它不是只能应付答辩的演示代码设备枚举、抓包线程、协议解析、过滤、落盘五个关键环节全部串成完整链路。下面我从工程结构、核心代码、避坑经验和二次开发几个方面把它拆开讲。2. 架构与核心模块嗅探器由哪几块拼起来一个嗅探器在功能上只有三步从网卡拿数据、按协议拆数据、把结果给人看。但落到代码工程里这三步需要拆成设备选择、抓包循环、过滤规则、协议解析和结果展示几个独立模块。这份实验源码的工程组织就是沿着这条链路走的结构清晰到可以拿来做网络实验的项目模板。2.1 文件结构解压后每个文件都在干什么先看整体。工程是标准的 VC 项目核心代码全用 C 编写界面可以是 MFC 对话框也可以退化成控制台输出。按功能划分源码文件大致分四类文件职责关键接口DeviceList.c枚举本机所有网卡接口EnumDevices()内部调用pcap_findalldevsCaptureEngine.c打开指定网卡、设置过滤、启动和停止抓包OpenAdapter()、StartCapture()、StopCapture()ParsePacket.c解析以太网、IP、TCP/UDP 协议头ParseEthernet()、ParseIP()、ParseTCP()Storage.c原始帧落盘与读取SaveToFile()、LoadFromFile()这个分工最值得留意的设计决策是“抓包与界面分离”。packet_handler()回调里只负责接收裸帧所有解析和展示逻辑都通过回调参数传给外部模块处理抓包循环不会因为界面刷新慢而被拖死。如果你把解析直接写进回调流量大时界面会卡到怀疑人生——这是新手最常见的第一个设计坑。除了这四类文件工程里还带filter.txt和config.ini两个配置文件。前者保存 BPF 过滤规则文本后者记录默认网卡序号、抓包长度上限snaplen和读取超时timeout。参数外置在实验代码里不常见但对二次开发很实用改过滤规则不用重新编译。2.2 数据流链路一帧报文从网卡到屏幕经过哪五步把整个程序从启动到界面刷新拉直了看数据流转是这样走的pcap_findalldevs()枚举全部网卡虚拟机网卡也会出现在界面下拉框里列出名称和描述用户选定网卡后pcap_open()以混杂模式打开它返回一个pcap_t*句柄pcap_compile()把用户输入的过滤字符串比如tcp port 80编译成 BPF 字节码再交给pcap_setfilter()挂到句柄上程序启动一个独立抓包线程在线程里循环调pcap_next_ex()阻塞式取帧。每取到一帧就触发一次回调回调里完成解析并把结果格式化成一串文本解析结果通过PostMessage投递回主线程界面表格更新显示同时如果开启存储模式原始帧会被追加写入 pcap 文件。这套链路最关键的设计是第 4 步抓包在独立线程里。pcap_next_ex()在 Windows 上是阻塞等待数据包如果放在 UI 线程里抓包期间窗口完全无法响应。实验代码的处理方式是创建抓包线程回调里用PostMessage把解析结果发回主线程再刷新表格。这也是一种界面与数据解耦的思路。WinPcap 提供了两套抓包机制pcap_next_ex()适合自己控制循环节奏随时能退出pcap_loop()把回调注册进库后由内部循环驱动适合无界面的命令行工具。这份源码选的是前者因为实验场景需要响应“开始/停止”按钮。你换到命令行工具场景时pcap_loop更简洁。2.3 为什么选 WinPcap 而不是原始套接字这个问题在实验答辩时几乎必问。为什么不用 Windows 自带的原始套接字Raw Socket我总结了四个理由。第一混杂模式。普通套接字只能收到发给本机的帧嗅探器的核心价值在于能接收共享介质上的所有流量把网卡切到混杂模式正是 pcap 这类库存在的理由。Windows 原始套接字要开混杂模式绕的圈子远比pcap_open传一个 flag 大。第二内核态过滤。BPF 表达式例如host 10.1.1.1 and tcp port 80是编译后传给驱动层处理的过滤在数据到达用户态之前就已发生。自己写套接字抓包过滤逻辑要放在用户态性能和复杂度完全不是一个量级。第三API 可移植。WinPcap 是 Libpcap 的 Windows 实现函数签名几乎一样。写好的解析模块到 Linux 上换个设备枚举和打开的调用就能继续用课程实验之后如果要扩展跨平台场景成本很低。第四文件格式兼容。pcap 文件格式是事实标准抓完的数据拿到协议分析工具里二次核对非常方便。原始套接字自行保存的数据没有通用格式后处理要自己写解析器。3. 核心代码拆解设备枚举、过滤规则与三层协议解析这一章落到真实代码。我挑三段最能代表整个项目技术点的核心代码网卡枚举与打开、BPF 过滤与抓包线程、从以太网到 TCP/UDP 的逐层拆包。这三段是嗅探器的三个承重墙界面代码本质上只是这三段输出的展示层。3.1 设备枚举与网卡打开先拿到所有可用网卡第一段代码解决“抓谁的数据”。Windows 上有多块网卡时程序必须先完整扫描所有适配器让用户选。枚举接口用pcap_findalldevs实现#include pcap.h #include stdio.h int list_devices(void) { pcap_if_t *alldevs; pcap_if_t *dev; char errbuf[PCAP_ERRBUF_SIZE]; int index 0; if (pcap_findalldevs(alldevs, errbuf) -1) { printf(枚举网卡失败: %s\n, errbuf); return -1; } for (dev alldevs; dev ! NULL; dev dev-next) { printf(%d: %s, index, dev-name); if (dev-description) { printf( (%s), dev-description); } printf(\n); } return index; }alldevs是 WinPcap 维护的链表dev-name是形如\Device\NPF_{GUID}的设备标识dev-description是对用户友好的描述文本。枚举完成后必须调用pcap_freealldevs(alldevs)释放链表否则每次枚举都泄漏一块内存跑久了必然出问题。设备选定后进入打开阶段。这里有两个必须关注的参数snaplen决定单帧最多捕获多少字节设为 65535 表示尽量抓完整帧timeout决定读取接口在无流量时最多阻塞多久。打开失败时返回值是空指针errbuf里会写好原因pcap_t *handle pcap_open(dev-name, 65535, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf); if (handle NULL) { printf(打开网卡失败: %s\n, errbuf); pcap_freealldevs(alldevs); exit(1); }PCAP_OPENFLAG_PROMISCUOUS打开的是混杂模式意味着网卡不再只收发给自己的帧同一网段共享介质上的广播帧和直接碰撞域里的帧都能收到。注意笔记本无线网卡和家庭路由器的环境不同无线网卡在驱动不支持混杂抓包时能看到的通常只有本机会话流量如果开了虚拟机\Device\NPF_{GUID}列表里还会混进虚拟网卡选错设备是抓不到数据的常见原因。3.2 BPF 过滤与抓包线程让收包过程更可控网络流量非常嘈杂必须用过滤规则把不关心的帧挡在用户态之前。BPF 过滤表达式要用pcap_compile编译再用pcap_setfilter挂到句柄上struct bpf_program filter; char filter_exp[] tcp port 80; // 只关心 TCP 80 端口流量 if (pcap_compile(handle, filter, filter_exp, 1, PCAP_NETMASK_UNKNOWN) -1) { printf(过滤器编译失败: %s\n, pcap_geterr(handle)); return; } if (pcap_setfilter(handle, filter) -1) { printf(过滤器设置失败: %s\n, pcap_geterr(handle)); return; }pcap_compile的第三个参数是 BPF 表达式第四个参数optimize传 1让库对过滤逻辑做布尔化简第五个netmask只有在表达式里用到广播地址时才有必要填多数场景直接给PCAP_NETMASK_UNKNOWN即可。我习惯把过滤表达式抽到 config 文件里抓包时动态读取再编译这样改规则不需要重编整个程序实验调试阶段能省下大量时间。抓包线程用 Win32 线程 API 创建回调里只做三件事解析原始帧、拼装显示文本、把文本投递回主线程。这段循环是抓包程序的常驻主体DWORD WINAPI capture_thread(LPVOID param) { pcap_t *handle (pcap_t*)param; struct pcap_pkthdr *header; const u_char *pkt_data; int res; while (running) { res pcap_next_ex(handle, header, pkt_data); if (res 1) { char line[1024]; parse_packet(pkt_data, header-caplen, line, sizeof(line)); PostMessage(g_hwnd, WM_PACKET_READY, 0, (LPARAM)line); } else if (res -1) { break; // 网卡被移除或驱动出错 } } return 0; }pcap_next_ex的返回值有三种1 表示成功抓到一帧0 表示本次超时超时时间内没有帧-1 表示出错。这里必须严格判断res 1很多新手写成了res 0结果把超时也当成数据帧处理界面疯狂刷空行。回调参数里的header-len是原始帧长度header-caplen是实际捕获字节数两者在 snaplen 小于帧长时会不一致。解析时边界判断只能用caplen按len去访问帧数据会在截断帧时越界。如果要把抓到的帧存成 pcap 文件不要自己拼文件头。WinPcap 自带pcap_dump系列接口pcap_dumper_t *dumper pcap_dump_open(handle, capture.pcap); // 在回调里追加原始帧 pcap_dump((u_char*)dumper, header, pkt_data); // 退出前关闭 pcap_dump_close(dumper);pcap_dump_open会写好 24 字节文件头和每帧前的 16 字节记录头时间戳也自动换算成正确格式抓完的文件能被外部协议分析工具直接打开对照。3.3 逐层拆包从以太网头到 IP、TCP/UDP 端口核心中的核心是协议解析。以太网帧头固定 14 字节其中 12 字节是 MAC 地址后面 2 字节是以太网类型。类型是0x0800时帧体为 IPv4 报文。代码用字节数组下标方式解析避免直接结构体强转造成字节序混乱#define ETHER_HEADER_LEN 14 void parse_packet(const u_char *pkt, int caplen, char *out, int out_len) { if (caplen ETHER_HEADER_LEN 20) { snprintf(out, out_len, 帧过长或被截断长度%d, caplen); return; } // 以太网类型是网络字节序手动拼成可读值 u_short ether_type (u_short)((pkt[12] 8) | pkt[13]); if (ether_type ! 0x0800) { snprintf(out, out_len, 非IPv4帧类型0x%04x, ether_type); return; } const u_char *ip pkt ETHER_HEADER_LEN; int ip_header_len (ip[0] 0x0f) * 4; // IP头长度字段单位4字节 int protocol ip[9]; // 协议字段6TCP 17UDP char src_ip[16], dst_ip[16]; snprintf(src_ip, 16, %d.%d.%d.%d, ip[12], ip[13], ip[14], ip[15]); snprintf(dst_ip, 16, %d.%d.%d.%d, ip[16], ip[17], ip[18], ip[19]); if (protocol 6) { // TCP头偏移由ip_header_len往后找 int tcp_header_len ((ip[ip_header_len 12] 4) 0x0f) * 4; u_short sport (u_short)((ip[ip_header_len] 8) | ip[ip_header_len 1]); u_short dport (u_short)((ip[ip_header_len 2] 8) | ip[ip_header_len 3]); snprintf(out, out_len, TCP %s:%d - %s:%d, src_ip, sport, dst_ip, dport); } else if (protocol 17) { u_short sport (u_short)((ip[ip_header_len] 8) | ip[ip_header_len 1]); u_short dport (u_short)((ip[ip_header_len 2] 8) | ip[ip_header_len 3]); snprintf(out, out_len, UDP %s:%d - %s:%d, src_ip, sport, dst_ip, dport); } else { snprintf(out, out_len, 协议 protocol%d, protocol); } }这段代码有三个值得反复看的细节。第一字节序。协议头字段一律按网络字节序传输端口号必须手动做高字节左移再拼低位不能直接把数据区强转后读取。第二变长头部。IP 头不是固定 20 字节ip[0]的低 4 位是头长度值乘以 4 才是真实字节数TCP 头的数据偏移字段同理需要定位到 TCP 头内部偏移 12 的字节取高 4 位再乘 4。解析时不能假设头部都是固定长度。第三边界防御。一帧数据如果只是部分包比如caplen小于头部长度的合计后续访问就越界了。用caplen做前置判断是必须的。把这段解析逻辑和前面的抓包循环拼在一起整个嗅探器的主体功能就已经齐了。4. 避坑与排查编译运行中会翻车的五个典型现场实验课常见的流程是把代码拷贝到自己的电脑上改一改、编一编、跑一跑然后把抓包结果截图写进报告。这个看起来简单的流程里翻车点比想象中多。下面是几个每次开课都会被反复问到的典型问题按现象、原因、解决的顺序拆解。我在调试这类实验时有个习惯永远先用控制台跑一次最小流程只开通设备和解析再从控制台切回界面。纯粹的界面问题线程、消息、刷新和逻辑问题字节序、过滤规则、头部解析混在一起时你会不知道先查哪边。所以如果你被某个诡异问题卡住先把界面逻辑注释掉用 printf 把解析结果打出来往往一眼能看到真相。4.1 现象新环境编译报错pcap.h 找不到现象把工程拷到一台刚装好 IDE 的机器上一编译就报“fatal error C1083: 无法打开包括文件: pcap.h”紧接着还有一堆“找不到标识符”的连带错误。原因WinPcap 分为运行库和开发库。运行库负责驱动开发库WpdPack 安装包里的 Include 和 Lib 目录才提供编译所需的头文件和导入库。只装了运行库、没装开发包或者装了开发包但工程属性里没有把目录指过去就会出现这个报错。解决装好 WpdPack 开发包记录解压路径。在 IDE 工程属性的“C/C → 常规 → 附加包含目录”填入...\WpdPack\Include在“链接器 → 常规 → 附加库目录”填入...\WpdPack\Lib再在“附加依赖项”里加上wpcap.lib。如果链接时报 LNK2001 找不到pcap_open之类的外部符号几乎都是这一步没做全。4.2 现象编译通过但一运行就提示 wpcap.dll 缺失现象代码编译顺利双击生成的 exeWindows 弹窗提示“找不到 wpcap.dll”进程直接退出。原因wpcap.dll是 WinPcap 运行库的用户态组件程序在链接期只需要导入库wpcap.lib运行期却必须在系统目录或 exe 同目录下找到对应的 dll。精简安装、安全软件清理、或者重装系统后运行库被卸载都会出现这个结果。解决安装官方发布的完整 WinPcap 运行包。如果是自己在开发机上调试把WpdPack\bin目录下的wpcap.dll、packet.dll手动复制到 exe 输出目录更直接。工程里加一个 post-build 命令行自动复制可以一劳永逸。4.3 现象程序正常启动抓包列表却一直空现象网卡也选了开始按钮也点了界面就是一条记录都不出等几分钟也没有反应。原因多数情况是选错了设备。装了虚拟机软件之后WinPcap 枚举出的设备列表里会多出虚拟网卡它也有自己的\Device\NPF_{GUID}名称和描述很容易排在列表靠前的位置。选中它之后宿主机如果没有虚拟流量自然什么都收不到。另一种常见原因是过滤表达式太严格比如写了tcp port 443但实际测试用的流量根本不走这个端口。解决先在无过滤条件下抓一次确认有数据再逐步加过滤条件。选设备时优先选描述里带“Ethernet”“有线”字样的物理网卡不放心的话在命令行跑route print看本机默认路由走哪个接口再到程序列表里对号入座。抓包期间也可以用ping 127.0.0.1产生最基础的本地回环流量来验证设备是否工作。4.4 现象抓到了帧但 IP 地址显示成倒序现象抓包列表里有数据但内容不对劲IP 地址显示成像0.0.0.0或12.1.168.192这种倒序值TCP 端口也是乱数。原因字节序问题。协议头里的 IP 地址和端口号是网络字节序存储直接按内存地址强转读取得到的是反序整数如果拿这个数再做一次网络序转换就会出现倒序 IP。还有一个同源坑以太网类型字段也必须从大端转成小端再和0x0800比较。解决所有头部字段都用字节下标方式读取再手动拼装或调用ntohs。上面 3.3 节的解析代码就是用数组下标手动拼端口从根上避开字节序问题。网络字节序看起来像“玄学”实际上只要记住“所有跨主机传输的头部字段都是大端”这一句就够了。4.5 现象抓包很流畅但界面卡死没法点停止现象抓包开始后界面无响应按钮点了没反应只能强行结束进程。原因抓包循环写在了 UI 线程里。pcap_next_ex是阻塞调用一帧等不到就一直卡住主线程根本轮不到处理窗口消息界面自然就假死了。解决抓包必须放独立线程。回调里用PostMessage把解析结果发回主线程UI 线程只负责接收消息刷新列表。在 MFC 工程里就是AfxBeginThread开线程回调里PostMessage通知主窗口更新这套双线程写法也是二次开发时的固定骨架。5. 二次开发把实验源码变成可用的协议分析工具如果只是交实验报告跑通就够了。但这份代码的结构给了它很大的改造空间我建议按下面三个方向练手。第一个方向把过滤器变成动态选择。现在过滤表达式是从 config 文件读的改成界面上一个下拉框预置“HTTP 流量tcp port 80”“DNS 流量udp port 53”“ICMP 流量icmp”等常用项用户选择后调用一次pcap_compile加pcap_setfilter重新挂载。这种改造只涉及新增一个控件和几行函数调用不影响抓包线程适合作为第一个练手目标。第二个方向加深应用层协议识别。在拿到 TCP 端口后增加一个端口到协议名的映射表80 标记 HTTP443 标记 HTTPS53 标记 DNS22 标记 SSH。如果再进一步在parse_packet中把 TCP 载荷前几个字节提取出来判断是否以 GET、POST、HTTP/1.1 开头就能识别明文 HTTP 请求——这是最基础的应用层还原。做这个方向时要特别注意载荷长度不足时的边界判断直接用caplen做保护。第三个方向把底层库从 WinPcap 换到 Npcap。已经停更多年的 WinPcap 在新版 Windows 上驱动签名校验越来越麻烦如果你要在自己电脑上长期用建议工程迁移到 Npcap SDK。两者 API 几乎一致头文件和库目录改一下再把pcap_open的标记参数换成 Npcap 的对应常量其余代码基本不用动。验证改造结果有一个很直接的办法开着程序访问几个网站列表里应能看到本机随机端口到服务器 443 端口的 TCP 记录本地ping 127.0.0.1应能看到 ICMP 记录命令行执行nslookup应能看到 UDP 53 的 DNS 记录。每加一种协议解析分支就用对应的实测流量去验证能当场发现问题。我从这个项目中学到最深的一点是做嗅探器这类工具不要同时开发 UI 和解包逻辑。先把抓包、解析做成无界面的核心模块UI 只是它的一层壳之后换底层库、加存储、加深解析改的都是壳或者解析层抓包循环可以保持不变。从那以后我做任何网络程序都会强制把数据采集和解释器严格分离这个习惯让后续几轮改造省了不少心。希望这个习惯对你也有效。本文还有配套的精品资源点击获取