☰
WinPcap与C++实现ARP局域网物理地址扫描实战
2026/10/8 11:59:56 网站建设 项目流程

简介:这份资源是广东工业大学计算机网络课程设计的完整项目文档,面向正在学习网络协议或需要完成同类课设的计算机专业学生。内容围绕使用ARP协议获取局域网内活动主机物理地址这一核心任务展开,涵盖ARP工作原理、以太网帧与ARP帧结构解析、Winpcap库的调用方法,以及从构造请求帧到接收解析响应、显示IP-MAC映射的完整实现流程。资源包内含1个PDF文件,大小约458KB,以图文与代码结合的方式呈现设计题目、技术参数、实现步骤、运行结果与代码分析,结构完整,可直接作为课设参考模板。目前已有169人学习下载。读者可从中获得协议分层交互的直观理解、数据帧构造与解析的实操思路,以及基于Visual C++与Winpcap的编程范例,适合需要快速搭建实验框架或查漏补缺的学习者。

1. 从一份课程设计说起:ARP 抓取局域网物理地址到底在做什么

很多同学拿到「使用 ARP 协议获取局域网内部活动主机物理地址的程序实现」这个题目时,第一反应是去搜 ARP 协议原理,看完三次握手式的请求应答图,觉得自己懂了,然后打开 Visual Studio 准备写代码,结果卡在第一步——怎么把网卡里的原始帧捞出来。这个题目的核心不是背 ARP 报文格式,而是用 C++ 在 Windows 上调用 WinPcap(或它的后继 Npcap)直接读写数据链路层,自己构造 ARP 请求帧发出去,再解析回来的应答帧,把 IP 和 MAC 的对应关系打印出来。它解决的是一个很实际的问题:局域网里哪些主机是活的、它们的物理地址是什么。适合已经学过计算机网络、会写基本 C++、但没碰过原始套接字和网卡抓包的人。下面我按自己踩过的路,把选型、环境、代码、参数和坑一次讲清楚。

2. 动手前的选型:为什么是 WinPcap 加 C++,而不是系统 API

2.1 三种获取局域网 MAC 地址的路子对比

在 Windows 上想拿到局域网内其他主机的 MAC 地址,常见做法有三条。第一条是调用SendARP,这是 Windows 自带的 IP Helper API,一行调用就能拿到指定 IP 的 MAC,但它只能查单个 IP,而且底层行为不透明,课程设计里老师通常不认。第二条是读系统 ARP 缓存表,用GetIpNetTable或者直接解析arp -a的输出,问题是缓存表里只有最近通信过的主机,不主动发请求就扫不全。第三条就是自己用 WinPcap 构造并发送 ARP 请求帧,主动探测整个网段,这也是这个题目真正想让你做的事。

方案能否主动探测可控性课程设计适用度
SendARP单 IP 逐个调用低,黑盒低,容易被判偷懒
读 ARP 缓存表不能,被动低低,扫不全
WinPcap 构造 ARP 帧能,整网段高,报文自己拼高,符合题目要求

选 WinPcap 的理由很直接:它提供了pcap_sendpacket和pcap_next_ex这一发一收的接口,让你能完全控制 ARP 帧的每一个字节。你需要自己填以太网头、ARP 头,算好各字段偏移,这种「从零拼一个协议帧」的过程才是网络编程课想训练的能力。C++ 在这里的角色是提供结构体对齐、指针偏移和缓冲区管理,比 C 写起来顺手,又不像 C# 那样被运行时挡在链路层外面。

2.2 环境搭建:WinPcap 开发包与 Visual Studio 配置

WinPcap 官方已经停止更新,但在课程设计场景下仍然够用。你需要两样东西:WinPcap 驱动安装包(装完系统里会有wpcap.dll)和 WinPcap Developer's Pack(里面有pcap.h和wpcap.lib)。安装驱动时如果提示与系统不兼容,常见做法是右键安装包选兼容模式运行。开发包解压后,把Include目录和Lib目录的路径记下来,下一步要用。

在 Visual Studio 里新建一个空 C++ 项目,然后做三处配置。第一,项目属性 → C/C++ → 常规 → 附加包含目录,填入开发包的Include路径。第二,链接器 → 常规 → 附加库目录,填入Lib路径。第三,链接器 → 输入 → 附加依赖项,加上wpcap.lib和ws2_32.lib。ws2_32.lib是为了用inet_addr这类转换函数。配置完写一个最小的测试程序,只调用pcap_findalldevs打印网卡列表,能编译通过并列出你的网卡名字,说明环境就通了。

#include <pcap.h> #include <iostream> int main() { pcap_if_t* alldevs; char errbuf[PCAP_ERRBUF_SIZE]; // 枚举本机所有网卡,结果挂在 alldevs 链表上 if (pcap_findalldevs(&alldevs, errbuf) == -1) { std::cerr << "枚举网卡失败: " << errbuf << std::endl; return 1; } for (pcap_if_t* d = alldevs; d != nullptr; d = d->next) { std::cout << "网卡: " << d->name; if (d->description) std::cout << " (" << d->description << ")"; std::cout << std::endl; } pcap_freealldevs(alldevs); return 0; }

这段代码的逻辑是:pcap_findalldevs把本机所有可抓包的网卡信息填进一个链表,每个节点包含网卡名name和描述description。errbuf是出错时的信息缓冲区,大小由PCAP_ERRBUF_SIZE固定。参数上没什么可调的,唯一要注意的是必须以管理员权限运行,否则枚举可能返回空列表。跑通这一步,后面才有得谈。

3. 构造并发送 ARP 请求帧:从字节偏移到网段扫描

3.1 以太网头与 ARP 报文的字段布局

要自己拼 ARP 帧,得先把两个结构在脑子里摆清楚。以太网头固定 14 字节:6 字节目的 MAC、6 字节源 MAC、2 字节类型。ARP 请求的目的 MAC 是广播地址FF:FF:FF:FF:FF:FF,类型字段填0x0806表示后面是 ARP 报文。ARP 报文本身 28 字节:2 字节硬件类型(以太网填 1)、2 字节协议类型(IPv4 填0x0800)、1 字节硬件地址长度(6)、1 字节协议地址长度(4)、2 字节操作码(请求填 1,应答填 2)、6 字节发送方 MAC、4 字节发送方 IP、6 字节目标 MAC(请求时全 0)、4 字节目标 IP。加起来一帧 42 字节,小于以太网最小帧长 60 字节,所以实际发送时网卡会自动填充到 60 字节,这个不用你管。

用 C++ 表示就是两个结构体,注意要加#pragma pack(1)关闭对齐,否则编译器会在字段间插填充字节,偏移全乱。这是血泪经验,我第一次写的时候没加,发出去的帧被 Wireshark 解析成未知协议,查了半天才发现是结构体对齐的锅。

#pragma pack(1) // 关键:关闭结构体对齐,保证字段连续 struct EthernetHeader { uint8_t destMac[6]; uint8_t srcMac[6]; uint16_t type; // 0x0806 表示 ARP }; struct ArpPacket { uint16_t hwType; // 1 = 以太网 uint16_t protoType; // 0x0800 = IPv4 uint8_t hwLen; // 6 uint8_t protoLen; // 4 uint16_t opcode; // 1 = 请求, 2 = 应答 uint8_t senderMac[6]; uint8_t senderIp[4]; uint8_t targetMac[6]; // 请求时填 0 uint8_t targetIp[4]; }; #pragma pack()

#pragma pack(1)告诉编译器按 1 字节对齐,uint16_t字段就不会被强行对齐到偶数地址。opcode是区分请求和应答的关键,你发出去时填 1,收到别人回给你的帧里这个字段是 2。senderMac和senderIp填你自己的网卡 MAC 和本机 IP,targetIp填你要探测的那个 IP,targetMac全填 0,因为你还不知道对方 MAC。

3.2 打开网卡、注入帧、循环扫描整个 C 段

有了结构体,接下来是打开网卡、拼帧、发送、接收的完整流程。打开网卡用pcap_open_live,第一个参数是网卡名(从pcap_findalldevs拿到的name),第二个是抓包长度,填 65536 保证整帧都能收到,第三个是混杂模式,ARP 探测填 0 就行,第四个是超时毫秒数,填 1000,第五个是错误缓冲区。发送用pcap_sendpacket,接收用pcap_next_ex。

扫描逻辑是:假设你的网段是192.168.1.0/24,就循环 1 到 254,每个 IP 构造一个 ARP 请求发出去,然后在一个循环里收应答,收到就解析出 IP 和 MAC 存起来。这里有个节奏问题,一次性发 254 个帧再收,可能会丢包,常见做法是发一批收一批,或者每发一个就等一小段时间。我一般用发送后立即进入接收循环、设一个总超时的方式,简单可靠。

// 打开网卡,devName 来自 pcap_findalldevs pcap_t* handle = pcap_open_live(devName, 65536, 0, 1000, errbuf); if (!handle) { std::cerr << errbuf << std::endl; return 1; } // 获取本机 MAC 和 IP,这里假设已通过 GetAdaptersInfo 拿到 uint8_t myMac[6] = {0xAA,0xBB,0xCC,0xDD,0xEE,0xFF}; uint8_t myIp[4] = {192,168,1,100}; for (int host = 1; host <= 254; ++host) { uint8_t frame[42] = {0}; EthernetHeader* eth = (EthernetHeader*)frame; ArpPacket* arp = (ArpPacket*)(frame + 14); memset(eth->destMac, 0xFF, 6); // 广播 memcpy(eth->srcMac, myMac, 6); eth->type = htons(0x0806); arp->hwType = htons(1); arp->protoType = htons(0x0800); arp->hwLen = 6; arp->protoLen = 4; arp->opcode = htons(1); // ARP 请求 memcpy(arp->senderMac, myMac, 6); memcpy(arp->senderIp, myIp, 4); memset(arp->targetMac, 0, 6); arp->targetIp[0] = 192; arp->targetIp[1] = 168; arp->targetIp[2] = 1; arp->targetIp[3] = (uint8_t)host; pcap_sendpacket(handle, frame, 42); }

这段代码里htons是把主机字节序转成网络字节序,因为协议字段是大端。frame + 14这个偏移就是以太网头之后 ARP 报文的起始位置,这个 14 是硬编码的,因为以太网头固定 14 字节。发送完 254 个请求后,进入接收循环,用pcap_next_ex拿帧,检查以太网类型是不是0x0806,是的话再看 ARP 操作码是不是 2,是应答就提取senderIp和senderMac。参数上,pcap_open_live的超时设 1000 毫秒意味着pcap_next_ex最多阻塞 1 秒就返回,你可以靠这个控制接收循环的退出时机。

4. 解析应答帧与结果输出:把 IP 和 MAC 对上号

4.1 从原始帧里提取 ARP 应答的关键字段

接收到的帧同样是 14 字节以太网头加 28 字节 ARP 报文。你要做的是跳过前 14 字节,把后面的 ARP 结构体指针接上,然后判断opcode是否为 2。应答帧里senderMac和senderIp就是回复你的那台主机的物理地址和 IP,直接读出来即可。注意收到的帧可能不止 ARP,还混着别的广播,所以类型判断不能省。

struct pcap_pkthdr* header; const u_char* pktData; int res; while ((res = pcap_next_ex(handle, &header, &pktData)) >= 0) { if (res == 0) continue; // 超时,继续等 EthernetHeader* eth = (EthernetHeader*)pktData; if (ntohs(eth->type) != 0x0806) continue; // 不是 ARP,跳过 ArpPacket* arp = (ArpPacket*)(pktData + 14); if (ntohs(arp->opcode) != 2) continue; // 不是应答,跳过 printf("IP: %u.%u.%u.%u MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", arp->senderIp[0], arp->senderIp[1], arp->senderIp[2], arp->senderIp[3], arp->senderMac[0], arp->senderMac[1], arp->senderMac[2], arp->senderMac[3], arp->senderMac[4], arp->senderMac[5]); }

pcap_next_ex返回 1 表示抓到包,0 表示超时,-1 表示出错,-2 表示文件结束(离线抓包时才有)。header里有时间戳和实际帧长,这里用不到。ntohs和发送时的htons对应,把网络字节序转回主机字节序再比较。打印格式里%02X保证 MAC 每段两位大写十六进制,这是最常见的输出习惯。

4.2 结果去重、排序与保存到文件

扫完一轮,同一个 IP 可能因为重传收到多个应答,需要去重。简单做法是用一个std::map<uint32_t, std::array<uint8_t,6>>,key 是 IP 转成的 32 位整数,value 是 MAC 数组,插入时天然去重。最后遍历 map 输出,顺便按 IP 排序。如果要交课程设计报告,把结果写进一个文本文件,格式一行一个「IP MAC」,方便贴进文档。

#include <map> #include <array> #include <fstream> std::map<uint32_t, std::array<uint8_t,6>> result; // 在解析到应答后: uint32_t ipKey = (arp->senderIp[0] << 24) | (arp->senderIp[1] << 16) | (arp->senderIp[2] << 8) | arp->senderIp[3]; std::array<uint8_t,6> mac; memcpy(mac.data(), arp->senderMac, 6); result[ipKey] = mac; // 重复 IP 会覆盖,达到去重效果 std::ofstream out("arp_result.txt"); for (auto& kv : result) { uint8_t* ip = (uint8_t*)&kv.first; fprintf(stderr, "%u.%u.%u.%u %02X:%02X:%02X:%02X:%02X:%02X\n", ip[3], ip[2], ip[1], ip[0], kv.second[0], kv.second[1], kv.second[2], kv.second[3], kv.second[4], kv.second[5]); }

这里有个字节序的坑:ipKey拼接时我是按大端顺序移位的,但(uint8_t*)&kv.first在小端机器上取出来是反的,所以打印时用ip[3] ip[2] ip[1] ip[0]倒回来。这个细节不处理,输出来的 IP 是反的,比如192.168.1.1会变成1.1.168.192。参数上没什么可调的,std::map自动按 key 排序,输出就是按 IP 从小到大。

5. 避坑与排查:那些让程序跑不通的细节

5.1 现象:编译报错找不到 pcap.h

原因基本是附加包含目录没配对,或者开发包解压后路径里有空格没加引号。解决方法是回到项目属性,确认Include目录填的是绝对路径,路径里如果有空格,Visual Studio 一般能处理,但保险起见放在无空格目录下。另一个可能是你装的是 Npcap 而不是 WinPcap,Npcap 的头文件路径不同,需要单独下载 Npcap SDK。

5.2 现象:程序运行后一个应答都收不到

先确认是不是以管理员权限运行,非管理员权限下pcap_open_live可能成功但发不出包。再确认网卡选对了,多网卡机器上选错网卡(比如选了无线网卡却插着网线)就扫不到。还有一个常见原因是 Windows 防火墙拦截了原始帧,临时关闭防火墙测试。如果这些都对,用 Wireshark 同时抓包,看你的 ARP 请求有没有真的发出去,发了但没应答,说明目标网段里确实没活动主机,或者你填的目标 IP 网段和本机不在同一个 C 段。

5.3 现象:收到的 MAC 全是广播地址或全零

这是解析偏移错了。检查#pragma pack(1)有没有加,没加的话结构体字段偏移和协议对不上,读出来的就是垃圾数据。另一个可能是你把pktData + 14写成了pktData + 16或别的值,以太网头就是 14 字节,多一个少一个都错。用 Wireshark 对照看一帧真实的 ARP 应答,逐字节核对你的偏移。

5.4 现象:扫描速度极慢,254 个 IP 要等好几分钟

原因是每发一个请求就阻塞等应答,超时设得太长。改进方法是先把 254 个请求全部发出去,再统一进接收循环收一段时间,比如 3 秒。或者用多线程,一个线程发,一个线程收。参数上把pcap_open_live的超时从 1000 降到 100,接收循环里累计超时次数达到阈值就退出,能明显加快。

5.5 现象:换一台机器编译通过但运行崩溃

大概率是pcap_findalldevs返回的网卡名在这台机器上变了,或者wpcap.dll版本不一致。解决方法是不要硬编码网卡名,让程序运行时列出网卡让用户选,或者按描述关键字自动匹配。另外pcap_freealldevs别忘了调,否则内存泄漏,长时间跑会出问题。

6. 进阶技巧:把扫描结果做成可复用的局域网资产表

基础版本跑通后,可以往上加两个实用功能。第一个是反查主机名,拿到 IP 后调用gethostbyaddr尝试解析 NetBIOS 或 DNS 名称,这样输出就不只是冷冰冰的 IP 和 MAC,而是「192.168.1.5 DESKTOP-ABC AA:BB:CC:DD:EE:FF」这种能直接看懂的表。第二个是定时重复扫描,把每次结果和上次对比,新出现的主机标出来,这对排查「谁偷偷连了我的局域网」很管用。

#include <winsock2.h> #pragma comment(lib, "ws2_32.lib") // 在拿到 IP 后反查主机名 struct hostent* he = gethostbyaddr((char*)arp->senderIp, 4, AF_INET); if (he) { printf("主机名: %s\n", he->h_name); } else { printf("主机名: 未知\n"); }

gethostbyaddr第一个参数是 IP 的字节数组指针,第二个是长度 4,第三个是地址族AF_INET。返回的hostent里h_name是主机名,解析不到就返回空。这个调用可能阻塞,建议放在单独线程里,别拖慢主扫描流程。用之前记得WSAStartup初始化 Winsock。

验证程序对不对,最可靠的方法是和arp -a的输出对照。你先跑自己的程序扫一遍,记下结果,然后在命令行执行arp -a,看两边列出的 IP 和 MAC 是否一致。注意arp -a显示的是缓存表,可能比你扫到的少,但凡是它列出来的,你的程序都应该能扫到,如果对不上,说明你的解析或发送有问题。另一个验证手段是开 Wireshark 抓包,过滤arp,看你的请求帧格式对不对,应答帧有没有回来。

参数调优上,我一般把发送间隔设成 1 毫秒,接收总超时设 3 秒,这个组合在 24 位掩码的局域网里能扫到九成以上的活动主机。如果网段更大,比如 16 位掩码,就得改成分段扫描,一次扫一个 C 段,否则广播风暴会把网络拖垮。这个边界要心里有数,别拿课程设计的代码去扫生产网。

最后说个习惯:每次改完代码,先用 Wireshark 抓一小段确认帧格式没变,再跑全量扫描。我吃过亏,有次改了个字段偏移,程序不报错但结果全错,扫了半小时才发现。先验证再放量,能省很多后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询