
摘要
本文围绕DHCP 欺骗(DHCP spoofing)这一网络威胁,给出一个贴近实战、以被动检测 + 警报/记录 为目标的工具设计与实现。文章先用口语化的方式把场景讲清楚,再给出一个完整的 Python 实现(基于 Scapy),并对每个模块、每段代码逐行解释,最后用示例演示如何运行与解读输出、并分析时间/空间复杂度与实用性。强调:本文只给出防御/检测工具;任何主动利用 DHCP 伪造去攻击他人的行为都是不被支持的。
描述
在很多企业或校园网络里,终端通常通过 DHCP 自动获得 IP、网关(默认路由)、DNS 等信息。一旦网络中出现伪造 DHCP 服务器,它就能把"默认网关"设成攻击者的主机,从而把本应发往外网的数据先发给攻击者------攻击者可以监听、篡改或转发这些流量(中间人)。这个攻击现实中常见的场景包括:
- 公共场所(学校食堂、会议中心、商场)的无线接入点旁边有人插入了恶意设备。
- 公司分支网络没有物理隔离或端口安全措施,某个插槽被人接入一台伪造 DHCP 服务器。
- 网络部署复杂、管理员未维护可信 DHCP 列表,导致终端随机接受第一个 OFFER。
现实中,我们希望的是:既不需要主动更改网络中的 DHCP 流量,也能尽早发现网络中是否存在可疑/冲突的 DHCP OFFER,从而报警、记录并通知管理员采取人工检查或自动化隔离(在网络设备上)。
因此本文实现一个实用的工具 dhcp_watchdog.py,它的功能包括:
- 被动监听局域网内的 DHCP/BOOTP 报文(DISCOVER/OFFER/REQUEST/ACK)。
- 解析每个 OFFER 中的关键选项(提供者硬件地址 / DHCP Server Identifier / Router(网关)/ lease time / DNS 等)。
- 在短时间内检测到同一子网/同一请求客户端收到来自多个不同 DHCP 服务器并提供不同默认网关时,判定为"可能的 DHCP 欺骗"并触发警报。
- 将警报写入日志文件、可选择发送邮件或通过 HTTP webhook(伪代码/接口留空,示例展示如何扩展)。
- 支持配置"可信 DHCP 服务器列表"(白名单)以减少误报。
下面给出设计答案与完整代码,然后逐块讲解。
题解答案
-
工具名:
dhcp_watchdog.py -
运行方式:在需要监控的网段上以 root 权限运行(因为需要抓包):
sudo python3 dhcp_watchdog.py -i eth0 -
主要依赖:
scapy(用于抓包与解析 DHCP),argparse、logging、threading、json。 -
工作流程:
- 启动后在指定接口上被动抓取 UDP 67/68(DHCP)的数据包。
- 对 OFFER 包(及含 server identifier 的包)提取:DHCP Server Identifier(IP)、你的目标客户端 MAC/IP(yiaddr/ciaddr)、Router(默认网关)等。
- 用短时窗口(例如 30 秒)为单位聚合来自不同 DHCP server 的 OFFER,若对同一客户端出现多个不同 "默认网关" 提供且提供服务器不在白名单时,生成警报。
- 日志记录所有可疑事件,输出到控制台并写文件。可选:扩展 webhook/邮件告警。
题解代码
注意:运行前请在具有抓包权限的环境中安装 scapy:
pip3 install scapy。出于安全与合规考虑,本工具仅用于检测/防御,不包含任何攻击或伪造 DHCP 响应的代码。
python
#!/usr/bin/env python3
# dhcp_watchdog.py
"""
被动 DHCP 欺骗检测工具(只读/检测)
运行: sudo python3 dhcp_watchdog.py -i <iface> --window 30 --threshold 2
"""
import argparse
import logging
import threading
import time
import json
from collections import defaultdict, deque
from datetime import datetime
from scapy.all import sniff, BOOTP, DHCP, UDP, IP, Ether
# ----------------------
# 配置与日志
# ----------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler("dhcp_watchdog.log", encoding="utf-8")
]
)
# ----------------------
# 全局参数
# ----------------------
# 默认短时窗口(秒),在该时间窗口内聚合 OFFER
DEFAULT_WINDOW = 30
# 当同一客户端在窗口内被 >= threshold 个不同 server 提供不同默认网关则报警
DEFAULT_THRESHOLD = 2
# ----------------------
# 工具函数:解析 DHCP options
# ----------------------
def parse_dhcp_options(dhcp_options):
"""
把 scapy 的 DHCP options 列表转换成 dict 方便使用
scapy 把 options 抽成像 [('message-type', 2), ('server_id', '192.168.1.1'), ... , 'end']
"""
opts = {}
for o in dhcp_options:
if isinstance(o, tuple) and len(o) >= 2:
key, val = o[0], o[1]
opts[key] = val
return opts
# ----------------------
# DHCP 事件存储结构
# ----------------------
class EventWindow:
"""
基于短时滑动窗口收集 DHCP OFFER 信息。
存储结构:
key = client_mac (str, 小写)
value = deque of (timestamp, server_ip, offered_gateway_list)
"""
def __init__(self, window_seconds=DEFAULT_WINDOW):
self.window = window_seconds
self.lock = threading.Lock()
self.data = defaultdict(deque)
def add_offer(self, client_mac, server_ip, router_list, raw_packet_summary):
"""
添加一条 OFFER 记录
router_list: list of router IPs (可能为空)
raw_packet_summary: 简要字符串描述包(便于日志记录)
"""
ts = time.time()
entry = (ts, server_ip, tuple(router_list), raw_packet_summary)
with self.lock:
q = self.data[client_mac]
q.append(entry)
# 清理过期
while q and (ts - q[0][0] > self.window):
q.popleft()
def snapshot(self, client_mac):
with self.lock:
return list(self.data.get(client_mac, []))
def clients(self):
with self.lock:
return list(self.data.keys())
# ----------------------
# 检测器
# ----------------------
class RogueDetector:
def __init__(self, window_seconds=DEFAULT_WINDOW, threshold=DEFAULT_THRESHOLD, trusted_servers=None):
self.window_seconds = window_seconds
self.threshold = threshold
self.window = EventWindow(window_seconds)
self.trusted = set(trusted_servers or [])
def process_offer(self, client_mac, server_ip, router_list, raw_summary):
"""
每收到一个 OFFER 调用一次
"""
self.window.add_offer(client_mac, server_ip, router_list, raw_summary)
# 做检测
self._analyze(client_mac)
def _analyze(self, client_mac):
"""
分析 client_mac 在窗口内的所有提供记录
如果发现 >= threshold 个不同 server 且这些 server 提供的 router 有差异并且 server 不都是可信的 -> 报警
"""
records = self.window.snapshot(client_mac)
if not records:
return
# 收集不同 server 提供的 router 集合
server_to_routers = {}
servers_seen = set()
for ts, server_ip, routers, summary in records:
servers_seen.add(server_ip)
# routers 是 tuple
server_to_routers.setdefault(server_ip, set()).update(routers)
# 如果可见 server 数量不达阈值,暂不报警
if len(servers_seen) < self.threshold:
return
# 如果所有 server 都在可信白名单中,则忽略
untrusted_servers = [s for s in servers_seen if s not in self.trusted]
if not untrusted_servers:
return
# 判断 router 值是否出现冲突(不同 server 提供的 router 集合不完全相同)
unique_router_sets = set()
for s, rset in server_to_routers.items():
# 将 rset 转成排序的元组便于比较
unique_router_sets.add(tuple(sorted(rset)))
if len(unique_router_sets) <= 1:
# 所有 server 提供的 router 集合一致,误报可能性大
return
# 满足条件:触发报警
self._alert(client_mac, servers_seen, server_to_routers, records)
def _alert(self, client_mac, servers_seen, server_to_routers, records):
alert = {
"time": datetime.utcnow().isoformat() + "Z",
"client_mac": client_mac,
"servers": list(servers_seen),
"server_router_map": {s: list(r) for s, r in server_to_routers.items()},
"recent_records": [
{"ts": datetime.utcfromtimestamp(ts).isoformat()+"Z", "server": s, "routers": list(r), "summary": summary}
for ts, s, r, summary in records
]
}
# 写日志与输出
logging.warning("可能的 DHCP 欺骗检测到: %s", json.dumps(alert, ensure_ascii=False))
# 这里可以扩展:发送 webhook / 邮件等
# send_webhook(alert) # 留空,交由运维根据需要实现
# ----------------------
# 抓包回调:解析并调用检测逻辑
# ----------------------
def dhcp_packet_callback(pkt, detector):
# 只关心 UDP 67/68 的 DHCP 包
if not (pkt.haslayer(UDP) and pkt.haslayer(BOOTP) and pkt.haslayer(DHCP)):
return
bootp = pkt[BOOTP]
dhcp = pkt[DHCP]
# 客户端 MAC(使用 chaddr)
client_mac = ":".join("{:02x}".format(x) for x in bootp.chaddr[:6]).lower()
dhcp_opts = parse_dhcp_options(dhcp.options)
msg_type = dhcp_opts.get("message-type")
server_id = dhcp_opts.get("server_id") # DHCP Server Identifier, IP
router = dhcp_opts.get("router") # 可能是单个 IP 或列表
# scapy 有时把 router 返回为单个字符串或单个元素的 list
router_list = []
if router:
if isinstance(router, list) or isinstance(router, tuple):
router_list = list(router)
else:
router_list = [router]
# 只处理 OFFER (2) 和 ACK (5) 和 OFFER-like server responses(为了检测)
if msg_type in (2, 5): # 2: OFFER, 5: ACK
summary = f"msg_type={msg_type} server_id={server_id} yiaddr={bootp.yiaddr} routers={router_list}"
detector.process_offer(client_mac, server_id or pkt[IP].src, router_list, summary)
# ----------------------
# 主函数:入口与参数
# ----------------------
def main():
parser = argparse.ArgumentParser(description="被动 DHCP 欺骗检测 (只读)")
parser.add_argument("-i", "--iface", required=True, help="监听接口,例如 eth0")
parser.add_argument("--window", type=int, default=DEFAULT_WINDOW, help="短时窗口(秒),默认30")
parser.add_argument("--threshold", type=int, default=DEFAULT_THRESHOLD, help="触发报警的不同 server 数量阈值,默认2")
parser.add_argument("--trusted", type=str, default="", help="可信 DHCP server 列表,逗号分隔的 IP")
args = parser.parse_args()
trusted_list = [ip.strip() for ip in args.trusted.split(",") if ip.strip()]
detector = RogueDetector(window_seconds=args.window, threshold=args.threshold, trusted_servers=trusted_list)
logging.info("启动 DHCP Watchdog,在接口 %s 上监听 (window=%ds, threshold=%d)", args.iface, args.window, args.threshold)
if trusted_list:
logging.info("可信 DHCP 服务器白名单: %s", trusted_list)
# 使用 scapy 抓包(过滤 UDP 67/68)
bpf = "udp and (port 67 or port 68)"
sniff(iface=args.iface, filter=bpf, prn=lambda pkt: dhcp_packet_callback(pkt, detector), store=False)
if __name__ == "__main__":
main()
题解代码分析
下面我们把代码拆成模块来逐段解释,帮助你读懂每一步在干什么,并指出为什么这样设计。
依赖与日志配置
python
from scapy.all import sniff, BOOTP, DHCP, UDP, IP, Ether
logging.basicConfig(...)
- 使用
scapy来抓包并解析 DHCP 报文(BOOTP/DHCP 是同一协议族)。 logging配置了控制台和文件输出,文件名为dhcp_watchdog.log,便于长期审计。
parse_dhcp_options
python
def parse_dhcp_options(dhcp_options):
...
- Scapy 将 DHCP options 解析成列表(含
'end'等),函数把它转换为字典以便按 key 访问(比如message-type,server_id,router)。
EventWindow
- 目的:在一个短时间窗口(默认 30 秒)内记下来自不同 DHCP servers 对同一客户端的 OFFER。
- 用
defaultdict(deque)存储:deque 有快速从左端弹出旧记录的能力,适合滑动窗口。 add_offer用时间戳记录每条 OFFER 并清理过期记录。
设计理由:DHCP 欺骗可表现为在极短时间里有多个不同的 DHCP server 提供不同的配置,因此短时聚合是合理的检测方法。
RogueDetector
-
初始化时可以给出
trusted_servers(白名单),避免已知合法的 backup DHCP 服务器引起误报。 -
process_offer:对每个 OFFER 调用add_offer并触发_analyze。 -
_analyze的判定规则(相对保守):- 统计在窗口内出现的不同 server 数量,若小于阈值则不报警(避免偶发)。
- 从白名单中剔除可信服务器,若剩余无 server 则不报警。
- 判断不同 server 提供的
router(默认网关)集合是否不一致,如果一致(即所有 server 都给出相同网关),不报警(可能是冗余 DHCP 服务的合法情况)。 - 否则生成警报并写日志(包含最近记录详情)。
-
这样既可探测恶意伪造(提供不同网关),也能尽量避免误报。
为什么不直接对"不同 server 出现"就报警?因为在一个大网络里可能存在容错配置或多台 DHCP server 为同一子网服务并且配置一致:我们只在"不同 server 提供不同 router(默认网关)"时提高警告级别。
抓包回调 dhcp_packet_callback
- 只处理含有
UDP、BOOTP、DHCP的包。 - 取出
client_mac(BOOTP.chaddr)、message-type(是否是 OFFER/ACK)、server_id(DHCP option: server_id)、router(DHCP option: router)。 - 对于 OFFER (message-type==2) 与 ACK (5) 触发
detector.process_offer。我们把 ACK 也纳入检测是因为有些设备在短时间内也可能收到 ACK,从检测角度看也有价值。
注意:chaddr 是原始 bytes,代码把前 6 个字节格式化为 MAC 字符串。
命令行与运行
- 支持
--window、--threshold、--trusted三个参数,运行时可以调整灵敏度。 - 用 BPF 过滤器
udp and (port 67 or port 68)以减少抓包量。 sniff(..., store=False)是被动监听,不保存包到内存,避免内存占用增大。
示例测试及结果
下面给出如何在实验室环境演示该工具并解读输出。注意:请在你被允许的网络环境中测试,不要在生产环境盲目运行抓包或进行攻击测试。
环境准备
- 在 Ubuntu / Linux 上安装 scapy:
pip3 install scapy - 需要 root 权限运行抓包:
sudo - 选定监听接口,例如
eth0/ens33/wlan0(取决于你的机器)
启动
假设你要监听 eth0,设置窗口 30 秒,阈值 2:
sudo python3 dhcp_watchdog.py -i eth0 --window 30 --threshold 2 --trusted 192.168.1.1
输出示例(控制台):
2025-10-28 20:40:00,123 [INFO] 启动 DHCP Watchdog,在接口 eth0 上监听 (window=30s, threshold=2)
2025-10-28 20:40:12,456 [INFO] 发现 DHCP OFFER: msg_type=2 server_id=192.168.1.1 yiaddr=192.168.1.100 routers=['192.168.1.1']
2025-10-28 20:40:13,011 [INFO] 发现 DHCP OFFER: msg_type=2 server_id=192.1.1.253 yiaddr=192.168.1.100 routers=['192.1.1.253']
2025-10-28 20:40:13,012 [WARNING] 可能的 DHCP 欺骗检测到: {"time": "2025-10-28T12:40:13.012345Z", "client_mac": "aa:bb:cc:dd:ee:ff", "servers": ["192.168.1.1","192.1.1.253"], "server_router_map": {...}, ...}
解释:
- 第一个 OFFER 来自可信服务器
192.168.1.1(在白名单),第二个来自未在白名单的192.1.1.253且提供的 routers 不同,触发警报。 - 工具会在
dhcp_watchdog.log写入完整 JSON 结构。
离线测试
如果没有真实环境,也可用另一个测试主机产生模拟的 DHCP OFFER(只作本地实验)。或在本机上用 pcaps 回放(tcpreplay)来验证分析器对记录的解读能力。工具本身是被动的,只要 pcap 中包含 DHCP OFFER/ACK,它就能处理。
时间复杂度
-
每收到一个 DHCP 包,
process_offer做的主要工作是:- 在对应客户 MAC 的 deque 末尾插入记录(O(1))。
- 从 deque 左侧弹出过期记录直到窗口内(每个记录在生命周期中被弹出一次),摊还复杂度 O(1) 每条记录。
- 分析时把当前 deque 中的记录遍历一次(假设窗口内最多
k条记录),构建映射与集合:O(k)。
-
因此单个包的平均时间复杂度为 O(k)(k 是窗口内同一客户端的记录数)。在常见网络下,k 很小(通常个位数),因此工具能高效运行。
空间复杂度
- 主存占用由
EventWindow.data决定:它保存窗口内的每个客户端的记录。 - 假设短时窗口内共有
C个不同客户端生成记录,每个客户端平均保存k条记录,则空间复杂度为 O(C·k)。 - 因为我们定期清理过期记录,且不保存完整 packet payload(只保存 summary 字符串),内存使用通常很小。即便在大型网络,可通过缩短窗口或限制每个客户端队列最大长度来控制内存。
扩展与实际部署建议
在真实企业/校园场景里,单靠一个主机运行检测脚本还不够------推荐的综合措施:
在网管层阻断:
- 在交换机上开启端口安全(Port Security),限制单端口 MAC 地址数量;对于办公桌面,锁定只允许授权的 MAC。
- 启用 DHCP snooping(大多数厂商交换机支持),并在交换机上配置可信端口/不可信端口,配合动态 ARP inspection(DAI)可以在交换机层面阻断伪造 DHCP 报文。
将检测工具作为 SIEM 的数据源:
- 把
dhcp_watchdog的日志或 webhook 集成到企业的 SIEM 或告警系统(如 Splunk、ELK),当检测到可疑事件自动创建工单或触发 ACL 策略。
在关键网段部署多点监控:
- 在多个分支或楼层的关键交换机旁部署轻量级探测虚拟机/容器,集中上报到守护中心,使得检测覆盖性更好。
白名单维护:
- 在工具运行时维护可信 DHCP server 列表(比如中心 DHCP、容灾 DHCP 的 IP),减少误报。工具可定期从配置管理数据库(CMDB)拉取可信服务器列表以实现自动化。
告警分级:
- 将"不同 server 但 router 一致"的情况作为 Info 级别日志,"不同 server 且 router 不一致"的情况提升到高优先级并自动封锁端口(需和网管设备集成并经过审批)。
总结
- 本文把 DHCP 欺骗的问题放到实际场景中:公共场合、分支网点或校园网的风险是现实的。我们实现了一个被动的检测工具(dhcp_watchdog),它基于 Scapy 抓包、短时窗口聚合、并通过对比不同 DHCP server 提供的 router(默认网关)来判断可疑事件。
- 设计原则是保守报警、减少误报、可扩展到告警/自动化运维。代码既能作为独立检测工具运行,也易于嵌入更大运维体系(比如将警报发送到 SIEM 或 webhook)。
- 最后给出部署建议:结合交换机层的 DHCP snooping、端口安全和集中化日志能把风险降到最低。