上个月有个做运维的朋友半夜给我发消息,说他们办公室打开自家官网会跳到莫名其妙的页面,但换成手机热点访问又一切正常。我第一反应就是DNS劫持——这类故障的典型特征太明显了:特定网络环境下域名解析结果被篡改,换个出口就恢复正常。很多人对DNS劫持的理解停留在"运营商插广告"这个层面,实际上它的成因横跨终端、路由器、链路、递归服务器、权威服务器这五个环节,每一层的检测手段和处置方式都不一样。这篇是我把过去几年处理过的十几起解析异常事件整理出来的实战版:前半段讲怎么在十分钟内判断劫持发生在哪一层,后半段讲分层防御怎么落地、哪些配置是真的有用、哪些只是心理安慰。适合手里管着几台服务器或者一整个办公网的运维、安全从业者,也适合只想知道"我家路由器是不是被人动了手脚"的普通用户,文中所有案例和地址都做了脱敏处理,你可以直接对着自己的环境替换验证。
1. DNS劫持的本质与常见手法拆解
1.1 一次解析请求到底走了哪些环节
要检测就得先知道正常长什么样。你在浏览器里敲一个域名,背后发生的事大致是这样:浏览器先查自己的缓存,再查操作系统的DNS缓存,然后是系统的hosts文件,都没有结果才把请求发给配置的首选DNS服务器。这台服务器如果是递归解析器,它会从根域名服务器开始逐级询问,依次拿到顶级域的权威服务器地址、二级域的权威服务器地址,最后从权威服务器那里取回记录,一路缓存回来给你。
整个链条上任何一环被动了手脚,你看到的结果就是错的。这里有个容易被忽略的点:递归解析器返回给你的应答里,会带着这条记录的TTL,也就是缓存存活时间。正常情况下这个值是从权威服务器原样透传下来的,比如 3600 秒、300 秒。如果有人在中途伪造了一个应答,他为了让自己的伪造结果尽快覆盖掉真实缓存、又不想频繁重复投毒,往往会把TTL设成一个很奇怪的值,比如 1 秒、2 秒,或者干脆是个跟你预期完全对不上的数字。这就是后面检测环节里最便宜好用的一个抓手。
还有一个基础概念必须说清楚:DNS协议本身基于UDP,默认无加密、无签名、无认证。请求里有一个16位的随机事务ID,应答必须带着同一个ID回来才算匹配。早年很多设备生成这个ID的方式很随意,加上源端口固定,攻击者只要在真正的应答到达之前抢先发一个伪造包,客户端就会认。这就是经典的缓存投毒,本质上是"谁先说谁赢"。理解了这个前提,你就能明白为什么现代防护要把ID和源端口都随机化,也就能理解为什么加密DNS和DNSSEC是有意义的,而不是厂商的营销话术。
1.2 五类典型劫持路径与识别特征
我把处理过的情况归成五类,每类的识别特征和处置方式差异很大,混在一起排查只会浪费时间。
| 类型 | 发生位置 | 典型现象 | 识别难度 |
|---|---|---|---|
| 终端本地篡改 | hosts文件、木马、浏览器插件 | 只有单台机器异常,同网络其他设备正常 | 低 |
| 路由器DNS被改 | 家用或办公网关 | 整个网络下所有设备异常,切流量即恢复 | 低 |
| 链路中间人投毒 | 接入网、无线热点 | 间歇性异常,常在特定时段或特定位置出现 | 中 |
| 递归服务器被污染 | 上游解析服务 | 一段时间内大面积异常,换解析器即恢复 | 中 |
| 权威侧配置错误或被接管 | 域名自己的NS | 所有解析器结果一致地错误 | 高 |
第一类最常见也最容易被误解。很多人一发现网页跳转就怀疑是网络问题,实际上先看 hosts 文件能省掉一半排查时间。我之前遇到过一个案例,某台办公电脑的 hosts 里被加了一条记录,把内网一个管理后台的域名指向了一个外部地址,任何人在这台机器上访问都会被导向钓鱼页面,而同网段其他机器完全正常——这就是典型的单点问题。
第二类是家用场景的重灾区。攻击者拿到网关的管理权限之后,只需要在WAN口配置里把DNS改成自己的服务器,全屋所有设备就都中招了,而且用户完全无感,因为设备连上网是正常的,只是解析结果不对。判断方法简单粗暴:登录网关看DNS配置项,如果里面填的地址你不认识,那就是它。
第三类和第四类需要一点工具才能区分。核心思路是从不同位置、向不同解析器发同一个查询,看结果是否一致。如果只有某个网络环境下异常,问题在链路或本地;如果所有位置向某个特定解析器查询都异常,问题在那个解析器;如果所有解析器都返回同一个错误结果,问题就出在权威侧了。这个判断逻辑后面第二节会展开成具体命令。
2. 实战检测:从被动现象到主动验证
2.1 症状分诊:先花三分钟缩小范围
拿到一个"网页打不开或者跳转异常"的问题,不要急着抓包,先做三件事定位范围。
第一,换设备验证。手机开流量访问同一个域名,如果正常,说明问题在你当前的网络出口之后,排除掉权威侧和大部分公网因素。第二,换域名验证。同一台机器访问几个不同域名,如果全都异常,大概率是本地DNS配置或网关问题;如果只有特定几个域名异常,更可能是针对性的篡改。第三,换协议验证。用 IP 直连(前提是知道目标IP)看能否正常访问,能连通但域名解析错,那就把范围锁死在DNS这一个环节上,不用再怀疑路由、防火墙、证书这些东西。
提示:做这三步的时候先别清理缓存,缓存本身就是证据。等定位完再清,否则你连复现都做不到。
这三步做完,问题基本能落到"单机 / 局域网 / 出口 / 上游"四个格子里的某一个。我个人的经验是,办公网里超过六成的解析异常最终追到网关或某台设备的静态DNS配置上,剩下的才是真正的攻击行为。所以先做便宜的排查,别一上来就往APT上想。
2.2 多解析器交叉比对:最有效的一招
确认问题在DNS环节之后,最直接的动作是拿着同一个域名,分别向多个不同的解析器查询,比较返回结果和应答来源。Linux 上dig是标配,Windows 上用nslookup,macOS 自带dig。下面这段是我常用的批量比对脚本,把要查的域名和几个解析器地址写进去跑一遍,异常基本一眼能看出来:
#!/usr/bin/env bash # 多解析器解析结果比对,示例地址均为文档保留网段,请替换为你环境中的实际地址 DOMAIN="www.example.com" SERVERS=( "192.0.2.53" # 本地网关 "198.51.100.53" # 内网自建递归 "203.0.113.53" # 外部公共解析A "203.0.113.54" # 外部公共解析B ) for s in "${SERVERS[@]}"; do echo "=== 查询 $s ===" dig @"$s" "$DOMAIN" A +noall +answer +comments \ | grep -E "status:|flags:|^$DOMAIN|Query time" echo done跑完之后看三个东西。第一看 status,正常应该是NOERROR,如果出现SERVFAIL而其他解析器正常,说明这台解析器在验证或转发环节出了问题。第二看 flags 里有没有aa标志,aa表示权威应答,正常情况下你问递归解析器是拿不到aa的,如果某台本该是递归的服务器返回了带aa的应答,反而可疑,说明它在冒充权威。第三看返回的 A 记录地址,把几个解析器的结果并排一比,不一致的那个就是嫌疑人。
再补一个更狠的招:dig +trace。它从根开始一级级问下来,把每一跳的应答都打出来。如果+trace走出来的最终结果和你本地解析器给的结果不一样,那基本可以确定你用的那台解析器缓存里被塞了假记录。这个命令在排查权威侧配置错误时特别好用,能直接看出是哪一级NS给错了委派。
# 逐级追踪解析路径,观察每一级的委派是否正常 dig +trace www.example.com A # 向权威服务器直接查询,绕过所有递归缓存 dig @ns1.example.com www.example.com A +norecurse+norecurse这个参数值得单独说:它告诉服务器"别帮我递归,你只回答你自己知道的东西"。如果权威服务器对+norecurse返回了正确记录,而递归解析器返回错误记录,责任就在递归这一侧的缓存上,跟域名配置毫无关系。
2.3 抓包看应答特征:确认是不是伪造包
结果比对能告诉你有问题,抓包能告诉你问题是怎么发生的。在客户端侧抓UDP 53端口的流量,重点看两件事:应答是否来自你配置的那个服务器地址,以及同一个查询是否收到了多个应答。
# 在客户端抓取DNS流量,观察请求与应答的对应关系 sudo tcpdump -i any -nn -s0 port 53 -w dns_capture.pcap # 只打印摘要,快速看是否存在一个查询多个应答 sudo tcpdump -i any -nn port 53 and udp -c 200如果一次查询收到了两个内容不同的应答,第二个到达的被系统丢弃,那就是典型的链路抢答。这种情况下你甚至能在时间戳上看出规律:伪造包往往比真实应答早到几十毫秒,因为攻击者就架在你和解析器之间的路径上。反过来,如果应答来源地址跟你配置的服务器对不上,那问题更直接——你的请求被重定向了,常见于网关做了DNS透明代理但没有正确转发。
抓到包之后用 Wireshark 或者tshark过滤一下响应码和记录内容,会比肉眼翻包快得多:
# 提取所有DNS应答中的域名与A记录,便于快速比对 tshark -r dns_capture.pcap -Y "dns.flags.response == 1" \ -T fields -e dns.qry.name -e dns.a -e dns.time2.4 TTL 与授权链的异常判别
前面提到TTL是个便宜的抓手,这里展开说怎么用。正常网站记录的TTL一般有明确规律,要么是整数档位(60、300、600、3600),要么是CDN厂商的固定值。如果你看到同一个域名在不同时间查出来的TTL在 1 到 5 之间反复跳动,而权威侧配置明明是 3600,那几乎可以确定有中间设备在持续投毒——它必须把TTL压到极低,才能保证自己伪造的记录不被真实缓存覆盖。
另一个检查点是应答里的附加段。正常应答的 AUTHORITY 段会带上管理该域的NS记录,ADDITIONAL 段会带对应NS的地址。伪造的应答往往这两段是空的,或者内容跟真实的NS对不上。用dig +noall +authority +additional单独把这些段打出来,跟+trace看到的真实NS列表一比,不一致就是问题。
还有一种隐蔽情况:解析结果本身没错,但返回的CNAME链异常长,或者中间夹了一个你不认识的域名。这种情况在域名被恶意接管的场景里出现过,攻击者在权威侧加了一条CNAME,把流量先导到自己控制的域名再转回来,中间可以做统计、可以做拦截、也可以在特定条件下才返回恶意地址。排查方法是dig +short看完整链路,dig CNAME单独确认每一跳的归属。
3. 分层防御方案设计
3.1 终端层:成本最低、收益最高的加固
终端是所有劫持路径的最终落点,这一层做扎实了,后面几层即使出问题也不至于全盘沦陷。
第一件事是锁定 hosts 文件。Windows 上把C:\Windows\System32\drivers\etc\hosts设为只读,并去掉继承权限中普通用户的写权限;Linux 和 macOS 上用chattr +i或者调整文件属主。这不是为了防高级攻击,而是防止恶意软件和误操作随手改。同时把 hosts 里内容定期导出做基线比对,多出一行就得查清楚是谁加的。
第二件事是把关键域名的解析固化下来,也就是内网重要的管理后台、OA、监控面板这类,直接在 hosts 里写死 IP。这些域名本来就不该依赖公网解析,写死之后即使DNS全挂,运维通道还在。这个做法我在好几个环境里推过,付出几乎为零,收益是故障时还能进去修。
第三件事是检查系统里有没有多出来的DNS配置。浏览器、某些客户端软件会用自己的DNS设置覆盖系统设置,这种"应用层解析"很容易被忽略。排查方法是看系统当前生效的解析器地址,跟你在网络设置里配的是否一致:
# Linux 查看当前生效的解析配置 resolvectl status 2>/dev/null || cat /etc/resolv.conf # Windows 查看所有网络接口的DNS配置 ipconfig /all | findstr /i "DNS Servers" # macOS 查看各服务的DNS设置 scutil --dns | grep nameserver3.2 网关与出口层:把控住唯一入口
办公网和家庭网络里,网关是流量必经之路,也是攻击者最想拿下的位置。加固逻辑就三条:改掉默认凭据、关掉不必要的外部管理、把DNS配置纳入定期核查。
默认凭据这条不用多说,但我要强调一个细节:很多网关的管理密码改了,无线密码也改了,却忘了固件更新。老固件里的已知问题往往就是入口,所以把固件更新纳入季度检查项是必要的。另外,关闭WAN侧的管理访问、关闭WPS和一键配对功能,能挡掉相当一部分自动化扫描。
DNS配置的核查建议做成脚本定期跑。核心是比对网关实际生效的DNS地址和你登记的地址是否一致,不一致立刻告警。我在一个环境里见过网关被改了DNS后,攻击者还顺手把管理界面的DNS配置项做了隐藏,肉眼看配置页面一切正常,只有从系统状态页或者SNMP读出来才能发现真实值不一样。所以别只信管理界面,从设备外部验证才可靠。
有条件的环境建议在内网自建递归解析器,把客户端全部指向它,由它统一向上游查询。这样做的价值不只是性能:你有了一个集中的、可控的、可记录所有查询的节点。缓存投毒想成功,得先过这一关;出问题时,日志里能直接看到谁在什么时候查了什么。
# 以 Unbound 为例的最小可用配置要点 # /etc/unbound/unbound.conf server: interface: 192.0.2.10 port: 53 access-control: 192.0.2.0/24 allow access-control: 0.0.0.0/0 refuse do-ip6: no hide-identity: yes hide-version: yes qname-minimisation: yes prefetch: yes这里几个参数值得解释。access-control默认拒绝,只放行内网段,这是为了防止自己的解析器变成开放递归被人拿去做放大攻击。qname-minimisation开启查询名最小化,向上游只暴露必要的层级信息,减少信息泄露。hide-identity和hide-version让外部探测拿不到软件版本。这几项都是几行配置的事,但对整体暴露面的收敛作用明显。
3.3 DNSSEC 与加密传输:分清哪个解决哪个问题
这两样经常被混为一谈,实际上解决的不是同一个问题。
DNSSEC 解决的是"数据完整性"。它在DNS协议上加了数字签名,权威服务器给记录签名,递归解析器负责验证。验证通过说明这条记录确实来自该域名的管理者且没被改过,验证失败就直接返回 SERVFAIL 而不是把错误结果给你。这才是从根子上防篡改的机制。但它的前提是域名自己部署了DNSSEC,客户端使用的解析器也必须开启验证,缺一不可。部署顺序上,先在解析器侧开启验证(这一步对下游立刻生效,能挡住大量伪造),再考虑给自己的域名签名。
加密DNS(DoT/DoH)解决的是"传输机密性和链路完整性"。它把DNS查询封装在TLS里,链路上的中间设备看不到内容,也没法篡改,因为它改了就过不了TLS校验。这一层主要防的是接入侧监听和投毒,对上游解析器本身的可靠性没有任何改善——如果解析器自己返回了错误结果,加密传输只会把这个错误结果安全地送到你手上。
所以正确的组合是:加密传输保护"你到解析器"这一段,DNSSEC保护"解析器到权威"这一段的真实性。两者叠加,才覆盖完整链路。只上其中一个都能被绕过,这点在方案设计时要想清楚。
| 机制 | 防护目标 | 部署位置 | 局限性 |
|---|---|---|---|
| DNSSEC 验证 | 记录真实性、防篡改 | 递归解析器 / 终端存根 | 需域名侧签名,验证失败即不可用 |
| DoT / DoH | 传输加密、防链路监听篡改 | 终端或解析器 | 不保证上游解析结果正确 |
| 响应策略区(RPZ) | 拦截已知恶意域名 | 递归解析器 | 依赖威胁情报质量 |
| 源端口与ID随机化 | 防缓存投毒抢答 | 解析器 | 现代系统已默认开启 |
3.4 持续监控:让异常自己冒出来
防御做到位之后,剩下的工作是假设它还是会出问题,然后让问题尽早暴露。
最实用的监控手段是周期性解析比对。选一批关键域名,每隔几分钟分别通过本地解析器和两个外部解析器查一次,把结果哈希之后比对,不一致就告警。这个脚本几十行就能写完,跑在任意一台常开的机器上,成本几乎为零,但能第一时间发现劫持,而不是等用户来报障。
import dns.resolver import hashlib WATCH = ["www.example.com", "mail.example.com", "vpn.example.net"] RESOLVERS = ["192.0.2.10", "203.0.113.53", "203.0.113.54"] def resolve(domain, server): r = dns.resolver.Resolver(configure=False) r.nameservers = [server] r.timeout = 3 r.lifetime = 5 try: ans = sorted(str(x) for x in r.resolve(domain, "A")) return hashlib.md5(",".join(ans).encode()).hexdigest() except Exception as e: return f"ERR:{type(e).__name__}" for d in WATCH: results = {s: resolve(d, s) for s in RESOLVERS} if len(set(results.values())) > 1: print(f"[异常] {d} 解析结果不一致: {results}") else: print(f"[正常] {d} -> {list(results.values())[0]}")告警阈值上有个经验:偶发一次不一致可能是缓存刷新窗口导致的正常现象,连续两次以上才告警,否则告警疲劳会让人直接忽略掉真问题。另外告警内容里一定要带上各解析器的具体返回地址,不然值班的人还得手动复现一遍才能判断。
4. 常见问题与排查实录
4.1 高频问题速查
| 现象 | 最可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| 单台机器跳转,其他正常 | hosts 被改或本地恶意软件 | 查看 hosts 文件内容 | 清理并锁定文件权限 |
| 全屋/全公司异常,切流量正常 | 网关DNS配置被改 | 登录网关对比DNS设置 | 改回并更换管理密码 |
| 间歇性异常,抓包见重复应答 | 链路中间人抢答 | tcpdump 观察多应答 | 启用加密DNS、更换接入点 |
| 只有某几个域名异常 | 针对性投毒或域名被接管 | dig +trace 看委派链 | 核对权威配置,必要时更换NS |
| 所有解析器结果一致但都错 | 权威侧问题 | 直接问权威 +norecurse | 联系域名服务方核查记录 |
| 特定时段异常 | 缓存刷新窗口或策略生效 | 记录异常时间分布 | 缩短监控周期定位规律 |
4.2 几条踩坑总结的经验
缓存是最大的敌人也是最好的证据。排查的时候先在异常机器上把缓存清干净再复现,否则你看到的可能是一个几小时前就被投毒的结果,误判方向。Windows 上用ipconfig /flushdns,Linux systemd 环境用resolvectl flush-caches,macOS 用sudo dscacheutil -flushcache。清理前记得把当前解析结果记下来,那是唯一的现场。
别只盯着 A 记录。很多劫持针对的是 CNAME 或者 MX,A 记录看起来完全正常。尤其是邮件相关的异常,第一时间查 MX 和 SPF、DKIM 相关的 TXT 记录,用dig TXT和dig MX单独拉出来看。我遇到过一起邮件投递失败的案例,客户的 A 记录和 MX 记录都对,最后发现问题出在 TXT 里的一条策略记录被改了,导致对方直接拒收。
权威侧的配置错误常常伪装成劫持。有一种情况是域名的 NS 记录指向了两家服务商,但只有一家真正配置了记录,结果解析结果取决于你问到了哪一家的服务器,不同解析器给出不同答案。这种"看起来像投毒"的问题,用dig NS加+trace一看委派就清楚了,责任根本不在劫持。
处置完要验证,验证要看下游。改完网关或者解析器配置之后,别拿服务器本机测就完事,一定从客户端侧重新发一次查询,确认端到端生效。缓存的存在会让你误以为已经修好,实际上终端还在用旧的错误结果,用户那边看起来问题依旧。
日志保留周期至少覆盖一个完整业务周期。DNS 查询日志在事后分析里价值极高,但很多环境默认不开或者只留几天。建议至少保留两周,关键环境保留一个月,磁盘成本其实很低,出事的时候就是唯一的追溯依据。
最后分享一个我在实际环境里用得很顺的小习惯:给每个关键域名维护一份"解析基线",记录它的正常IP、TTL区间、NS列表、CNAME链路,存成文本文件跟着变更走。每次出现异常,先拿当前结果跟基线一比,三十秒就能判断是真出问题还是正常变更。这个文件我通常就放在监控脚本同一个目录下,出故障的时候随手就能翻,比到处找历史记录快得多。