ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinPcap与C++实现ARP局域网物理地址扫描实战

WinPcap与C++实现ARP局域网物理地址扫描实战 简介这份资源是广东工业大学计算机网络课程设计的完整项目文档面向正在学习网络协议或需要完成同类课设的计算机专业学生。内容围绕使用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而不是系统 API2.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 Developers 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 和本机 IPtargetIp填你要探测的那个 IPtargetMac全填 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::mapuint32_t, std::arrayuint8_t,6key 是 IP 转成的 32 位整数value 是 MAC 数组插入时天然去重。最后遍历 map 输出顺便按 IP 排序。如果要交课程设计报告把结果写进一个文本文件格式一行一个「IP MAC」方便贴进文档。#include map #include array #include fstream std::mapuint32_t, std::arrayuint8_t,6 result; // 在解析到应答后 uint32_t ipKey (arp-senderIp[0] 24) | (arp-senderIp[1] 16) | (arp-senderIp[2] 8) | arp-senderIp[3]; std::arrayuint8_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 而不是 WinPcapNpcap 的头文件路径不同需要单独下载 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 抓一小段确认帧格式没变再跑全量扫描。我吃过亏有次改了个字段偏移程序不报错但结果全错扫了半小时才发现。先验证再放量能省很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表