ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++与Winpcap实战:ARP帧构造与局域网活动主机扫描

C++与Winpcap实战:ARP帧构造与局域网活动主机扫描 简介这份PDF是广东工业大学计算机网络课程设计的完整项目文档面向计算机相关专业学生及C网络编程初学者围绕ARP协议实现局域网活动主机物理地址获取这一经典课题展开。文档系统梳理了ARP工作原理、以太网帧与ARP帧结构、Winpcap库的发送与接收流程并给出Visual C环境下的代码实现与运行结果分析帮助读者理解网络层与数据链路层的地址解析交互。资源包内仅含1个PDF文件大小约458KB内容涵盖设计题目、技术参数、实现步骤、网络基础理论、代码分析及结果展示结构完整可直接作为课程设计参考或网络协议学习材料。目前已有169人学习适合需要完成同类课设或希望动手实践ARP抓包与帧构造的读者参考借鉴。1. 从一份广工课设说起用 C 和 Winpcap 把局域网里的活动主机“点名”很多人第一次接触 ARP是在课本上背“广播请求、单播应答”那几句话考完就忘。真正让我把 ARP 记住的是当年做广工那份计算机网络课程设计——用 C 配合 Winpcap自己构造 ARP 帧把局域网里活着的主机一台台“点名”出来拿到它们的 IP 和 MAC 对应关系。这份资源就是那份课设的完整实现包含帧结构定义、Winpcap 收发、多线程扫描和结果解析。它解决的不是“ARP 是什么”而是“ARP 帧到底长什么样、怎么用代码发出去、怎么把回来的包拆开”。适合正在做网络课设、想搞懂数据链路层抓包、或者想自己写一个局域网 IP 扫描工具的人。下面我按当年踩过的顺序把这份代码拆开讲一遍。2. 先立住原理ARP 帧的字节布局与 Winpcap 选型理由2.1 为什么必须用 Winpcap而不是普通 socket普通 socket 编程工作在传输层你拿到的是 TCP/UDP 的数据操作系统已经把以太网帧头和 ARP 层剥掉了。而 ARP 是网络层和数据链路层之间的协议它的报文直接封装在以太网帧里类型字段是 0x0806。想自己构造并发送这种帧就必须绕过协议栈直接操作网卡。Winpcap 提供的pcap_sendpacket和pcap_next_ex就是干这个的前者把一段原始字节流从指定网卡发出去后者从网卡抓原始帧回来。选它而不是 Npcap 或 raw socket是因为这份课设的环境是 Visual CWinpcap 的pcap_findalldevs_ex、PCAP_SRC_IF_STRING这套 API 在 Windows 上资料最多课设答辩时老师也认。常见做法是装 Winpcap 4.1.3 驱动开发时链接wpcap.lib和Packet.lib把pcap.h和Packet32.h的包含路径配好。2.2 28 字节 ARP 帧 14 字节以太网头一个字节都不能错这份代码里定义了两个结构体ethernet_head和arp_head然后用#pragma pack(1)强制按 1 字节对齐。这是整个项目最容易翻车的地方。C 默认会对结构体做内存对齐unsigned long会按 4 字节边界放结果你memcpy出来的字节流和协议规定的布局对不上发出去的包对方根本不认。#pragma pack(1)就是关掉这个对齐让结构体紧凑排列。#pragma pack(1) // 关键按1字节对齐保证结构体内存布局与协议字节流一致 // 14字节以太网帧头 struct ethernet_head { unsigned char dest_mac_add[6]; // 目的MAC广播时为 ff ff ff ff ff ff unsigned char source_mac_add[6]; // 源MAC unsigned short type; // 帧类型ARP 为 0x0806 }; // 28字节ARP报文 struct arp_head { unsigned short hardware_type; // 硬件类型以太网为 1 unsigned short protocol_type; // 协议类型IP 为 0x0800 unsigned char hardware_add_len; // 硬件地址长度MAC 为 6 unsigned char protocol_add_len; // 协议地址长度IP 为 4 unsigned short operation_field; // 操作码请求1应答2 unsigned char source_mac_add[6]; // 发送端MAC unsigned long source_ip_add; // 发送端IP unsigned char dest_mac_add[6]; // 目的端MAC请求时填 0 unsigned long dest_ip_add; // 目的端IP }; // 最终发送的42字节包 struct arp_packet { struct ethernet_head ed; struct arp_head ah; };参数说明hardware_type填 1 表示以太网protocol_type填 0x0800 表示上层是 IP两个长度字段固定 6 和 4operation_field请求填 1、应答填 2。所有多字节字段在发送前都要用htons或htonl转成网络字节序因为 x86 是小端网络协议是大端。这份代码里eh.type htons(ETH_ARP)、ah.hardware_type htons(ARP_HARDWARE)就是在做这件事。漏掉任何一个htons抓包看字段就是反的。2.3 广播请求、单播应答以及那个“随便设”的源 IPARP 请求的目的 MAC 是全ff也就是广播交换机收到后会转发给同一广播域内所有主机。目的 IP 填你要探测的那个地址。应答回来时目的 MAC 是你的网卡源 MAC 是对方网卡操作码变成 2。代码里GetSelfMac函数有个细节它把请求方的源 IP 设成了100.100.100.100注释写着“随便设的”。这是为了在还没拿到自己 IP 的情况下先探自己的 MAC——发一个目的 IP 等于自己 IP 的请求看谁应答从应答包里取源 MAC。这个技巧在课设里能用但要注意如果局域网里真有人用 100.100.100.100 这个地址就会串。我一般会改成用pcap_findalldevs_ex拿到的真实 IP更稳。3. 动手复现从选网卡到多线程扫描的完整链路3.1 环境搭建与网卡枚举先把 Winpcap 开发包下下来解压后把Include和Lib目录配到 Visual C 的项目属性里。链接器输入加上wpcap.lib和Packet.lib。然后就是枚举网卡pcap_if_t *alldevs; char errbuf[PCAP_ERRBUF_SIZE]; // 获取本机所有网卡设备列表 if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, alldevs, errbuf) -1) { fprintf(stderr, Error in pcap_findalldevs: %s\n, errbuf); exit(1); } // 打印列表供用户选择 int i 0; for (pcap_if_t *d alldevs; d; d d-next) { printf(%d) %s\n, i, d-name); if (d-description) printf( (%s)\n, d-description); }逻辑说明pcap_findalldevs_ex返回一个链表每个节点代表一块网卡d-name是设备名形如rpcap://...或\Device\NPF_{GUID}d-description是人类可读的描述。用户输入编号后用循环跳到对应节点再调pcap_open打开。pcap_open的第二个参数 65536 是捕获缓冲区大小保证能收全每个帧第三个参数PCAP_OPENFLAG_PROMISCUOUS开混杂模式这样网卡不只收发给自己的帧广播帧和别人的帧也能抓到第四个参数 1000 是读取超时毫秒数。3.2 构造并发送 ARP 请求帧打开网卡后先拿到自己的 IP、掩码和 MAC。ifget函数遍历d-addresses找AF_INET家族用iptos把 32 位整数转成点分十进制字符串。然后GetSelfMac发一个探针包拿自己的 MAC。拿到这些之后就可以进入扫描线程了。DWORD WINAPI SendArpPacket(LPVOID lpParameter) { sparam *spara (sparam *)lpParameter; pcap_t *adhandle spara-adhandle; char *ip spara-ip; unsigned char *mac spara-mac; char *netmask spara-netmask; unsigned char sendbuf[42]; struct ethernet_head eh; struct arp_head ah; memset(eh.dest_mac_add, 0xff, 6); // 目的MAC全ff广播 memcpy(eh.source_mac_add, mac, 6); // 源MAC是本机 memcpy(ah.source_mac_add, mac, 6); memset(ah.dest_mac_add, 0x00, 6); // 请求时目的MAC填0 eh.type htons(ETH_ARP); ah.hardware_type htons(ARP_HARDWARE); ah.protocol_type htons(ETH_IP); ah.hardware_add_len 6; ah.protocol_add_len 4; ah.source_ip_add inet_addr(ip); // 源IP是本机IP ah.operation_field htons(ARP_REQUEST); // 根据本机IP和掩码算出网段起始地址 unsigned long myip inet_addr(ip); unsigned long mynetmask inet_addr(netmask); unsigned long hisip htonl((myip mynetmask)); // 遍历网段内所有可能的主机地址 for (int i 0; i HOSTNUM; i) { ah.dest_ip_add htonl(hisip i); memset(sendbuf, 0, sizeof(sendbuf)); memcpy(sendbuf, eh, sizeof(eh)); memcpy(sendbuf sizeof(eh), ah, sizeof(ah)); pcap_sendpacket(adhandle, sendbuf, 42); Sleep(50); // 每发一个等50ms避免瞬间打爆网络 } Sleep(1000); flag TRUE; // 通知接收线程扫描结束 return 0; }逻辑说明myip mynetmask得到网络号htonl转回主机字节序后加i再htonl转回网络序填进目的 IP。HOSTNUM定义为 255覆盖一个 C 类网段。Sleep(50)是节流255 个包大概 12 秒发完。如果不加这个延时有些老交换机或无线网卡会丢包导致扫描结果不全。flag是一个全局布尔量发送线程结束后置TRUE接收线程看到就退出循环。3.3 接收应答并解析 IP-MAC 映射接收线程GetLivePC在一个while(true)里不断调pcap_next_ex抓包抓到后先看以太网类型是不是 0x0806再看 ARP 操作码是不是 2应答。DWORD WINAPI GetLivePC(LPVOID lpParameter) { gparam *gpara (gparam *)lpParameter; pcap_t *adhandle gpara-adhandle; struct pcap_pkthdr *pkt_header; const u_char *pkt_data; while (true) { if (flag) { // 发送线程已完成 printf(扫描完毕按任意键退出!\n); break; } if (pcap_next_ex(adhandle, pkt_header, pkt_data) 0) { // 以太网类型偏移12字节ARP操作码偏移20字节 if (*(unsigned short *)(pkt_data 12) htons(ETH_ARP)) { struct arp_packet *recv (arp_packet *)pkt_data; if (*(unsigned short *)(pkt_data 20) htons(ARP_REPLY)) { printf(IP:%d.%d.%d.%d MAC:, recv-ah.source_ip_add 255, recv-ah.source_ip_add 8 255, recv-ah.source_ip_add 16 255, recv-ah.source_ip_add 24 255); for (int i 0; i 6; i) { printf(%02x, pkt_data[22 i]); } printf(\n); } } } Sleep(10); } return 0; }逻辑说明pkt_data指向原始帧的起始。偏移 12 字节是帧类型字段偏移 20 字节是 ARP 操作码偏移 22 字节开始是发送端 MAC偏移 28 字节是发送端 IP。这里直接按偏移取值是因为结构体已经pack(1)对齐recv-ah.source_ip_add和pkt_data28是同一个位置。IP 的打印用了移位和掩码因为source_ip_add是网络字节序低地址字节对应 IP 的第一段。MAC 用%02x逐字节打印保证两位十六进制。4. 避坑与排查那些年 ARP 课设翻过的车4.1 现象程序能编译但一个应答都收不到原因通常有三个。第一网卡选错了。笔记本上常有虚拟网卡VMware、VirtualBox和无线网卡如果你选了虚拟网卡广播包发不到真实局域网。解决方法是看d-description选带“Realtek”“Intel”“Wireless”字样的物理网卡。第二防火墙拦了。Windows 防火墙有时会拦截原始帧的收发临时关掉防火墙再试。第三pcap_open没开混杂模式或者超时设得太短。确认第三个参数是PCAP_OPENFLAG_PROMISCUOUS超时给 1000 毫秒。4.2 现象抓包能看到请求但解析出来的 IP 是反的这是字节序问题。inet_addr返回的已经是网络字节序你直接打印它的十进制值在 x86 上就是反的。代码里用 255、 8 255这种方式逐字节取就是手动做字节序转换。如果你用inet_ntoa它接受的是struct in_addr也要注意传入的字节序。我一般会在解析前统一用ntohl转成主机序再处理不容易乱。4.3 现象扫描结果里出现了自己的 IP 和自己的 MAC正常。你发的请求里源 IP 是自己的有些系统或某些网卡驱动会对自己发的广播做一个应答或者交换机回环。代码里没有过滤自己所以自己的 IP 也会出现在结果里。如果不想要在打印前加一个判断如果源 IP 等于本机 IP跳过。4.4 现象pcap_sendpacket返回 -1GetLastError报错常见原因是网卡句柄无效或者 Winpcap 驱动没装好。先确认pcap_open返回的不是 NULL。如果返回 NULL看errbuf里的错误信息。另一个原因是权限不够Winpcap 需要管理员权限才能发原始帧用管理员身份运行 Visual Studio 或编译出的 exe。4.5 现象扫描到一半程序卡死多半是发送线程和接收线程共用了一个pcap_t句柄而 Winpcap 的句柄不是线程安全的。这份代码里两个线程都用了adhandle在单核或低负载下能跑但并发高时可能出问题。稳妥做法是发送和接收各开一个pcap_open或者加锁。课设演示时如果卡死先看是不是这个原因。5. 进阶技巧把扫描结果落盘并做 ARP 缓存对照跑通之后我习惯把结果存成 CSV方便和系统 ARP 缓存对照。系统缓存用arp -a就能看但那是操作系统维护的可能有过期项。自己扫出来的才是当前活着的。// 在打印应答的地方追加文件写入 FILE *fp fopen(arp_scan_result.csv, a); if (fp) { fprintf(fp, %d.%d.%d.%d,%02x:%02x:%02x:%02x:%02x:%02x\n, recv-ah.source_ip_add 255, recv-ah.source_ip_add 8 255, recv-ah.source_ip_add 16 255, recv-ah.source_ip_add 24 255, pkt_data[22], pkt_data[23], pkt_data[24], pkt_data[25], pkt_data[26], pkt_data[27]); fclose(fp); }参数说明以追加模式打开每次应答写一行IP 在前 MAC 在后逗号分隔。跑完用 Excel 打开和arp -a的输出并排看能发现哪些主机在系统缓存里但已经下线哪些在线但缓存里没有。这个对照表在课设答辩时很加分能说明你不仅会发包还理解 ARP 缓存的生命周期。另一个技巧是控制扫描速度。Sleep(50)是保守值千兆局域网可以降到 10 毫秒但无线网络建议保持 50 以上。如果目标网段是 192.168.1.0/24HOSTNUM设 255 就够如果是 /16得改成 65535但那样扫描时间会很长通常只扫已知的几个网段。我一般会先ping一下网关确认网络通再跑扫描避免对着一个没连上的网卡空发。从那以后我每次做网络层实验都强制先pcap_findalldevs_ex把网卡列表打出来确认再动手写收发逻辑这个习惯帮我省了至少三次“选错网卡白忙一晚上”的血泪。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进