DNS解析监测与hosts映射:域名劫持排查实践

发布时间:2026/10/23 6:05:43
DNS解析监测与hosts映射:域名劫持排查实践 简介面向网络管理员、运维人员与安全测试者的域名劫持测试工具包主要用于模拟和验证DNS解析重定向可服务于内部网络流量控制、故障排查、网站服务迁移以及DNS配置正确性检查等场景。工具围绕自定义DNS解析展开支持为特定域名指定目标IP并提供重定向活动监控与日志记录便于后续分析与审计同时可限定用户或网络段实施劫持并按需要选择临时或长期重定向以降低对正常服务的影响。压缩包为rar格式体积约227KB文件总数显示为0类型明细暂无数据。已有2245人学习下载反映出DNS安全测试与网络管理方向对该主题的持续关注。读者可从中了解域名劫持的实现思路、测试边界与恢复方法掌握重定向配置、日志核查和安全控制等要点并建立合法授权、测试后及时恢复DNS配置的操作意识。1. 域名劫持到底是什么从一次解析跑偏说起线上服务没动过配置值班群突然有人说打不开本机curl却是通的换个网络又好了——这种“一半人正常一半人异常”的问题十有八九卡在解析层。域名劫持工具这个叫法容易让人联想到攻击实际落到运维和开发手里它做的事是把“域名→IP”这条链路的每一跳展开来看浏览器缓存、系统缓存、hosts、递归服务器、权威服务器到底是哪一环返回了不该返回的地址又是从什么时候开始的。它适合做域名资产维护、上线前解析校验、CDN 切换验证和故障定位的人也适合自己搭站、天天跟 DNS 记录打交道的人。这里只谈解析的可控、可验证和可回滚涉及对他人域名做未授权改写的部分不在范围内也不建议碰。2. DNS 劫持的四类典型路径先认清对手再谈工具装工具之前得先明白一件事所谓“域名被劫持”从来不是单一动作而是链路上某一环的结果被人换掉了。不同环节被换排查手法、影响范围、恢复速度完全不同。我见过最常见的误区是出问题就先去改 hosts 或者换 DNS结果真正的改动在权威侧折腾半天没碰到根上。这一章把链路拆开给出每一环的识别特征和验证命令后面几章的监测脚本、映射工具都建立在这套基础上。2.1 一条解析请求到底经过哪几个环节从一个域名在地址栏敲下回车到真正发出 TCP 连接中间至少经过六层应用或浏览器自身的 DNS 缓存、操作系统的 DNS 缓存、hosts 文件、本机配置的递归服务器运营商、公共 DNS 或内网 DNS、根与顶级域的委派、最后是域名自己的权威服务器。每一层都可能返回缓存结果也每一层都可能被人为改写。我一般按“影响范围”倒推只有一个浏览器错看应用缓存只有一台机器错看系统缓存和 hosts一个办公网或一个运营商网段错看递归服务器全网错且各地一致直奔权威服务器。这个倒推顺序能把定位时间从小时级压到分钟级。环节维护方被改写后的典型症状快速验证方式应用/浏览器缓存客户端单个浏览器错换无痕窗口正常换浏览器、开无痕系统 DNS 缓存客户端全机错清缓存或重启解析服务即恢复清缓存前后对比hosts 文件客户端仅本机错同事同网络正常直接看文件内容递归服务器运营商/内网一个网段错指定其他server正常多服务器对比查询权威服务器域名持有方全网错且各地结果一致直接查权威证书层站点方解析对但证书域名不匹配openssl s_client校验表格里最后一行容易被忽略解析结果是正确的 IP但 TLS 握手报证书不匹配这种情况多半是流量在中间被转发到了另一台主机属于比改记录更隐蔽的一类。遇到“能连上但证书报警”别急着怀疑证书过期先比对一下响应 IP 和权威记录是否一致。2.2 hosts 改写、缓存投毒与权威记录被改症状不一样hosts 改写是最容易识别的改动只影响本机同网络的其他机器完全正常文件里往往能直接看到一条不该存在的映射。它的特点是“精确打击单机”常见于本地开发调试、也常见于某些软件安装时顺手写入。缓存投毒影响的是一个递归服务器背后的所有用户症状是同一运营商、同一内网段的用户集体出错而指定其他递归服务器查询立刻恢复正常。这类问题的难点在于它会随 TTL 过期自愈等你赶到现场可能已经好了所以必须靠留档而不是靠现场观察。权威记录被改是影响最大的一类所有递归服务器最终都要回源回源拿到什么就缓存什么所以表现为全网一致地错。判定方法很直接——绕开所有递归直接向该域名的权威服务器发起查询看记录是否已经变了。还有一种不涉及记录改动的做法CNAME 指向的目标域名被人接管或者对方把一条 CNAME 记录改到了自己的地址上。这种情况下主域名的 A 记录看起来完全正常问题藏在 CNAME 链的下游。判定顺序我习惯这么走直接查权威服务器确认源记录是否被改用两个以上不同网段的递归服务器查询确认影响范围在本机清缓存前后各查一次排除本地缓存检查 hosts 和系统代理配置排除本机改写比对响应 IP 的归属和证书排除中间转发。2.3 用 dig 建立解析基线命令和参数逐条讲定位任何解析问题第一步都是建立“正常长什么样”的基线。没有基线的监测等于没有监测因为你不知道拿到的结果该和谁比。下面这几条命令我基本是条件反射地敲。# 1. 直接问权威服务器绕开所有递归缓存 # - 先用 NS 查出权威服务器名字再指定它查询 A 记录 dig short NS example.test dig ns1.example.test example.test A norecurse # 2. 从两个不同的递归服务器查询对比结果与 TTL dig 203.0.113.53 example.test A noall answer dig 203.0.113.54 example.test A noall answer # 3. 跟踪完整解析路径看委派在哪一层发生变化 dig trace example.test A # 4. 查询 DNSSEC 签名状态AD 标志位表示验证通过 dig example.test A dnssec multiline # 5. 全类型快照用于后续比对 dig example.test ANY noall answer参数逐个说清楚norecurse表示不让对方代为递归只返回它自己知道的答案查权威时必须加否则拿到的是别人的缓存noall answer只输出答案段适合脚本解析trace会从根开始逐级查询输出很长但能一眼看出委派在哪一级断了或者指错了dnssec会带上RRSIG记录和AD标志AD为真说明沿途的递归服务器完成了签名验证。后面跟的地址建议不要只用一个。我一般准备三个一个内网递归、一个公共递归、一个直连权威。三者结果一致基本可以排除解析层三者不一致差异本身就是线索。提示代码里的203.0.113.x是文档保留地址实际使用时换成你手上的递归服务器 IPexample.test同理换成你自己的域名。2.4 为什么监测脚本比“一键改写”更值得装一键改写的工具用起来爽但它只给你一个结果不给你过程。记录被改之后你真正需要回答的是三个问题改了多久、影响了哪些网段、TTL 到期后会不会自动恢复。这三个问题都得靠历史数据回答靠现场看一眼是答不出来的。监测类工具的价值就在这里它按固定频率把多个递归服务器、多个记录类型的应答原文和时间戳存下来形成一条可回溯的时间线。事故复盘时把时间线拉出来什么时候开始漂、漂到了哪个 IP、哪个网段先漂一目了然。选型上我会看三点能不能指定多个递归服务器而不是只用自己的系统配置能不能保存应答原文而不是只存解析后的 IP能不能区分“记录变了”和“TTL 到期后正常刷新”。第三点尤其关键很多误报都是把正常的 CDN 调度当成了异常。3. 搭一套域名解析监测从基线采样到告警脚本工具装好只是开始能不能稳定产出可比对的数据才是分水岭。这一章给一套最小可用的监测方案一个采样脚本、一套漂移判定规则、一个定时任务。整套东西加起来不到两百行但能覆盖绝大多数解析异常场景。3.1 采样脚本多节点、多记录类型、保留应答原文采样脚本的核心要求是“可重复”和“可回溯”。同一批域名、同一批服务器、同一批记录类型每次都按相同顺序查一遍结果写进带时间戳的文件里。这样两次采样之间的差异才有意义。import json import time from datetime import datetime, timezone import dns.resolver import dns.exception # 被测域名清单按业务重要性排序 TARGETS [ {domain: example.test, types: [A, AAAA, CNAME, NS]}, {domain: api.example.test, types: [A, CNAME]}, ] # 多个递归服务器至少包含一个内网和一个外部 RESOLVERS [203.0.113.53, 203.0.113.54] # 单次查询超时太短会误判为无应答太长会拖慢整轮采样 TIMEOUT 3.0 def query_one(resolver_ip, domain, rdtype): 对单个服务器发一次查询返回结构化结果异常也记录不抛出 resolver dns.resolver.Resolver(configureFalse) resolver.nameservers [resolver_ip] resolver.timeout TIMEOUT resolver.lifetime TIMEOUT record { ts: datetime.now(timezone.utc).isoformat(), resolver: resolver_ip, domain: domain, type: rdtype, } try: answer resolver.resolve(domain, rdtype, raise_on_no_answerFalse) # 保留每条记录的文本形式和 TTLTTL 是判断缓存层行为的关键 record[answers] [ {data: r.to_text(), ttl: answer.rrset.ttl} for r in answer.rrset or [] ] record[status] ok except dns.resolver.NXDOMAIN: record[status] nxdomain except dns.resolver.NoNameservers: record[status] no_nameservers except dns.exception.Timeout: record[status] timeout except Exception as exc: record[status] error record[error] str(exc) return record def main(): out_path fdns_sample_{int(time.time())}.jsonl with open(out_path, w, encodingutf-8) as fh: for target in TARGETS: for resolver_ip in RESOLVERS: for rdtype in target[types]: rec query_one(resolver_ip, target[domain], rdtype) fh.write(json.dumps(rec, ensure_asciiFalse) \n) print(fwritten: {out_path}) if __name__ __main__: main()逻辑说明Resolver(configureFalse)是关键它屏蔽系统/etc/resolv.conf的干扰保证查询只发往指定服务器。raise_on_no_answerFalse让“域名存在但没有该类型记录”不抛异常否则大量域名会因为没配 AAAA 而中断采样。异常全部落成status字段而不是抛出这样一轮采样不会因为单个域名超时而整体失败。参数怎么调TIMEOUT设 3 秒跨运营商查询一般 2 秒内返回超过 3 秒基本可以判定链路有问题RESOLVERS至少要有一个和你业务用户所在网段接近的服务器否则采到的结果没有代表性TARGETS里把 CNAME 单独列出来因为很多异常是从 CNAME 链下游开始的只看 A 记录会漏。3.2 漂移判定怎么区分正常调度和异常改写拿到两次采样结果直接做差集误报会多到没法看。CDN 域名在不同递归服务器上返回不同 IP 是常态TTL 到期后 IP 变了也是常态。我用的判定规则有下面几条按优先级从高到低判定项正常情况异常信号权威与递归结果递归结果属于权威记录集合递归返回了权威里不存在的 IPTTL 数值在配置值附近波动被强制压到很小如 30 秒以下且持续CNAME 链长度固定与配置一致突然多出一跳指向陌生域名同一递归多次查询结果稳定短时间内反复跳变响应 IP 归属与业务部署区域一致落在完全无关的网段第一条是硬规则递归服务器缓存的一定是权威给过的记录除非被改写。所以只要出现“递归返回的 IP 不在权威记录集合里”就值得立刻拉出来看。第二条也很有用某些改写手段为了让自己的结果尽快生效会把 TTL 强行压低正常业务很少这么做。第三条容易被忽略。CNAME 链上多一跳可能意味着某个中间域名被别人接管了而主域名的查询结果看起来毫无异常。注意CDN 场景下同一域名返回多个 IP 属于正常判定时要比较“集合”而不是“单个 IP”否则每天都在误报。3.3 定时任务与告警落地采样脚本跑通之后挂到定时任务上频率我一般设 5 分钟一次。太密会给自己增加噪声太疏会漏掉短时改写。数据按天切分文件方便回溯也方便清理。# 每 5 分钟采样一次日志按月归档 */5 * * * * cd /opt/dns-watch \ /usr/bin/python3 sample.py /var/log/dns-watch/run.log 21 # 每天凌晨把前一天的数据压缩归档保留 90 天 30 0 * * * find /opt/dns-watch -name dns_sample_*.jsonl -mtime 1 \ -exec gzip {} \; -exec mv {}.gz /data/archive/ \;告警这块有个血泪经验不要每条异常都发。第一次上线时我配的是“有任何差异就告警”结果一天几百条第三天就没人看了。后来改成按域名聚合、按异常类型去重、同一域名同一类型 30 分钟内只发一次有效告警才真正被处理。去重的实现思路很简单告警前先读一个状态文件记录每个“域名异常类型”组合的上次发送时间未超过窗口期就跳过超过就发并更新时间戳。窗口期设 30 分钟对大多数业务够用。还有一条告警内容里必须带上采样时间、递归服务器 IP、期望值和实际值。只发一句“解析异常”收到的人还得自己去查等于没告警。3.4 把证据保全下来应答原文与时间戳解析类问题的麻烦之处在于它会自愈。TTL 一过缓存刷新现场就没了。所以采样时保存应答原文比保存解析结果重要得多——原文里有 TTL、有 CNAME 链、有权威段这些都是事后复盘的依据。我一般要求采样文件里至少保留查询时刻UTC、递归服务器 IP、域名、记录类型、完整应答文本、TTL、状态。字段不多但每一条在复盘时都用得上。尤其是 UTC 时间戳跨时区协作时能省掉大量换算。数据保留周期按业务定域名资产变动不频繁的话90 天足够覆盖一次完整的变更和回滚周期。4. 本地域名映射工具怎么用hosts 管理与开发环境调试监测是“看”映射是“改”。开发调试、灰度验证、上线前预演都离不开在本地把某个域名指到指定 IP。这一章讲 hosts 的正确用法、批量维护脚本以及什么时候该从 hosts 升级到本地 DNS 服务。4.1 hosts 的生效顺序与三个容易忽略的细节大多数 Linux 发行版的名称解析顺序由/etc/nsswitch.conf里的hosts:行决定默认是files dns也就是先查 hosts 文件、查不到再走 DNS。这意味着 hosts 的优先级高于任何 DNS 记录写进去就立刻覆盖。三个细节经常翻车。第一是 IPv6域名同时有 A 和 AAAA 记录时只写 IPv4 的 hosts 条目某些客户端仍会优先走 AAAA 查询并拿到真实地址看起来像 hosts 没生效。稳妥做法是 A 和 AAAA 都写上不需要 IPv6 就写::1或直接屏蔽。第二是尾部点。example.test.和example.test在部分工具里被视为不同名字写 hosts 时统一不带尾点。第三是缓存刷新。Linux 上 systemd-resolved 会缓存改完 hosts 要执行resolvectl flush-cachesmacOS 需要sudo dscacheutil -flushcache配合sudo killall -HUP mDNSResponderWindows 是ipconfig /flushdns。不刷缓存就断言“hosts 不生效”是很常见的误判。还有一个使用习惯hosts 里给自己维护的条目加统一注释前缀比如# dev-managed这样批量更新和清理时不会误删系统或软件写入的行。4.2 用脚本批量维护映射条目手工改 hosts 在条目少的时候没问题超过十条就容易冲突、重复、漏删。我一般用一个带标记块的脚本管理只操作自己那段其他内容原样保留。import re from pathlib import Path HOSTS Path(/etc/hosts) BEGIN # dev-managed END # dev-managed # 映射清单域名 - IP按环境分组维护 MAPPINGS { example.test: 203.0.113.10, api.example.test: 203.0.113.11, static.example.test: 203.0.113.12, } def render_block(): lines [BEGIN] for domain, ip in MAPPINGS.items(): lines.append(f{ip}\t{domain}\t# dev) lines.append(END) return \n.join(lines) def update_hosts(): text HOSTS.read_text(encodingutf-8) block render_block() pattern re.compile( re.escape(BEGIN) r.*? re.escape(END), re.S ) if pattern.search(text): # 已有标记块则原地替换不影响文件其他部分 new_text pattern.sub(block, text) else: # 没有标记块则追加到文件末尾保留原有内容 new_text text.rstrip(\n) \n\n block \n HOSTS.write_text(new_text, encodingutf-8) print(hosts updated, remember to flush dns cache) if __name__ __main__: update_hosts()逻辑说明用成对标记把托管区域圈起来正则匹配时开启re.S让.能跨行实现整块替换。没有标记块时追加而不是覆盖避免把系统原有条目冲掉。参数说明MAPPINGS按环境拆成多个字典更实用比如DEV、STAGING切换环境时换一个字典重跑即可。写文件需要 root 权限实际使用建议先写临时文件再原子替换避免中途失败导致 hosts 被截断。提示脚本只负责写不负责刷缓存。写完记得按平台执行对应的刷新命令否则改动不会立刻生效。4.3 从 hosts 升级到本地 DNS 服务hosts 的局限很明显只对单机生效、不支持通配符、不支持按域名后缀批量转发。当需要模拟整个子域、或者要在一个小团队内共享同一套映射时本地 DNS 服务更合适。常见做法是用 dnsmasq 起一个轻量解析服务配置文件里写死几条映射其余请求转发到上游。配置很短# /etc/dnsmasq.d/dev-mapping.conf # 只监听本地和测试网段别开在公网口上 listen-address127.0.0.1,203.0.113.1 # 单条静态映射 address/api.example.test/203.0.113.11 # 整个子域指向同一地址hosts 做不到这一点 address/internal.example.test/203.0.113.20 # 其余域名转发到上游递归服务器 server203.0.113.53address的语义是匹配到该域名含其子域就直接返回指定地址不再向上游查询。server指定默认上游。配置改完systemctl reload dnsmasq生效不需要重启。这套方案的好处是可以让测试网段的所有机器统一走这台 DNS映射一次全组生效。代价是多了一个需要维护的服务所以只在确实有多机共享需求时才上单机调试用 hosts 更省事。5. 排查与避坑六个真实翻车现场工具用起来之后坑基本都出在“以为它应该这样”和“实际它那样”之间。这一章把踩过的六次记下来按现象、原因、解决三段写遇到类似症状可以直接对照。5.1 采样环节的两个坑现象一采样脚本跑了一晚上第二天打开文件发现只有几十条记录大量域名状态是timeout。原因是脚本对所有查询共用一个 Resolver 实例超时时间又是全局的第一个慢查询把后续查询的预算吃掉了。另外部分递归服务器对短时间内的高频查询会限速连续打过去直接不回应。解决是把 Resolver 实例化放到每次查询内部让每次查询独立计时同时在服务器之间加 100 到 200 毫秒的间隔避免触发限速。改完之后同样一批域名超时率从三成降到不足百分之一。现象二某个域名的监测结果每天都在跳一会儿多一个 IP一会儿少一个但人工查又是正常的。原因是这个域名配了 CDN不同递归服务器、不同时刻拿到的 IP 本来就不一样而判定脚本用的是“集合完全相等”。这类误报最消耗信任几次之后就没人看告警了。解决办法是判定逻辑改成子集判断只要递归返回的 IP 全部落在权威记录集合内就算正常超出的才报。另外把 TTL 也纳入比对正常调度的 TTL 是稳定的异常改写往往伴随 TTL 异常。5.2 判定与告警环节的两个坑现象三告警集中在凌晨触发白天一条没有看起来像定时被改。原因是凌晨有批量任务会集中查询采样脚本和业务任务撞在一起本地递归服务器压力大部分查询超时后返回空结果被判定成“记录丢失”。这属于自己制造的问题。解决是给采样单独指定一个递归服务器不跟业务共用同时在判定时把timeout和nxdomain分开处理超时不计入异常只在连续多次超时的时候单独告警。把“查不到”和“查到了但不是预期”混为一谈是判定逻辑里最常见的错。现象四某个域名告警了整整两天人工核查却发现解析完全正常。原因是这个域名在半年前换过一次服务商旧的权威服务器还留着一条过期记录采样时偶尔会路由到旧服务器上。两套权威返回不同结果判定脚本自然认为是异常。解决是采样时先查 NS 记录确认当前权威列表只向列表内的服务器发起查询同时在域名变更流程里加一条“旧权威下线确认”避免僵尸记录长期存在。这类问题不解决会一直消耗排查精力。5.3 配置与生效环节的两个坑现象五hosts 改完ping生效了但业务程序连的还是老地址。原因是业务程序用的是自己的 DNS 解析库有些语言的运行时会在进程启动时缓存解析结果还有些程序直接读自己的配置文件而不走系统解析。ping走的是系统解析两者根本不是一条路径。解决办法是先确认目标程序用的是哪套解析机制Java 有自己的缓存策略Node.js 有dns.lookup和dns.resolve两条不同路径。确认之后再决定是改 hosts、改程序配置还是重启进程。只验证ping是不够的必须用程序自身的连通性测试来验证。现象六dnsmasq 配好之后本机正常同事的机器还是解析到真实地址。原因是同事的机器根本没指向这台 DNS还在用默认的运营商递归。配置只在本机resolv.conf生效没有下发到测试网段。解决办法是二选一要么在 DHCP 里把 DNS 指向这台机器让网段内自动生效要么让同事手动改网络配置。同时注意resolv.conf可能被网络管理服务覆盖需要确认它是不是被某个服务托管直接在文件里改会被冲掉。这六条的共同点在于每一条都不是工具本身的问题而是对“哪一层在起作用”判断错了。排查解析问题先把链路画出来再确定自己在哪一层动手比直接改配置有效得多。6. 把防护做进流程DNSSEC、CAA 与证书透明日志的日常检查监测能发现异常但防不住异常发生。真正降低风险的是把几项配置检查固化进日常流程让问题在被人利用之前暴露。我一般在域名上线和季度巡检时各跑一遍下面这套检查。检查项命令或入口通过标准DNSSEC 签名dig dnssec看AD标志标志为真链上无断点CAA 记录dig CAA只允许已知 CA 签发证书域名openssl s_client -connect主题与 SAN 均匹配证书透明日志按域名检索签发记录无未知签发记录NS 记录一致性多个递归对比 NS权威列表一致且无残留DNSSEC 这块AD标志为假不一定代表没配也可能是你用的递归服务器不做验证。判断时要区分“域名未签名”和“验证未通过”前者看有没有RRSIG记录后者看递归服务器自身是否开启验证。CAA 记录的作用是告诉证书颁发机构“只允许这几家给我签发”。配置很简单一条记录就够example.test. CAA 0 issue letsencrypt.org。它的价值在于即使有人通过其他途径申请到了该域名的证书正规 CA 也会因为 CAA 不匹配而拒绝签发。证书透明日志是另一道保险。每次有 CA 签发证书都会写入公开日志。定期按自己的域名检索一遍出现不认识的签发记录就要立刻排查。这一步不需要任何工具浏览器检索入口就能查。# 一次性快照DNSSEC 状态 CAA 证书有效期 dig example.test A dnssec short dig example.test CAA short echo | openssl s_client -connect example.test:443 -servername example.test 2/dev/null \ | openssl x509 -noout -subject -dates -ext subjectAltName这三条命令我一般放在变更前和变更后各跑一次输出存档。变更后 DNSSEC 的AD标志消失往往意味着签名没跟上记录更新这种情况在上线窗口里发现比等用户反馈要好得多。有个习惯是被一次事故逼出来的那次改完解析记录忘了同步更新 DNSSEC 签名结果部分做验证的递归服务器直接返回解析失败而没做验证的线路一切正常问题隐藏了将近一天才被定位。从那以后我每次动解析记录都强制把 DNSSEC 状态、CAA 记录、证书有效期这三项走一遍输出和变更单一起存档。看起来多花十分钟但省下的是整晚的排查。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询