GFW的DNS包伪造的简单研究

16 分钟读完

最近稍微研究了一下这个,就拿来写点东西吧。。

其实早两个礼拜就打算要写这个了,然而因为在那之前把KDE删了,终端模拟器也就从Konsole换到了Hyper。Hyper本身相当不错,基于Electron,支持插件、主题,还可以用CSS做简单的(其实复杂的也可以。。)自定义。

然而它底层使用的hterm却对CJK字符相当不友好:要加CSS强行设置字符宽度才能正常显示,还不支持IME,所以就没办法在用vim的时候打中文了。。近几天Hyper在1.x升级到2.x以后把hterm换成了Xterm.js,终于能够完美支持CJK字符了(虽然目前还是canary版,bug超级多,插件兼容性也不好。。),所以就终于可以继续写博客了。。

本文假设读者有关于DNS、UDP、TCP的基础知识,不了解的话就不要看了。。

DNS协议本身的问题

在DNS被设计出来的年代,互联网才刚刚出现,网络安全也并不受到重视,所以最初的DNS协议本身几乎没有安全方面的考量,它默认使用的是无连接的UDP协议,也没有任何的加密或认证措施,这使得DNS包伪造变得及其简单。尤其是在攻击者拥有网络基础设施的控制权时,只要向DNS客户端发送一个简单地构造的响应包,并且设置好UDP包的源地址和源端口,客户端没有任何办法来分辨伪造的数据包。虽然DNS也支持使用TCP进行传输,但这也仅仅是略微提高了攻击的难度而已。

国内DNS解析现状

由于众所周知的原因,大量国外的网站在中国是被封锁的,而基于DNS包伪造的封锁可以说是目前最常用的方式了。GFW在出口路由上监控经过的DNS请求,发现有对黑名单内的域名的请求,就会实行相应的封锁措施。

对于使用UDP的DNS请求,GFW会伪装成DNS服务器,给查询者返回伪造的DNS响应包。相对于实际的DNS响应,伪造的响应显然能够更早地抵达查询者,并被误认为是DNS服务器的响应,随后抵达的真正的响应包却会被丢弃,这被称为“抢答”。之前也说了,对于GFW这种部署在网络基础设施上的攻击者,进行DNS查询的客户端没有办法分辨出伪造的请求。

比如下面是一个向Google的公共DNS服务器8.8.8.8查询www.google.com的情形(dig @8.8.8.8 www.google.com)。

查询国外服务器的抓包结果

可以看到前两个看似是8.8.8.8发回响应把www.google.com指向了93.46.8.89,这个IP地址ping不通,80和443端口打不开,和Google也没有任何关系,所以这两个响应很明显是GFW伪造的,最后那个216.58.200.36则是8.8.8.8返回的真正的响应。

GFW发送的抢答包中使用的伪造的IP地址一般是没有被运行Web服务器的或是在相关国际标准中被保留,不会出现在Internet上的IP地址。但也有少部分情况使用的是一些已经被封锁的国外网站的IP地址,例如*.onedrive.live.com就是这种情况,有时抢答包中的IP地址是属于Facebook或Dropbox的。

UPDATE 目前有不少域名已经是在返回被封锁的其他国外网站的IP的了,这使得下面提到的利用响应包中的IP地址过滤对于这些网站基本无效了。

$ dig @8.8.8.8 onedrive.live.com

; <<>> DiG 9.11.2 <<>> @8.8.8.8 onedrive.live.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60482
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;onedrive.live.com.             IN      A

;; ANSWER SECTION:
onedrive.live.com.      182     IN      A       74.117.178.43

;; Query time: 14 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Sep 27 19:29:07 CST 2017
;; MSG SIZE  rcvd: 51

对于使用TCP的情况,GFW则是使用了与应对其他明文TCP连接时一样的连接重置的方法,具体信息可以去看维基百科上的相关内容

国内的递归DNS服务器

直接查询国外的DNS服务器毫无疑问会受到之前说的封锁措施的影响,DNS抢答只会发生在通信经过部署了GFW的出口路由时,所以直接对国内的DNS服务器的请求不会被抢答。但是这些服务器本身要得知被查询的域名的IP地址,还是要向国外的DNS服务器进行查询并存储到缓存中,这一过程会被GFW抢答所影响,所以最终返回给用户的响应也是错误的。这种间接的DNS包伪造被称为DNS缓存污染。

查询国内服务器的抓包结果

可以看到上图中对114.114.114.114进行的查询并没有被抢答,只有一个响应包,但其中的IP地址仍旧是错误的。

伪造的DNS包的来源

从之前的图片中可以发现,每次进行DNS查询时,都会收到两个由GFW伪造的响应包,那么它们究竟来自哪里呢?我利用IP协议的TTL并配合mtr进行路由追踪做了下分析。具体方法是用printf构造DNS请求包,并用nc将其发送给DNS服务器,但在发出去的IP包上设置较小的TTL,这样就可以大致判断DNS请求包是在哪一层路由时被GFW发现了,然后再用mtr得到的路由追踪信息就可以得到对应的路由设备的IP地址了。

那个稍早到达的DNS抢答包的来源较容易判断。我用本地的电信宽带测试的结果是对各个国外DNS服务器的请求大都在202.97.57.x上被检测到并发送了抢答包,而这个IP地址段是属于电信骨干网的。前几天我也用移动宽带进行了测试,结果是抢答包来自联通骨干网。

而那个较迟到达的抢答包的来源就难以判断了,要把TTL设置到大约20+才能收到抢答包,而那个位置的路由要么已经是国外的网络了,要么是不存在的(例如我这里的电信宽带连接208.67.222.222(OpenDNS)只需经过不到20次路由)。GFW极有可能对发送它的服务器的位置进行了伪装。

解决方法

当然,GFW的这种封锁方式还是有漏洞可寻的。一方面,如今DNS的安全性已经受到了关注,有不少用于增强DNS安全性的方案被提了出来,甚至标准化了;另一方面,GFW的DNS包伪造程序本身也并不完善。下面我分类列举了一些可以防止在查询国外DNS服务器时被GFW干扰的方法,国内服务器由于本身已经被污染了,就没有办法了。

要提一下的是,修改系统的hosts文件也是解决这一问题的方法之一,但它是通过手动指定域名和IP地址的对应,完全绕过了DNS机制。而这里仅讨论真正和DNS服务器通信时的措施。

密码学

最直接的解决方法就是通过密码学措施,使得GFW无法监听、伪造DNS通信。

DNSCrypt

DNSCrypt是一个为DNS提供安全机制的协议,它的官方实现是一个开源项目。虽然它默认使用被国内ISP严重干扰的UDP通信,但也可以支持使用TCP。目前这个协议在国内并没有被封锁的迹象,世界上也有许多提供DNSCrypt的DNS服务器(比如OpenDNS),而且还有一个使用起来相当方便的GUI程序Simple DNSCrypt。所以DNSCrypt可以说是相当理想的防DNS抢答的方案了。

DNS over HTTPS

这是Google的公共DNS服务器提供的服务,使用HTTPS提供DNS服务。由于HTTPS本身可以防御中间人攻击,所以GFW无法伪造响应,也有相当不错的开源客户端实现。但是提供这个服务的dns.google.com已经被封锁了,想要使用的话必须配合其他翻墙方式才行。

UPDATE 现在DNS over HTTPS (DoH)已经标准化(RFC 8484)。知名CDN提供商Cloudflare最近也开始提供支持DoH的DNS解析服务,同时,最新版的Firefox也支持了DoH,可以在选项->网络设置->设置->启用基于HTTPS的DNS处配置。目前来看,相比于DoT,DoH前景更加好一些,虽然标准化时间比DoT迟,但已经有了支持它的公共服务器。它可以利用现有的HTTPS客户端,所以实现相对简单,而且可以利用HTTP/2提供的长连接和多路复用特性来优化响应速度。

DNS over TLS (DoT)

这个协议类似于前面两个,但使用TLS来为DNS提供安全性,已经标准化了(RFC 7858)。但是这个协议还相当的新,目前并没有提供相关支持的公共DNS服务器。

DNESEC

DNSSEC通过给DNS记录加上数字签名来防止DNS包的伪造,同样已经标准化了(RFC 4033等)。但遗憾的是,由于启用DNSSEC需要DNS客户端和各级DNS服务器全都支持才行,目前DNSSEC的部署并不广泛,没有支持DNSSEC的域名,就没有办法受到DNSSEC保护了。

过滤

另一类解决方法是通过一些手段辨识出GFW伪造的DNS响应,并过滤掉相应的响应包,只保留真正来自DNS服务器的响应包。

过滤IP地址

由于GFW使用的伪造的IP地址只有有限的那么几个,只要拿一个IP地址的黑名单进行过滤就基本可以解决掉。ChinaDNSPcap_DNSProxy都提供了这一功能。

EDNS

再观察一下之前那个查询8.8.8.8时的抓包结果:

查询国外服务器的抓包结果

可以发现发出的DNS请求中是带有OPT记录的,8.8.8.8返回的响应同样也有,但是GFW伪造的响应却没有。也就是说,GFW的DNS抢答程序并不支持EDNS,利用这个,在和支持EDNS的DNS服务器通信时就可以过滤掉伪造的响应包。不过目前似乎并没有利用了这一原理的实现。

混淆

GFW的DNS抢答程序对于DNS协议的支持并不完善,之前也说了,它并不支持EDNS。另一个它不支持的标准是用于DNS消息压缩的指针机制(RFC 1035中的4.1.4节),所以可以在发送的DNS请求中使用这一机制,使得GFW无法正确识别请求,自然也就无法进行抢答了。

在早几年的时候,GFW对于指针是完全无法正常处理的。只需在DNS消息的域名部分的末尾添加一个指向消息中任意0字节的指针,正确实现的DNS服务器会解析这个指针,并且认为这个域名已经结束,而GFW因为不识别指针,会把指针当作长度信息处理。由于指针的开头两bit必须为1,GFW解析出来的长度信息会相当大,导致所谓的缓冲区过读(Buffer over-read),读取大量无关的内存并且在伪造的DNS响应中发给DNS客户端,这样的响应自然是不符合DNS协议的格式的。ChinaDNSPcap_DNSProxy也都提供了利用这一漏洞的功能。

而现在GFW已经能在一定程度上识别指针了。之前提到过,来自GFW的抢答包有两个,而对于指针,较早到达的那个响应仍然不能正确处理指针,是会返回包含其服务器内存信息的不合法的响应包的,较迟到达的那个响应包却是正确处理了绝大多数的指针的情况的。

例如我现在构造一个包含指针的DNS请求:

包含指针的DNS请求

然后将其发送给8.8.8.8

用包含指针的请求查询国外服务器的抓包结果

再来看看那个已经被Wireshark识别为“Malformed Packet”的响应包的内容:

格式错误的响应包

途中红线之前的还是正常的DNS消息,之后就完全是乱码了。

虽然较早到达的那个抢答包已经能够处理绝大多数包含指针的请求了,但还是有漏洞的,例如下面这个请求包:

另一个包含指针的DNS请求

GFW完全无法识别这个请求,连抢答包都没有了:

新的抓包结果

不过目前也没有利用这一漏洞的实现。

代理

当然,通过各种类型的代理协议连接到国外的服务器,直接在国外服务器上进行DNS解析,这样也可以达到绕过GFW的DNS抢答的效果。但这个方法要么需要花钱租用服务器,要么就得使用安全性未知的第三方服务器,这里就不展开了。

留下评论