DNSSEC原理和分析

以前对于DNS相关的技术仅仅是略懂皮毛,没有进行全面和深入的了解。最近几年由于各方面的原因,主动被动的接触了不少,包括:

  • 家庭组网和改造:尤其是近两年国内IPv6的普及;
  • 公司兼职网管:负责处理公司的网络问题和VPN问题;
  • VPS:也就是本博客VPS的配置;
  • 项目、任务:内网渗透、应急响应等;
  • 越来越糟糕的网络环境。

其中,影响最大的是DoH、DoT、DNSCrypt等安全DNS协议的出现,同时越来越多的厂商开始部署,主流操作系统也开始逐渐支持。这是一个好的开始,至少在保护个人隐私方面有了更多的选择。同时,我也开始在个人电脑、家庭、公司网络(小范围)中部署、测试安全DNS,为了做出更好的选择,在这过程中学习了很多DNS相关的知识,趁着最近因为疫情仍未复工,整理一些以前的笔记。

今天先从DNSSEC说起。作为一项互联网的基础设施,DNS协议十分重要但过于单纯,也正因如此,DNSSEC很早就被提出,是一种通过扩展DNS协议实现认证的机制,其核心原理是使用数字签名机制和信任链来确保DNS的完整性。通过DNSSEC机制,DNS Resolver或者终端用户可以确保DNS查询到的结果是否可信,即是否为权威域名服务器返回,某种层面上增加了DNS的安全性。

DNSSEC简介

DNSSEC通过扩展资源记录(Resource records)实现认证,增加的几个主要类型如下:

  • DNSKEY:用于校验RRSIG的公钥信息
  • DS:delegation signer,保存DNSKEY的哈希信息
  • RRSIG:resource record signature,RRset的数字签名

和A记录、TXT记录一样, DNSKEY、DS记录均能够通过DNS查询获取:

➜  ~ dig @8.8.4.4 +dnssec thecjw.me. DNSKEY

;; ANSWER SECTION:
thecjw.me.              3599    IN      DNSKEY  257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
thecjw.me.              3599    IN      DNSKEY  256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
thecjw.me.              3599    IN      RRSIG   DNSKEY 13 2 3600 20200313043621 20200113043621 2371 thecjw.me. bWMvyG5jcY0dqCN9pi323O3cUcCODs3lyOZRpT54Tkx6Oz1Zvvp3QLa1 nppPBW9SVpiOodzQbYTF3zCTZZJH0g==

可以看到,正确配置了DNSSEC的服务器在响应中均包含了RRSIG字段,该字段内容包含了域名、时间、签名、算法等。不考虑信任链的情况下,DNS Resolver首先通过DNSKEY记录获取公钥,之后便可通过DNSKEY中记录的Zone Signing Key(公钥)来验证之后的DNS请求是否来自于权威域名服务器。

因为最终完成DNS查询的均是权威域名服务器,所以上述所有值都来自于权威域名服务器。比如本博客使用CloudFlare作为CDN,则权威服务器便是CloudFlare的服务器:

➜  ~ dig @8.8.4.4 +dnssec thecjw.me. NS

;; ANSWER SECTION:
thecjw.me.              21599   IN      NS      asa.ns.cloudflare.com.
thecjw.me.              21599   IN      NS      brett.ns.cloudflare.com.

在开启了DNSSEC后,CloudFlare还负责使用私钥对RRset(资源记录)进行签名,并将结果放入RRSIG记录中,同时将签名对应的公钥放在DNSKEY中,以便于DNS Resolver进行检查。

仅仅通过上述机制,只能部分证明RRset是否完整(功能等同于哈希),因为公钥和签名都来自不可信地方,攻击者可以通过篡改DNSKEY和RRSIG绕过校验。基于这种最基本的校验逻辑,DNSSEC进一步使用DS和Key-Signing Key机制,构建了DNSSEC的信任链。

DNSSEC信任链

为了实现信任链,DNSSEC中的签名信息被分为了Zone-Signing KeyKey-Signing Key

Zone-Signing Key(ZSK),顾名思义,用于对Zone内所有的资源记录进行签名,由权威认证服务器生成、签名。权威认证服务器在每次DNS查询的时候,都会使用ZSK对查询结果(资源记录)进行数字签名,并将数字签名放在RRSIG记录中。ZSK可以通过DNSKEY记录获取。

为了防止ZSK被修改,DNSSEC还使用了一对叫做Key-Signing Key(KSK)的公私钥对。DNSKEY记录的RRSIG会使用KSK进行签名,并将结果放在DNSKEY的RRSIG记录中。在DNSKEY记录中,会同时包含ZSK和KSK。所以,在一个zone中,除DNSKEY记录外,其他的记录均由ZSK签名。

即便如此,还是无法构成信任链。此时就需要DS记录将当前的Zone和TLD的Zone Signning Key进行连接,构成信任链。DS记录中包含了KSK的哈希值,DNS Resolver可以通过DS记录确定KSK的完整性,进而确定DNSKEY记录的完整性,最后确定当前zone其他DNS记录的完整性。注意到,DS记录也包含RRSIG记录,而该记录的签名由上一级的域名服务器使用私钥生成,该私钥对应的公钥在上一级DNS服务器的DNSKEY记录中。上一级服务器的DNSKEY校验过程和本级的类似,如此往复,直到根服务器,也就是DNSSEC Root。DNSSEC信任根的信息可以在IANA DNSSEC的网站找到。

所以,DNSSEC通过DS记录和使用上一级Zone-Signing Key对Zone的KSK的哈希进行签名的方法,构建了自身的信任链。这种体系相对简单,不涉及到密钥的管理等问题,而且配置相对简单。以在CloudFlare开启为例,开启DNSSEC时,CloudFlare会为当前的Zone生成特定的KSK和ZSK,同时提供KSK的哈希(包括算法),也就是DS记录的信息;将DS记录的信息提交到域名提供商(当前的域名是me.)即可。

整个DNSSEC的过程图示如下:

使用dig校验

为了更好的理解整个过程,下面使用dig进行从下往上的校验流程分析。

除了通过手工的方式,以下工具还可以更清晰的理解、校验DNSSEC。使用+sigchase参数,可以让dig输出DNSSEC校验的过程,该参数需要指定信任的根证书,实践中,应该通过安全的方式获取,这里为了方便,直接使用dig查询根节点的DNSKEY并存储,命令如下:

dig @8.8.8.8 . DNSKEY | grep -Ev '^($|;)' > root.keys

接下来,可以直接使用dig进行DNSSEC的校验:

dig @8.8.8.8 +sigchase +trusted-key=./root.keys thecjw.me. A

结果比较长,需要分步骤来看。首先dig输出了A记录的查询结果和对应的RRSIG:

当前域名的A记录

从RRSIG中可以看到,当前的DNS记录使用了KeyTag为34505的ZSK进行签名,而密钥需要通过DNSKEY记录获取:

当前域名的DNSKEY记录

上述记录即查询DNSKEY的结果,该记录也包含了RRSIG,与A记录不同,使用了KeyTag为2371的key进行签名,注意DNSKEY记录的签名由KSK完成。通过+multiline参数,可以看到key的信息:

dig @8.8.8.8 +multiline thecjw.me. DNSKEY

和预期的一样,2371为KSK,34505为ZSK。目前,还需要DS记录来验证KSK是否完整,回到dig的输出:

当前域名的DS记录

上图的dig日志中,首先获取了DS记录。这里需要注意的是,DS记录同样进行了签名,通过RRSIG记录,可以看到,负责对DS签名的KeyTag为39077,不属于本Zone的ZSK和KSK,而是属于上一层TLD的ZSK,之后的校验就要寻找KeyTag为39077的公钥,用于DNSSEC校验信任链。之后的log为使用DNSKEY、DS进行校验的过程输出。首先通过DNSKEY中的ZSK校验A记录是否完整,然后通过DS校验KSK是否完整,如果KSK完整,则DNSKEY完整,进而被ZSK签名的A记录完整。此时就该向上继续了,现在要做的是获取me.的DNSKEY:

me.的DNSKEY记录

可以看到,me.的DNSKEY记录中包含了4个KEY,继续使用+multiline参数查看具体的KeyTag:

dig @8.8.8.8 +multiline me. DNSKEY

由上图可知,me.的DNSKEY如下:

  • 39077:ZSK
  • 46829:ZSK
  • 2569:KSK
  • 53233:KSK

其中,39077就是对本域名进行签名的公钥。而DNSKEY记录中的RRSIG信息显示,该DNSKEY分别被两个KSK签名。但39077这个ZSK也对DNSKEY签名了,这里没搞懂。有了DNSKEY之后,需要me.的DS记录,用于校验KSK的有效性:

me. 的DS记录

具体过程不再赘述,和之前的校验过程类似,因为包含了两个KSK,所以DS记录中有两条,验证的过程中,只会对和校验链相关的进行验证。同时需要注意,该DS记录由KeyTag为33853的Key进行签名,而该签名来自于根证书。所以需要继续向上进行校验:

根域名服务器的DNSKEY信息

再一次的,查看根域名服务器的DNSKEY信息:

dig @8.8.8.8 +multiline . DNSKEY

如预期所料,根域名服务器的DNSKEY中包含了一个ZSK,正是负责给me顶级域名服务器DS记录签名 33853。注意到,该DNSKEY记录使用了20326进行签名,而顶级服务器没有DS记录,那应该如何校验呢?实际上这里已经是根证书的地方,20326的证书即DNSSEC的可信根证书,可以通过IANA的网站获取。20326的哈希如下:

<TrustAnchor id="380DC50D-484E-40D0-A3AE-68F2B18F61C7" source="http://data.iana.org/root-anchors/root-anchors.xml">
<Zone>.</Zone>
<KeyDigest id="Kjqmt7v" validFrom="2010-07-15T00:00:00+00:00" validUntil="2019-01-11T00:00:00+00:00">
<KeyTag>19036</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>
49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
</Digest>
</KeyDigest>
<KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>
E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
</Digest>
</KeyDigest>
</TrustAnchor>

如果一个DNS请求的数据能够通过上述校验,则证明在DNS响应的过程中,DNS的结果没有被篡改。

其他工具

DNSSEC Analyzer/DNSViz

DNSSEC Analyzer是VeriSign推出的一个在线DNSSEC检测工具,可以快速的对DNSSEC进行检测,同时输出较为关键的Log。如果DNSSEC配置正常,效果如下:

DNSSEC配置正确

图中展示了各个Zone中参与运算的Key和校验的过程。从运行过程来看,DNSSEC Analyzer是通过自顶向下的方式进行校验。另外两个DNSSEC failed的效果如下:

模拟DNSKEY被篡改的情况
DS存在但没有DNSKEY、RRSIG,模拟DNSKEY被篡改

VeriSign的另外一个Dnsviz工具则以可视化的方式展示了整个DNSSEC校验的过程,并且还在Github开源了。

dnspython

使用python的好处就是可以看到一些dig中需要解析的数据,同时进行一些其他的测试。dnspython库可以完成DNSSEC的校验,但校验信任链需要自行完成。另外,dnspython进行DNSSEC校验依赖于pycryptodome和ecdsa两个库,由于在extras_require中,需要自行安装,否则会校验失败:

    'tests_require': ['typing ; python_version<"3.5"'],
    'extras_require': {
        'IDNA': ['idna>=2.1'],
        'DNSSEC': ['pycryptodome', 'ecdsa>=0.13'],
        },

进行校验的演示代码如下:

target = "thecjw.me."
default_dns_server = "8.8.8.8"

query = dns.message.make_query(target, dns.rdatatype.DNSKEY)
response = dns.query.udp(query, default_dns_server, 3)
dnskeys = response.answer[0]

query = dns.message.make_query(target, dns.rdatatype.A, want_dnssec=True)
response = dns.query.udp(query, default_dns_server, timeout=5)

answer = response.answer

if answer[0].rdtype == dns.rdatatype.RRSIG:
    rrsig, rrset = answer
elif answer[1].rdtype == dns.rdatatype.RRSIG:
    rrset, rrsig = answer

keys = {dns.name.from_text(target): dnskeys}
dns.dnssec.validate(rrset, rrsig, keys)

注意,这段代码并不严谨,完整的代码请参考electrum工程中dnssec.py的实现。

总结

DNSSEC通过数字签名实现了一种认证机制,确保了DNS Resolver有方法校验查询到的DNS记录是否来自可信来源,对于中间人攻击有一定的效果。但DNSSEC的缺点也比较明显,一方面是DNSSEC的普及度并不高,对于DNS Resolver而言必须做兼容处理,而攻击者完全可以对DNSSEC相关的数据继续剥离,欺骗DNS Resolver目标没有开启DNSSEC,在国内这种情况十分常见。

同时,开启DNSSEC的网站数目相对较少,测试了一些大厂的域名,基本都没有设置。也正因为如此,大多数公共DNS基本没有对DNSSEC进行处理,测试中发现,在使用114DNS的情况下,http://dnssec.fail/依旧可以访问:

使用dig和GoogleDNS进行对比的结果如下:

➜  ~ dig @8.8.4.4 +dnssec dnssec.fail

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 512
;; QUESTION SECTION:
;dnssec.fail.                   IN      A

➜  ~ dig @114.114.114.114 +dnssec dnssec.fail

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;dnssec.fail.                   IN      A

;; ANSWER SECTION:
dnssec.fail.            2822    IN      A       194.63.248.47

最后,DNSSEC并没有对DNS记录进行加密,还是存在被追踪、监视的风险。所以,DNSSEC正如其名,只是一个安全的扩展,并不能真正的解决DNS的安全和终端用户的安全需求。目前DoT、DoH、DNSCrypt配合可信DNS Resolver的方式,才真正的解决了DNS的安全问题,具体细节以后再写。

参考

发表评论