内网评估做到第五天的时候我在一台分析机上盯着抓包结果发愣整个办公网段里终端的默认网关 MAC 地址清一色指向了同一台不该出现在这个位置的设备。事后复盘才发现问题不在路由、不在 DNS、也不在什么高深的漏洞而是最古老、最没有防护的那一层——ARP。这件事之后我对内网评估的顺序做了调整先看二层再看三层及以上。因为二层一旦被“接管”上面所有基于 IP 的信任关系都变成了纸糊的。这篇内容聊的就是这个方向用 Python 去理解和验证 ARP 协议的信任模型以及在合法授权的实验环境里站在“中间位置”之后各类应用层协议到底会暴露什么、该怎么观测、又该怎么被发现。关键词是ARP 毒化、Scapy、二层转发、协议观测、检测与加固。适合有一定 Python 基础、正在做内网安全学习或内部风险评估的读者也适合负责内网运维、想知道自己网段到底有多脆弱的同学。所有操作都建议在完全隔离的虚拟实验环境里完成不要在任何生产网络上做验证——这一条不是客套话后面会专门讲为什么。1. ARP 为什么会“听话”二层寻址的信任模型拆解1.1 一次正常 ARP 交互的完整时序要理解 ARP 为什么容易被左右得先把它正常工作时的样子看清楚。主机 A 想给同网段的主机 B 发包它手里只有 B 的 IP没有 B 的 MAC而以太网帧头必须填目的 MAC 才能发出去。于是 A 在广播域内发一个 ARP 请求源 MAC 是自己的源 IP 是自己的目的 IP 是 B 的目的 MAC 填全 F 的广播地址。这个帧会被交换机泛洪到同 VLAN 的所有端口。B 收到后发现目的 IP 是自己于是回一个 ARP 应答源 MAC 是自己的源 IP 是自己的目的 MAC 是 A 的目的 IP 是 A 的。这个应答通常是单播回给 A 的。A 收到后把B的IP → B的MAC这条映射写进自己的 ARP 缓存然后才开始正常通信。整个过程有三个特征值得记住第一请求是广播的同一个广播域里谁都收得到包括不该收到的人第二应答是“自证”的B 说自己是 BA 没有任何办法独立验证第三协议里没有身份凭证、没有序列号、没有签名。ARP 诞生在一个人人互信的小型网络年代它的设计目标是把包发出去而不是防住谁会撒谎。1.2 ARP 缓存的三条容易被忽略的更新规则真正让事情变得微妙的是主机处理 ARP 缓存的几条默认规则。这些规则在很多文档里一句话带过但在实际验证中恰恰是决定成败的关键。第一条收到 ARP 应答时不校验自己是否发过对应的请求。也就是说你完全可以主动给一台从没跟你通信过的主机发一个应答它照样会收下并更新缓存。这类主动发出的应答在协议里叫免费 ARPGratuitous ARP本来是用来做地址冲突检测和缓存刷新的合法机制但它和伪造应答在报文结构上长得一模一样。第二条缓存条目会被后续应答无条件覆盖。没有“先来后到”的保护也没有“版本号更大才更新”的逻辑。谁最后说话谁就定义了映射关系。想维持住一条伪造的映射只需要周期性地重复发送把老化计时器一次次重置。第三条老化时间是有限的而且各系统差异很大。Linux 的邻居表有gc_stale_time之类的参数Windows 的可达性探测有随机化的时间窗口交换机侧的 CAM 表老化通常是几分钟量级。这意味着伪造的映射不会永久生效需要持续维持反过来排查的时候也不能只看某一瞬间的表项得看变化趋势。把这三条串起来看结论就很清楚ARP 缓存是一个无认证、可覆盖、有超时的键值存储而写入这个存储的唯一凭据就是“有人发了包”。这不是实现缺陷是协议本身的信任假设。1.3 为什么交换机不会替你拦住伪造应答很多人第一反应是交换机不是会学习 MAC 吗难道看不出异常答案是看不出因为交换机工作在二层它关心的是以太网帧头的源 MAC 和目的 MAC用来维护 CAM 表、决定从哪个端口转发。至于帧载荷里那 28 字节的 ARP 报文写了什么 IP、声称了哪种映射关系普通交换机默认是不解析的。所以一个伪造的 ARP 应答只要帧头的源 MAC 是真实的攻击机自己的网卡 MAC交换机就会老老实实学习、老老实实转发。整个过程在二层看来完全合法。要拦住它需要交换机具备解析 ARP 载荷并比对绑定关系的能力这就是后面要讲的动态 ARP 检查一类的机制默认配置下这道门是敞开的。还有一个细节常被忽略如果伪造的应答是广播发送的它会泛洪到整个 VLAN影响面一下就扩大到所有终端如果是单播精确发给某一台影响面小但隐蔽性更好。选择广播还是单播直接决定了这次验证的“噪音”有多大——在授权范围有限的评估里这一点必须提前想清楚因为广播方式很容易波及范围外的资产。2. 用 Python 把“欺骗”这件事拆成三个动作2.1 实验环境与依赖把不确定性先消灭掉在动手写代码之前环境比代码重要得多。我这几年踩过的坑里至少一半不是逻辑错而是环境没理干净。推荐的最小实验拓扑是一台虚拟化的宿主上面跑三台虚拟机——一台扮演“被影响终端”一台扮演“网关”一台作为“验证机”。三台接在同一个仅主机模式或内部模式的虚拟网络上不要桥接到物理网卡否则广播包会漏到你控制不了的网段里去。装依赖这一步很朴素Python 3 环境加 Scapy。Scapy 的价值在于它把报文当成 Python 对象来拼装字段名和 RFC 里的叫法基本对得上调试时可以直接show()出来逐字段看。比起手写字节数组用 Scapy 更像是“描述一个报文”而不是“构造一段内存”。开工前先确认三件事这三件事确认完后面 80% 的“跑不通”都能提前排除用ip addr或ipconfig看清验证机的网卡名和 IP用ip route确认默认网关的地址确实是你要模拟的那台确认三台机器互相能 ping 通且arp -a里能看到彼此的条目。如果这一步arp -a就是空的说明你压根没在同一个广播域里后面做什么都是白费。提示虚拟机网络模式里“仅主机”和“内部网络”是两种不同的隔离方式。前者通常仍带一个虚拟 DHCP后者更干净。做二层实验优先选后者能省掉很多“为什么网关自己冒出来了”的困惑。2.2 构造伪造应答从字段语义入手而不是背代码ARP 报文的核心就四个地址字段hwsrc发送方硬件地址、psrc发送方协议地址、hwdst目标硬件地址、pdst目标协议地址外加一个op字段表示是请求还是应答。理解这四个字段的语义比记住任何一行代码都有用。伪造应答的本质是让psrc填成你希望对方相信的那个 IP比如网关 IP而hwsrc填成你自己的网卡 MAC。这样对方收到后会在缓存里写下“网关 IP 对应攻击机 MAC”这条错误映射。用 Scapy 描述出来大致是这样from scapy.all import ARP, Ether, sendp def build_reply(fake_ip, victim_ip, victim_mac, my_mac): return Ether(dstvictim_mac, srcmy_mac) / ARP( op2, # 2 表示 ARP 应答 psrcfake_ip, # 冒充的身份 hwsrcmy_mac, # 真实发出者的硬件地址 pdstvictim_ip, hwdstvictim_mac, )这里有个层级概念需要说清楚Ether()是二层帧头ARP()是载荷两者用/拼接。发这种自定义二层帧要用sendp()而不是send()因为send()走的是三层、会重新做一次路由和 ARP 解析反而把你精心构造的帧头丢掉。另一个常被问到的点是为什么op要设成 2 而不是 1请求报文会触发对方回一个应答虽然很多系统在收到请求时也会顺便更新缓存但应答报文的影响更直接、副作用更小。做验证时优先用应答能减少不必要的广播噪音。至于向网关方向的那一半逻辑是镜像的psrc填被影响终端的 IPpdst填网关 IPhwdst填网关 MAC。两个方向都做才构成所谓“双向”的中间位置只做一半的话去程或回程会有一边走直连表现为“能连上但特别慢”或者“单向不通”——这个现象在排查时极具辨识度。2.3 转发开关与两个必须检查的内核参数把流量引到自己身上只是第一步接下来必须让自己转发这些流量否则终端会直接断网。断网是最糟糕的结果不仅验证失败还会立刻被人发现。开启转发的方式很简单把net.ipv4.ip_forward置为 1 即可但只改这一个参数往往不够。第一个坑是防火墙的 FORWARD 链。不少发行版默认把 FORWARD 链的策略设成 DROP此时内核虽然允许转发包却在 netfilter 那一层被丢掉。表现和没开转发一模一样但原因完全不同。排查的时候直接看iptables -L FORWARD -n -v的计数器有没有增长比猜要快得多。第二个坑是反向路径过滤。rp_filter会在收包时检查源地址是否能从收到该包的那张网卡回程路由出去不匹配就静默丢弃。在中间位置的场景下这个检查很容易误伤因为包的源地址确实不是本机的。把它调成宽松模式或者按网卡精细配置往往能一下解决“抓包能看到但转发不出去”的怪现象。还有一个容易被忽视的问题路由不对称。如果验证机有两张网卡、两张都在活动内核可能把转发出去的包从错误的接口送出导致回程断裂。做这类实验时我习惯把无关网卡先 down 掉只留一张在实验网段上宁可少点便利也别给自己制造玄学问题。2.4 收尾与恢复比开始更重要的一步这一段我想单独强调因为见过太多次“实验做完了网段里几台机器还在抽风”。伪造的映射会留在对方的缓存里直到老化期间所有流量还在往验证机跑。如果验证机这时候关机或者把转发关了那几台机器就真的断网了。正确的收尾动作是主动纠正把正确的映射关系重新发一遍重复若干次覆盖掉错误条目。具体做法就是构造和之前结构相同、但psrc和hwsrc都填真实值的应答包发给之前影响过的每一台设备。发三到五轮间隔一秒左右基本就能把缓存刷回来。我个人的习惯是在开始之前先把每台相关设备的原始 ARP 表项打印出来存成文本实验结束后逐条比对确认没有残留。这个动作花不了两分钟但能避免很多麻烦。任何涉及二层状态的操作都应该按“可回滚”来设计——这不是流程洁癖是让人能睡好觉的东西。3. 站在中间之后协议层能看到什么、能验证什么3.1 明文协议的可观测面DNS、HTTP、FTP 与 Telnet位置挪到中间之后最有价值的不是“能改什么”而是“能证明什么”。内网评估里真正需要向决策者说明的往往是风险的实际暴露程度而不是某段代码有多精巧。先说 DNS。DNS 查询默认走 UDP 53报文里的查询域名、查询类型、响应记录全是明文。一个中间位置可以完整看到某台终端最近访问了哪些内部域名、哪些外部域名、解析到了什么地址。从资产梳理的角度这些信息比端口扫描友好得多因为它是“真实发生的访问”而不是“可能的开放”。再说 HTTP。请求行里的方法、路径头部里的 Host、User-Agent、Cookie、Referer以及请求体里的表单字段全都是可读的。这里有个实操细节现代站点大量使用 HTTPS纯 HTTP 的比例在下降但内网系统是例外——很多内部管理后台、老旧的监控面板、打印机配置页仍然跑在 80 端口上而且往往还带着会话 Cookie。这才是这类验证真正的价值点。FTP 和 Telnet 更直接口令在协议里就是明文字段不需要任何破解。抓到一个 FTP 登录过程用户名和密码就在报文里躺着。这类协议现在公网基本绝迹但内网的网络设备管理、老旧的备份系统里还能碰到。做字段提取的时候用 Scapy 的sniff()加回调函数就够用了from scapy.all import sniff, TCP, IP, Raw def on_packet(pkt): if pkt.haslayer(Raw) and pkt.haslayer(TCP): payload pkt[Raw].load if bHTTP in payload or bHost: in payload: print(pkt[IP].src, -, pkt[IP].dst, payload[:200]) sniff(ifaceeth0, prnon_packet, filtertcp port 80, storeFalse)storeFalse这个参数要记得加否则所有包都会堆在内存里跑几分钟就能把机器吃满。filter用的是 BPF 语法在内核层就完成过滤比在 Python 回调里判断效率高一个量级。3.2 DHCP 与网关发现流程里的薄弱点如果说 ARP 是最容易被忽视的一层那 DHCP 就是第二容易被忽视的。DHCP 的交互是四步发现Discover广播、提供Offer、请求Request广播、确认Ack。整个过程没有认证客户端无法验证回应它的到底是不是真正的服务器。这里面有两个值得关注的点。第一客户端对第一个 Offer 往往照单全收谁能更快回应谁就更容易被采纳。第二DHCP 下发的选项里不仅有 IP 和掩码还有默认网关、DNS 服务器、域名后缀、WPAD 地址这些字段。这些字段一旦被替换终端的流量走向就整体改变了而且终端自己完全无感。从验证的角度这里要观察的不是“能不能发一个假的 Offer”而是“网段里到底有没有人在盯着这件事”。有些网络的接入交换机配了 DHCP Snooping会把非信任端口上的 Offer 直接丢弃有些网络完全没有这层配置任何一台接入设备都能扮演服务器。这两种情况的风险等级差得很远而这个差异只有实际去看配置才能知道。顺带说一个容易被忽略的观测点DHCP 报文里带主机名字段Option 12和厂商类别字段Option 60。做资产清单的时候从 DHCP 流量里提取这两项能快速得到一份“这个网段里有哪些设备、什么系统”的粗略画像比逐个扫描温和得多也准确得多。3.3 加密协议的边界拦不住的部分与要盯防的部分必须说清楚一件事站在中间不等于能看穿加密流量。TLS 的握手会校验证书链浏览器和现代客户端对证书异常的处理都很严格中间位置拿不到可信证书就没法完成握手。ARP 层面的位置改变改变的是流量路径不是加密强度。那这类位置对加密流量还有什么意义答案是元数据。即使内容加密你依然能看到连接的源和目的地址、端口、时间、持续时长、上下行字节数以及在 TLS 握手中以明文传输的服务器名称指示SNI字段。这些元数据拼起来足以回答“这台终端在跟哪些外部服务通信”“有没有在非工作时间连一些不该连的地方”这类问题。做内网合规检查时元数据分析往往比内容分析更实用因为它不碰隐私边界争议小、落地快。另一个需要盯防的是协议降级。如果终端第一次访问某站点走的是 HTTP被重定向到 HTTPS那这个重定向过程本身是明文的可以被观察甚至被改写。现代站点普遍启用了 HSTS浏览器会强制走 HTTPS这条路基本被堵死了但内网系统启用 HSTS 的比例很低。所以做评估时我会专门检查内网系统有没有下发 HSTS 头、有没有把 HTTP 端口彻底关掉。这个检查做起来很简单价值却不低。3.4 把观测做成工具抓包之外的工程化单次抓包谁都会难的是把观测变成可重复、可对比的流程。我在实际项目里通常会做三件事。第一件是分片存储。长时间抓包不要写成一个巨大的 pcap按时间或大小切分比如每分钟一个文件。这样后续分析时能快速定位某个时间点导出的文件也方便交付。第二件是结构化提取。抓到的包不要只留在 pcap 里把关心的字段抽出来写成 JSON 或者 CSV一行一个会话包含五元组、时间、字节数、提取到的关键字段。这样做的好处是可以直接用表格工具做排序、筛选、统计比在 Wireshark 里一条条点开高效得多。第三件是基线对比。同一个网段在正常状态和验证状态各抓一段对比协议分布、会话数量、域名集合的变化。很多时候异常不是单个包能看出来的而是分布上的偏移——比如某个平时只有几十个 DNS 查询的终端突然出现了几百个不同域名的查询这就很值得追下去。这三件事听起来平淡但它们决定了一份评估报告是“我抓到了几个包”还是“我能说清这个网段平时长什么样、异常时长什么样”。后者才是真正有说服力的东西。4. 检测视角怎么在日志和表项里发现“中间有人”4.1 从 ARP 表异常到 MAC 漂移的排查链路站在防守方最有用的技能不是构造报文而是从一堆正常数据里把异常挑出来。ARP 层面的异常有两个经典特征记住这两条排查效率会高很多。第一个特征是一个 MAC 对应多个 IP。正常情况下一台终端的网卡 MAC 只会对应它自己的那个 IP如果 ARP 表里出现同一个 MAC 同时对应网关、对应好几台服务器那基本可以确定有问题因为一块网卡不太可能同时是好几个不同地址的主人。第二个特征是网关的 MAC 变了。网关的 MAC 在很长时间内应该是稳定的一旦发现它指向了一台陌生的设备就要立刻警觉。排查的链路我一般这么走先在受影响的终端上把 ARP 表打出来看网关那条记录的 MAC 是什么再到核心交换机上查这个 MAC 是从哪个端口学到的然后对照资产台账看这个端口对应的物理位置是不是应该有这台设备。三步走完基本就能定位到具体的接入点。如果交换机支持查看 MAC 地址表的变动历史还能进一步确认这条记录是什么时候变的跟终端反馈的异常时间点对不对得上。这里有个实操上的坑有些异常是瞬时的。伪造的映射发出后几秒内就生效但如果你在几分钟后才去查可能看到的是已经恢复的正常状态。所以排查时要问清楚“症状出现的准确时间”并且尽可能在症状还在的时候抓现场。事后复盘靠日志事中定位靠现场两者不能互相替代。4.2 交换机侧的三道防线动态 ARP 检查、端口安全与 DHCP Snooping真正能从根本上压制这类问题的是接入交换机上的配置。这几项配置的价值排序我个人认为是DHCP Snooping 打底动态 ARP 检查DAI主防端口安全兜底。DHCP Snooping的作用是建立一张“IP 与 MAC 与端口”的绑定表把连接合法 DHCP 服务器的端口设为信任端口其余端口上收到的 DHCP Offer 一律丢弃。这张绑定表是后续所有检查的基础没有它动态 ARP 检查就没有比对依据。动态 ARP 检查DAI会逐条检查经过的 ARP 报文把报文里的源 IP 和源 MAC 拿去跟绑定表比对不匹配就丢弃。配好之后伪造的应答在接入层就进不来了效果非常直接。要提醒的是DAI 依赖 DHCP Snooping 的绑定表如果网段里有大量使用静态 IP 的设备需要额外手工配置静态绑定否则这些设备会被误伤——这是上线时最常见的翻车点。端口安全限制一个端口能学到的 MAC 数量超了就触发保护动作。它能挡住“一个端口后面挂了一台不断变化源 MAC 的设备”这种情况但对“用自己的真实 MAC 冒充别人的 IP”这种手法效果有限所以只能算兜底不能替代前两项。三项配置之间的关系我用一张表来对照会更清楚机制拦的是什么依赖条件常见误伤DHCP Snooping非法 DHCP 服务器下发的地址与网关信息需明确划分信任端口级联端口漏配导致合法服务器被拦动态 ARP 检查源 IP 与 MAC 不匹配的 ARP 报文依赖 Snooping 绑定表静态 IP 设备未配绑定而被丢包端口安全单端口 MAC 数量异常增长需设定 MAC 数量上限与保护动作上限设太小导致正常终端掉线4.3 终端侧的低成本加固与一次完整的排查复盘交换机配置不是所有环境都能动很多时候只能在终端侧做文章。低成本的几条把网关的 ARP 条目做成静态绑定这样伪造的应答就覆盖不了缩短 ARP 缓存的老化时间让异常状态存在的时间窗口变小在关键服务器上配置双向静态绑定把自己和网关的映射都钉死。静态绑定的代价是维护成本。设备一换、网关一迁移就得手工改一遍容易忘。所以我的建议是只对少数关键资产做静态绑定其余靠交换机和监控来兜底。说一个我印象比较深的排查过程。某个工作日的中午陆续有同事反馈“访问内部系统特别慢但网页能打开”。第一波排查查了出口带宽、查了服务器负载、查了 DNS 响应时间都正常。第二波才想到看二层在受影响最集中的一段网络上抓了几分钟包发现大量重复的 ARP 应答源 MAC 指向一台刚接入的测试设备。查交换机的 MAC 地址表确认那台设备的接入端口在某个工位。到工位一看是有人在跑一个网络相关的实验脚本脚本没做清理就跑去吃饭了。整个过程从第一次有人反馈到定位花了将近一个小时其中四十分钟耗在“往上层找原因”上。这个教训我记了很久当症状表现为“能通但慢”“时通时不通”“特定网段集中出现”时优先怀疑二层。因为上层的问题通常是确定的——要么通要么不通很少呈现出这种弥漫性、间歇性的特征。5. 实验环境里的边界感与工程习惯5.1 授权范围、时间窗与可回滚设计技术本身是中性的但做技术的人需要边界感。这些操作的影响范围是整个广播域而不是你指定的那一台机器——一个广播的伪造应答会让同 VLAN 里所有设备都受波及。所以在任何真实环境里做验证第一件事是确认三件事范围哪些网段、哪些资产、时间窗什么时段做、谁在旁边看着、回滚方案出问题怎么恢复、多久能恢复。范围这件事要写得具体。“办公网”不是一个范围“192.0.2.0/24 网段内的 3 台测试终端”才是一个范围。时间窗也一样“晚上做”不如“22:00 到 23:00 之间中间有人值守”。回滚方案必须提前演练过——不是写在文档里就算数得真的在实验环境里试一遍恢复流程确认能在几分钟内把缓存刷回来。还有一点是我个人的坚持关键操作要有第二个人在场。不是为了监督是为了在出问题时有人能冷静地判断“现在该恢复还是该继续抓现场”。一个人操作时容易陷入“再抓一个包就好”的状态判断力会下降。5.2 跑不通的时候先看这几条这类实验的失败原因高度集中我把常见情况整理成一张表遇到问题按顺序过一遍能省掉大量时间现象最可能的原因快速验证方式目标完全断网未开启转发或 FORWARD 链策略为丢弃查看转发开关与 FORWARD 链计数抓得到包但转发不出去反向路径过滤丢弃或路由不对称临时放宽过滤策略后复测目标毫无变化不在同一广播域或虚拟网络模式选错检查双方 ARP 表是否可见只影响单向只做了一侧映射补齐反方向的映射再测一开始生效后来失效缓存老化未持续维持观察映射条目的存活时间设备根本不响应目标已配置静态绑定查看目标 ARP 表条目类型表格里最后一条值得多说两句。静态绑定是终端侧最简单也最有效的防护一旦配上任何伪造应答都覆盖不了。做验证时如果发现怎么发都没反应先别怀疑代码去看看目标是不是静态绑定——这个排查顺序能省下大量自我怀疑的时间。5.3 把一次实验沉淀成检查清单我习惯每次做完这类验证都把过程压缩成一份检查清单下次直接照着走。清单大概长这样确认实验网络完全隔离记录所有相关设备的原始 ARP 表项确认转发开关与防火墙策略确认反向路径过滤已调整确认验证机只有一张活动网卡在实验网段准备好恢复脚本实验结束后逐条比对 ARP 表确认无残留最后关闭实验环境。这份清单看着琐碎但它把“靠记忆”变成了“靠流程”。人在连续操作几个小时后判断力会下降这时候清单比经验可靠。最后分享一个我自己的心得这类实验真正的收获往往不在“成功做到了”而在“发现了哪一步比想象中脆弱”。比如我第一次做的时候最意外的不是伪造应答能生效而是发现网段里居然没有任何一层在做校验——没有 Snooping没有 DAI终端全是动态绑定。那个“什么都没有”的发现比任何技术细节都更值得写进报告里。评估的价值就在于把这种“默认的空白”变成看得见的东西然后一项一项补上。