☰
域名劫持工具:DNS解析链路四模块验证与隔离靶场实践
2026/10/24 4:37:44 网站建设 项目流程

简介:面向网络管理员、安全测试人员及运维学习者的DNS劫持测试工具包,主要用于在受控环境中模拟域名重定向,验证DNS配置正确性与网络安全性。压缩包为RAR格式,体积约227KB,上游文件总数标记为0、类型明细暂无数据,包内实际构成需以解压结果为准。工具围绕自定义DNS解析展开,可将指定域名指向目标IP,并配合重定向活动记录、按用户或网段实施安全控制、临时或永久重定向等能力,便于开展内部流量管理、故障排查及网站迁移时的解析验证,也可用于识别钓鱼重定向等风险。已有2245人学习或下载,适合需要理解域名劫持原理、检查解析风险并做小规模测试的读者参考。使用时应在授权环境内操作,测试后及时恢复DNS配置,避免影响正常服务。

1. 域名劫持工具到底在劫什么:从一次解析跑偏说起

前阵子有位同行遇到一件棘手的事:内网管理后台的地址栏没变、图标没变、证书锁也还在,输完账号密码才发现登录页是假的。事后检查发现服务器没有任何改动,真正被改动的是“域名变成 IP”这一步。域名劫持工具就是围绕这一步做的验证工具集,包含 hosts 改写、伪造应答、缓存投毒模拟、解析记录比对四个模块;四个模块分别对应一层可信度,每次动作都会留下日志。它要解决的是“我这条链路有多容易被改写、被改写之后多久能发现”这个问题,并非“怎么攻破别人”。做安全测试、DNS 运维、应急响应的读者,可以在隔离环境里跑通一遍;新手照着步骤也能复现。

2. 把解析链路拆成四段:域名劫持工具的四个动手位置

2.1 一次递归解析的五跳,每跳的可信度都不一样

从浏览器敲下回车,到拿到一个 A 记录,中间至少经过五跳:应用的 stub resolver、操作系统的解析库与缓存、本机配置的递归解析器、根与顶级与权威这一串委派、最后是应答原路返回。这五跳里,只有权威服务器那一跳是“域名持有人说了算”的,其余四跳都在别人的地盘上。工具之所以有价值,是因为它把每一跳单独拎出来,让你看到“这一段断了会怎样”。

真正让劫持变得可行的,是 DNS 协议本身的设计。UDP 53 端口上的查询和应答没有任何认证机制,客户端判断“这个应答是不是我要的”,靠的只有三样东西:查询报文里的 16 位 TXID、UDP 源端口、以及查询的名字本身。攻击者要做的,就是在真正的权威应答到达之前,抢先塞一个伪造应答进去,并且让 TXID 和源端口对得上。这就是常说的抢答,比拼的核心是时间窗口的大小。

历史上把这个窗口压得最狠的一次改进,是递归解析器开始对源端口做随机化。在这之前,很多实现会把源端口固定或者顺序递增,攻击者只要猜中 16 位 TXID 就有机会。源端口也加入随机化之后,两条 16 位合起来大约 32 位熵,配合“先制造一次缓存未命中、再抢在权威之前应答”的手法,命中率被压到很低,但并非为零。工具的缓存投毒模拟模块,测的就是这个“很低”到底有多低。

再往上一层是委派。域名的 NS 记录指向哪台权威、DS 记录有没有配上、注册商那边的解析服务器设置有没有锁,这些属于“域名持有人自己这边的门”。工具里对应的模块做的其实是比对:把多个出口、多个时间点的解析结果拉到一起,找出不一致的那一条。这部分在应急场景里比前面三个模块都实用,因为它不需要你动手,只需要你看得懂差异。

2.2 四个攻击面:从客户端一路往上到注册商

把上面几跳摊开,能动手的位置其实就四类。工具把每一类做成了一个模块,模块之间互不依赖,可以单跑也可以串起来跑。

攻击面典型手法工具模块影响范围复现难度
客户端本地改 hosts、改 DNS 设置、装带解析能力的扩展hosts 改写单机极低
局域网链路同网段伪造应答、抢占网关伪造应答一个广播域中
递归解析器制造缓存未命中后抢答、控制转发器缓存投毒模拟该递归下的所有客户端高
注册与权威侧改 NS、改注册商设置、区域传送泄露记录比对整个域名取决于账号安全

这张表最该看的是最后一列。复现难度越低的攻击面,现实里出现的概率反而越高,因为它不需要什么技术,只需要拿到一台机器或者一次社工。所以做排查的时候,顺序应该反过来:先查最低成本的那一层,再往上走。

2.3 为什么工具要拆成四个模块,而不要合成一个

我一开始也觉得拆成四个是多余的,直到在一个靶场里连续踩了两次坑才明白。四个模块的前置条件完全不同:hosts 模块只改本地文件,不需要任何网络权限;伪造应答模块要求工具所在的位置和目标客户端在同一个二层广播域,跨了三层就没戏;缓存投毒模拟模块要求你能直接向某个递归解析器发查询,也就是你得在它的允许递归列表里;记录比对模块压根不需要发包,它只需要几个能用的解析出口。

如果做成一个“一键劫持”的按钮,你会分不清失败到底是“手法不对”还是“环境不对”。分开之后,每一步的成败都能归因。这也是我在写实验脚本时坚持的一条:每个模块单独跑通、单独留日志,再去串。

2.4 先用两条命令量化随机化强度

在动手之前,先花五分钟看看你所在环境的解析器“抗不抗抢”。抓一段解析器出口的查询,把 TXID 和源端口导出来看分布。

# 在解析器出口抓 40 个查询报文,只看 UDP 53 sudo ip netns exec lab-resolver \ tcpdump -i veth-r2 -n -c 40 'udp port 53' -w /tmp/lab53.pcap # 导出 TXID 和源端口,统计每个源端口出现了几次 tshark -r /tmp/lab53.pcap -T fields -e dns.id -e udp.srcport 2>/dev/null \ | sort | uniq -c | sort -rn | head

-c 40控制抓包数量,40 个足够看出趋势;-e dns.id取 DNS 头里的 16 位标识,-e udp.srcport取源端口。判读方式很直接:如果 40 个包里源端口只有两三个值,或者 TXID 呈等差递增,说明随机化很弱,后面抢答的命中率会明显上升;如果源端口高位分散、TXID 看不出规律,说明这个出口的防护在线,工具跑出来的“失败”是真实失败,不要反复调参数去凑。

提示:这一步只抓自己实验网段里的报文。生产环境要做同类观测,走镜像口或者解析器自身的查询日志,不要在共享网段上抓包。

接下来才是搭靶场。没有隔离环境,后面所有结论都是玄学。

3. 隔离靶场怎么搭:三个命名空间和一条只跑 .lab 的解析链

3.1 地址规划与三条硬性要求

靶场不追求像生产,追求的是“每次跑出来结果一样”。我一般用三个 network namespace 拼一条最小链路:客户端、递归解析器、自建权威。三者用 veth 两两对接,地址全部落在 10.10.0.0/16 里,不碰默认路由,不碰宿主机解析配置。

角色命名空间接口地址作用
被测端lab-clientveth-c10.10.10.20/24发起查询,观察是否跑偏
递归lab-resolverveth-r1 / veth-r210.10.10.1 / 10.10.20.1工具的主要落脚点
权威lab-authveth-a10.10.20.53/24提供真实应答,作为对照

三条硬性要求:域名一律用.lab这类保留后缀,永远不要拿真实在跑的域名做实验;递归只服务实验域,其余查询一律拒绝,避免靶场变成开放解析器;每轮实验前回滚快照或者重建命名空间,不要指望“清一下缓存就好了”。

3.2 建命名空间并把 veth 对接起来

# 三个隔离命名空间 sudo ip netns add lab-client sudo ip netns add lab-resolver sudo ip netns add lab-auth # client <-> resolver sudo ip link add veth-c type veth peer name veth-r1 sudo ip link set veth-c netns lab-client sudo link set veth-r1 netns lab-resolver 2>/dev/null || sudo ip link set veth-r1 netns lab-resolver sudo ip netns exec lab-client ip addr add 10.10.10.20/24 dev veth-c sudo ip netns exec lab-resolver ip addr add 10.10.10.1/24 dev veth-r1 sudo ip netns exec lab-client ip link set veth-c up sudo ip netns exec lab-resolver ip link set veth-r1 up sudo ip netns exec lab-client ip link set lo up sudo ip netns exec lab-resolver ip link set lo up # resolver <-> auth sudo ip link add veth-r2 type veth peer name veth-a sudo ip link set veth-r2 netns lab-resolver sudo ip link set veth-a netns lab-auth sudo ip netns exec lab-resolver ip addr add 10.10.20.1/24 dev veth-r2 sudo ip netns exec lab-auth ip addr add 10.10.20.53/24 dev veth-a sudo ip netns exec lab-resolver ip link set veth-r2 up sudo ip netns exec lab-auth ip link set veth-a up sudo ip netns exec lab-auth ip link set lo up # 客户端默认路由指向递归,否则出了命名空间就不知道往哪走 sudo ip netns exec lab-client ip route add default via 10.10.10.1 # 关键一步:resolv.conf 要写进 /etc/netns/<name>/ 下,别直接 echo 覆盖 sudo mkdir -p /etc/netns/lab-client echo 'nameserver 10.10.10.1' | sudo tee /etc/netns/lab-client/resolv.conf

这段脚本里唯一需要解释的是最后两行。ip netns exec会把/etc/netns/<名字>/下的文件 bind-mount 到命名空间里的对应路径,所以写resolv.conf必须写在这个目录下。如果你图省事直接ip netns exec lab-client sh -c 'echo ... > /etc/resolv.conf',改的是宿主机的解析配置——这个坑我在第 5 章还会再提一次,因为它真的很容易中招。

ip route add default via 10.10.10.1这一行决定了客户端的所有查询都会打到递归上,递归那边用什么上游、走不走转发,跟客户端无关,这样实验变量就收窄到一个点上。

3.3 起一个只服务实验域的递归

递归用 dnsmasq 就够了,配置短、日志清楚、行为可预期。核心是no-resolv加上一条指向自建权威的server=,其余域名一律不转发。

# /etc/dnsmasq-lab.conf port=53 bind-interfaces listen-address=10.10.10.1 # 不读 /etc/resolv.conf,避免把宿主机的上游带进来 no-resolv no-hosts no-poll cache-size=1000 # 只有实验域交给自建权威,其他域名没有匹配的 server= 就会被拒绝 server=/demo.lab/10.10.20.53 # 先关掉 DNSSEC 校验,第 6 章要用它做对照 dnssec=off log-queries log-facility=/var/log/dnsmasq-lab.log

no-resolv是关键:没有它,dnsmasq 会把宿主机的上游服务器全读进来,你的实验域查不到就会去公网绕一圈,日志里全是噪音。listen-address只绑客户端那一侧的地址,递归不会在权威网段上暴露。log-queries一定要开,后面判读模块输出、对齐时间戳全靠它。

# 起递归 sudo ip netns exec lab-resolver \ dnsmasq -C /etc/dnsmasq-lab.conf --pid-file=/run/dnsmasq-lab.pid # 看它是不是真的起来了,以及绑在哪个地址上 sudo ip netns exec lab-resolver ss -lunp | grep ':53'

ss -lunp | grep ':53'输出的本地地址应该只有10.10.10.1:53。如果看到0.0.0.0:53或者[::]:53,说明bind-interfaces没生效,检查配置文件里有没有多打一个空格或者顺序放错。

3.4 基线验证:三次查询看 TTL 和来源

靶场起好之后,先别急着上工具。跑三次查询,把“没被动过手脚的样子”记下来,后面所有异常都是相对这个基线说的。

# 第一次:看应答段、权威段和统计信息 sudo ip netns exec lab-client \ dig @10.10.10.1 www.demo.lab A +noall +answer +authority +stats # 隔 5 秒再查,TTL 应该递减,说明走的是递归上的缓存而不是每次回源 sleep 5 sudo ip netns exec lab-client dig @10.10.10.1 www.demo.lab A +noall +answer # 确认客户端真的在用这个出口 sudo ip netns exec lab-client cat /etc/resolv.conf

+noall +answer只显示应答记录,干净;+authority会带上权威段,能看出应答是不是来自你自建的权威;+stats里有个Query time,第一次通常比第二次大,因为第二次命中缓存。如果第二次的 TTL 没变,说明缓存没生效,先回去看cache-size和查询名是否完全一致——大小写不同在 DNS 里算同一个名字,但有的实现会当成两次查询。

注意:基线这一步不要跳过。第 4 章所有“是否被改写”的判断,比的都是这里记录下来的答案、TTL 和应答来源。

4. 四个模块的参数与判读:dry-run、TTL 和抢答窗口怎么设

4.1 hosts 层:最容易复现也最容易还原

hosts 模块看着最不起眼,实际是排查时最先要排除的一层。它只改本地文件,不涉及任何网络行为,所以复现成本最低,也最容易被误判成“网络问题”。

# 改之前先备份,实验结束后第一件事就是还原 sudo cp -a /etc/hosts /etc/hosts.lab.bak printf '10.10.10.99\twww.demo.lab\n' | sudo tee -a /etc/hosts # getent 走的是系统解析顺序,能直接看出 hosts 有没有压过 DNS getent hosts www.demo.lab sudo ip netns exec lab-client getent hosts www.demo.lab # 还原 sudo mv /etc/hosts.lab.bak /etc/hosts

getent hosts比dig更贴近应用的真实行为,因为应用大多走getaddrinfo,而dig是直接向指定服务器发查询,会绕过 hosts。这两个命令的差异本身就是一个重要信号:如果getent的结果和dig对不上,差异大概率出在 hosts、nsswitch 顺序或者本地 stub 缓存上,跟链路层无关。

这里有个必须提前知道的点:/etc/hosts不属于 network namespace,你在命名空间里改和在外面改是同一个文件。所以第 3 章把resolv.conf挪到/etc/netns/下的做法,对 hosts 不适用。做干净实验的方式是改完之后立刻还原,不要指望命名空间帮你隔离。

4.2 伪造应答模块:先 dry-run,再谈抢答

这个模块的前置条件最苛刻——工具必须能看到客户端的查询,并且能在同一广播域内把应答送回去。参数不多,但每一个都会显著改变结果。

参数作用实验建议值说明
--iface收发报文的接口veth-r1必须是能双向看到查询的那个口
--rule域名匹配规则*.demo.lab支持通配,写太宽会误伤同名后缀
--answer返回的伪造 A 记录10.10.10.99指向你自己的观测机,别指向真实服务
--ttl应答里的 TTL60越短越像攻击者,越长污染窗口越大
--delay应答延迟,单位毫秒0用来控制抢在真实应答之前的时机
--mode应答模式racerace只抢第一次,always每次都应答
--log日志路径/tmp/lab.log后面判读全靠它

第一次一定用--dry-run。它只做规则匹配和日志预演,一个报文都不发,你能从输出里确认命中的 qname 到底有哪些。

# 只预演:确认规则命中的只有你自己的测试域名 sudo ip netns exec lab-resolver \ ./domain-redirect --iface veth-r1 \ --rule '*.demo.lab' \ --answer 10.10.10.99 \ --ttl 60 \ --delay 0 \ --mode race \ --log /tmp/lab.log \ --dry-run

--rule '*.demo.lab'里的通配要写全后缀。写成*demo.lab会把notdemo.lab也匹配进来,这种边界问题在 dry-run 阶段最容易发现,跑起来之后就很难注意到了。--ttl 60是个折中值:设成 0 每次都要重新抢,实验次数一多就很累;设成 3600 又会让客户端缓存太久,下一轮实验前必须手动清。

去掉--dry-run之前,先确认两件事:规则命中的 qname 全在你自己的实验域里;--answer指向的地址是你自己的观测机,不是任何真实在跑的服务。这两条任何一条不满足,就停下来改参数。

4.3 缓存投毒模拟:TTL 是核心变量

这个模块测的是递归解析器的抗抢答能力,参数比上一个多,但真正影响成败的就三个:随机子域前缀的生成方式、并发查询数、以及抢答延迟的分布。

原理不复杂:先让解析器对一个从没查过的名字产生缓存未命中,它在回源的过程中会向外发查询,你在这个窗口里抢先应答。名字每次都必须不一样,否则第二次就命中缓存了,根本不会回源。所以工具会按规则生成一串随机前缀,比如a7f3.demo.lab、9c21.demo.lab,每个只用一次。

并发数决定你能覆盖多大的窗口。单线程串行发查询,窗口很窄,命中率低;开到 50 到 100 并发,能明显感觉到差异,但再往上会被解析器的限速和丢包吃掉,日志里会出现大量超时,看起来像失败,其实是打满了。我一般从 20 起步,每次翻倍,直到命中率不再上升为止,那个拐点就是这个环境的上限。

延迟分布比延迟本身重要。--delay 0意味着应答和查询几乎同时发出,能不能抢到全看网络栈的调度;把延迟做成 0 到 5 毫秒的随机分布,命中率反而更高,因为它覆盖了不同的回源路径耗时。工具的日志里会记录每次的 TXID、发出时刻、应答时刻和判定结果,判读的时候看的是“抢到的那批的延迟落在哪个区间”,而不是平均值。

提示:如果实验目标是评估 DNSSEC 的效果,把第 3 章配置里的dnssec=off改成dnssec=on,再跑同一组参数。你会看到抢答照样成功,但客户端会因为签名校验失败而丢弃应答——这正是 DNSSEC 的价值所在,它不阻止你抢,它阻止你被相信。

4.4 日志判读与报告导出

四个模块的日志格式我统一成了七个字段:时间戳、规则、qname、qtype、answer、抢答耗时、判定。判定只有win、miss、timeout三种,不要加模糊状态,加了之后统计就没法做了。

# 按规则统计命中次数、抢到次数和平均耗时 awk '{hit[$2]++; sum[$2]+=$6; if($7=="win") win[$2]++} END{for(k in hit) printf "%-16s 命中=%d 抢到=%d 均值=%.2fms\n", k, hit[k], win[k], sum[k]/hit[k]}' \ /tmp/lab.log

$2是规则名,$6是抢答耗时,$7是判定。这段统计能一眼看出三件事:规则是不是写宽了(命中数远超预期)、抢答是不是真的成功(抢到数占比)、耗时是不是异常(均值突然变大通常意味着环境里有别的东西在抢带宽)。

报告导出走的是同一份日志,只是按 qname 分组再拼上当时的dig结果做交叉。只有日志和dig结果同时指向“答案被改成了 10.10.10.99”,这一条才算复现成功。单看日志不算,单看页面跳转更不算,这个原则在第 5 章会反复用到。

5. 避坑记录:五类翻车现场与固定排查顺序

5.1 先查这三处,再怀疑工具

遇到“结果不对”的时候,我的固定顺序是:第一步确认客户端到底走哪个解析出口,第二步确认报文有没有真的到达工具所在的接口,第三步才去看应答有没有被客户端接受。这三步能挡掉八成的误判,因为绝大多数“工具失效”其实是环境没配对。

顺序不能反。先去看工具参数,很容易越调越乱,最后连基线是什么样都忘了。第 3 章记录下来的那次dig输出,就是这时候用来对照的。

5.2 五条现象、原因和解决

第一条:在命名空间里改了resolv.conf,回到宿主机发现宿主机的解析也变了。

现象是宿主机突然解析不了内网域名,或者cat /etc/resolv.conf里出现了10.10.10.1。原因是ip netns exec只隔离网络栈,不隔离挂载点,/etc/resolv.conf是同一个文件,你echo进去就是改的宿主机。解决方式是写/etc/netns/lab-client/resolv.conf,ip netns exec会自动把它 bind-mount 进去。已经被改坏的,重新生成一份或者从备份恢复。

第二条:工具日志显示“已应答 N 次”,客户端解析结果一点没变。

现象很迷惑,日志一片成功,dig出来的还是原始地址。原因通常有三个:客户端走了 DoH 或 DoT,直接绕过了本地 53 端口;系统里跑着 stub resolver,缓存没清;或者 nsswitch 里 hosts 排在 dns 前面,而 hosts 里有残留条目。排查方式是先用dig @10.10.10.1直连递归做对照——直连能改、getent不能改,问题就在客户端本地这一层。解决方式是关掉浏览器的安全 DNS、清掉本地缓存、检查 hosts 有没有历史残留。

第三条:第一轮能复现,第二轮怎么都不行。

现象是同样的参数、同样的拓扑,第二轮命中率掉到零。原因是缓存:递归上的缓存、客户端的缓存、连接跟踪表里的会话,三层任意一层有残留都会让第二轮“看起来失效”。解决方式是在实验脚本开头固定加三步:重启递归进程、重建命名空间、清客户端缓存。别指望“等一会儿就好了”,等待时间不确定,实验就不可复现。

第四条:同一个域名两次查出来不一样的地址,在几个值之间来回跳。

现象是dig三次拿到三个不同 IP,看起来像被劫持了。原因是上游本来就是多线或者有内容分发的权威,返回不同地址是正常行为;另一个常见原因是客户端优先走 IPv6,而你只观察了 A 记录。解决方式是固定qtype=A、用+noall +answer只看应答段,客户端侧临时关掉 IPv6(sysctl -w net.ipv6.conf.veth-c.disable_ipv6=1)再对比。判断“是否被改写”一定要基于同一 qtype、同一出口、同一时刻的三元组。

第五条:报告里写“劫持成功”,人工复核发现是假阳性。

现象是页面确实跳到了另一个登录页,但解析记录完全正常。原因是把“应用层跳转”当成了“解析被改写”——重定向、反向代理、前端路由都能做出一样的效果。解决方式是把判定标准收紧成三条同时成立:dig的应答地址变了、TLS 证书的签发对象对不上、TCP 连接的真实目的 IP 和dig结果一致。三条里任何一条对不上,就不算复现成功,老老实实写“未复现”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询