找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
广告投放联系QQ68610888
查看: 11527|回复: 29

OpenWRT反DNS劫持总结

[复制链接]
本文是我正在写的《购物路由器的原理与实现》中的一部分,全文链接:https://docs.google.com/document ... SD1CZV4XY1OMG8/edit
目前写到了穿墙部分,基于OpenWRT路由器做了一些穿墙实验。第一部分关于DNS劫持的已经完成。摘录如下:

实验环境准备

        穿墙比购物要复杂得多,但也有意思得多。本章节以实验为主。实验的设备是家庭用的路由器,我用的是水星4530R。需要有公网IP。刷的操作系统是OpenWRT Attitude Adjustment 12.09 rc-1版本。使用的包有:
NetfilterQueue(https://github.com/fqrouter/fqrouter 中有)
bind-dig
shadow
dpkt (不是OpenWRT的包,是python的 http://dpkt.googlecode.com/files/dpkt-1.7.tar.gz
        本文并不打算详细讲解实验环境的设置。对于有OpenWRT编译和刷机经验的朋友可能可以按照我的叙述重建出实验环境来。整个实验的关键在于
公网上的ip地址
Linux
python
python访问netfilter queue的库
        如果你有一台公网上的Linux机器,安装了Python和Python的NetfilterQueue,也可以进行同样的实验。
        如果你使用的是路由器,需要验证你有公网ip。这个可以访问ifconfig.me来证实。其次要保证路由器是OpenWRT的并且有足够的空间安装python-mini。到这里基本上都和普通的OpenWRT刷机没有什么两样。重点在于:
安装Python的NetfilterQueue

        OpenWRT提供了NetfilterQueue的C的库。但是使用C来做实验太笨重了。所以我选择了Python。但是Python的NetfilterQueue的库没有在OpenWRT中。下载https://github.com/fqrouter/fqrouter 解压后可以得到一个名字叫fqrouter的目录。然后给feeds.conf添加一行src-link fqrouter /opt/fqrouter/package。把/opt/fqrouter替换为你解压的目录。然后scripts/feeds update -a,再执行scripts/feeds install python-netfilterqueue就添加好了。然后在make menuconfig中选择Languages=>Python=>python-netfilterqueue。
        有了这个库就赋予了我们使用Python任意抓包,修改包和发包的能力。在OpenWRT上,除了python没有第二种脚本语言可以如此简单地获得这些能力。
安装Python的dpkt

        能够抓取和发送IP包之后,第二个头疼的问题是如何解析和构造任意的IP包。Python有一个库叫dpkt可以帮我们很好地完成这项任务。这是我们选择Python做实验的第二个重要理由。
        在路由器上直接下载http://dpkt.googlecode.com/files/dpkt-1.7.tar.gz,然后解压缩,拷贝其中的dpkt目录到/usr/lib/python2.7/site-packages下。
DNS劫持观测

        我们要做的第一个实验是用python代码观测到DNS劫持的全过程。
应用层观测

        dig是DNS的客户端,可以很方便地构造出我们想要的DNS请求。dig @8.8.8.8 twitter.com。可以得到相应如下

; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5494
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;twitter.com.                        IN        A

;; ANSWER SECTION:
twitter.com.                4666        IN        A        59.24.3.173

;; Query time: 110 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Jan 13 13:22:10 2013
;; MSG SIZE  rcvd: 45

        可以很清楚地看到我们得到的错误答案59.24.3.173。
抓包观测

        使用iptables我们可以让特定的IP包经过应用层的代码,从而使得我们用python观测DNS查询过程提供了可能。代码如下,保存文件名dns_hijacking_obversation.py(https://gist.github.com/4524294):

from netfilterqueue import NetfilterQueue
import subprocess
import signal
def observe_dns_hijacking(nfqueue_element):
   print('packet past through me')
   nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)
def clean_up(*args):
   subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
   subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
   subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
   subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
   print('running..')
   nfqueue.run()
except KeyboardInterrupt:
   print('bye')

执行python dns_hijacking_observation.py,再使用dig @8.8.8.8 twitter.com应该可以看到package past through me。这就说明DNS的请求和答案都经过了python代码了。
        上一步主要是验证NetfilterQueue是不是工作正常。这一步则要靠dpkt的了。代码如下,文件名相同(https://gist.github.com/4524299):

from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket

def observe_dns_hijacking(nfqueue_element):
   try:
       ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
       dns_packet = dpkt.dns.DNS(ip_packet.udp.data)
       print(repr(dns_packet))
       for answer in dns_packet.an:
           print(socket.inet_ntoa(answer['rdata']))
       nfqueue_element.accept()
   except:
       traceback.print_exc()
       nfqueue_element.accept()

nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)

def clean_up(*args):
   subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
   subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)

signal.signal(signal.SIGINT, clean_up)

try:
   subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
   subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
   print('running..')
   nfqueue.run()
except KeyboardInterrupt:
   print('bye')

执行python dns_hijacking_observation.py,再使用dig @8.8.8.8 twitter.com应该可以看到类似如下的输出:

DNS(ar=[RR(type=41, cls=4096)], qd=[Q(name='twitter.com')], id=8613, op=288)
DNS(an=[RR(name='twitter.com', rdata=';\x18\x03\xad', ttl=19150)], qd=[Q(name='twitter.com')], id=8613, op=33152)
59.24.3.173
DNS(an=[RR(name='twitter.com', rdata='\xc7;\x95\xe6', ttl=27), RR(name='twitter.com', rdata='\xc7;\x96\x07', ttl=27), RR(name='twitter.com', rdata="\xc7;\x96'", ttl=27)], ar=[RR(type=41, cls=512)], qd=[Q(name='twitter.com')], id=8613, op=33152)
199.59.149.230
199.59.150.7
199.59.150.39

可以看到我们发出去了一个包,收到了两个包。其中第一个收到的包是GFW发回来的错误答案,第二个包才是正确的答案。但是由于dig只取第一个返回的答案,所以我们实际看到的解析结果是错误的。

 楼主| | 显示全部楼层
观测劫持发生的位置

        利用IP包的TTL特性,我们可以把TTL值从1开始递增,直到我们收到错误的应答为止。结合TTL EXECEEDED ICMP返回的IP地址,就可以知道DNS请求是在第几跳的路由器分光给GFW的。代码如下(https://gist.github.com/4524927):

from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket
import sys

DNS_IP = '8.8.8.8'

# source http://zh.wikipedia.org/wiki/%E5 ... 8%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
   '4.36.66.178',
   '8.7.198.45',
   '37.61.54.158',
   '46.82.174.68',
   '59.24.3.173',
   '64.33.88.161',
   '64.33.99.47',
   '64.66.163.251',
   '65.104.202.252',
   '65.160.219.113',
   '66.45.252.237',
   '72.14.205.99',
   '72.14.205.104',
   '78.16.49.15',
   '93.46.8.89',
   '128.121.126.139',
   '159.106.121.75',
   '169.132.13.103',
   '192.67.198.6',
   '202.106.1.2',
   '202.181.7.85',
   '203.161.230.171',
   '207.12.88.98',
   '208.56.31.43',
   '209.36.73.33',
   '209.145.54.50',
   '209.220.30.174',
   '211.94.66.147',
   '213.169.251.35',
   '216.221.188.182',
   '216.234.179.13'
}

current_ttl = 1

def locate_dns_hijacking(nfqueue_element):
   global current_ttl
   try:
       ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
       if dpkt.ip.IP_PROTO_ICMP == ip_packet['p']:
           print(socket.inet_ntoa(ip_packet.src))
       elif dpkt.ip.IP_PROTO_UDP == ip_packet['p']:
           if DNS_IP == socket.inet_ntoa(ip_packet.dst):
               ip_packet.ttl = current_ttl
               current_ttl += 1
               ip_packet.sum = 0
               nfqueue_element.set_payload(str(ip_packet))
           else:
               if contains_wrong_answer(dpkt.dns.DNS(ip_packet.udp.data)):
                   sys.stdout.write('* ')
                   sys.stdout.flush()
                   nfqueue_element.drop()
                   return
               else:
                   print('END')
       nfqueue_element.accept()
   except:
       traceback.print_exc()
       nfqueue_element.accept()


def contains_wrong_answer(dns_packet):
   for answer in dns_packet.an:
       if socket.inet_ntoa(answer['rdata']) in WRONG_ANSWERS:
           return True
   return False

nfqueue = NetfilterQueue()
nfqueue.bind(0, locate_dns_hijacking)

def clean_up(*args):
   subprocess.call('iptables -D OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
   subprocess.call('iptables -D INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
   subprocess.call('iptables -D INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)

signal.signal(signal.SIGINT, clean_up)

try:
   subprocess.call('iptables -I INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)
   subprocess.call('iptables -I INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
   subprocess.call('iptables -I OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
   print('running..')
   nfqueue.run()
except KeyboardInterrupt:
   print('bye')

执行 dig +tries=30 +time=1 @8.8.8.8 twitter.com 可以得到类似下面的输出:

=== 隐去 ===
=== 隐去 ===
=== 隐去 ===
219.158.100.166
219.158.11.150
* 219.158.97.30
* * 219.158.27.30
* 72.14.215.130
* 209.85.248.60
* 216.239.43.19
* * END

出现*号前面的那个IP就是挂了GFW的路由了。脚本只能执行一次,第二次需要重启。另外同一个DNS不能被同时查询,把8.8.8.8改成你没有在用的DNS。这个脚本的一个“副作用”就是dig返回的答案是正确的了,因为错误的答案被丢弃了。
反向观测

        前面我们已经知道从国内请求国外的DNS服务器大体是怎么一个被劫持的过程了。接下来我们在国内搭建一个服务器,从国外往国内发请求,看看是不是可以观测到被劫持的现象。
        把路由器的WAN口的防火墙打开。配置本地的dnsmasq为使用非标准端口代理查询从而保证本地做dig查询的时候可以拿到正确的结果。然后在国外的服务器上执行

dig @国内路由器ip twitter.com

        可以看到收到的答案是错误的。执行前面的路由跟踪代码,结果如下:

=== 隐去 ===
=== 隐去 ===
=== 隐去 ===
115.160.187.13
213.248.76.73
219.158.33.181
219.158.29.129
219.158.19.165
* 219.158.96.225
* * * 219.158.101.233
END

        可以看到不但有DNS劫持,而且DNS劫持发生在非常靠近国内路由器的位置。这也证实了论文中提出的观测结果。GFW并没有严格地部署在购物境前第一眺的位置,而是更加靠前。并且是双向的,至少DNS劫持是双向经过实验证实了。
       
回复

使用道具 举报

 楼主| | 显示全部楼层

通过避免GFW重建请求反DNS劫持

使用非标准端口

        这个实验就非常简单了。使用53之外的端口查询DNS,观测是否有错误答案被返回。

dig @208.67.222.222 -p 5353 twitter.com
       
        使用的DNS服务器是OpenDNS,端口为5353端口。使用非标准端口的DNS服务器不多,并不是所有的DNS服务器都会提供非标准端口供查询。结果如下:

; <<>> DiG 9.9.1-P3 <<>> @208.67.222.222 -p 5353 twitter.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5367
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 8192
;; QUESTION SECTION:
;twitter.com.                        IN        A

;; ANSWER SECTION:
twitter.com.                5        IN        A        199.59.150.39
twitter.com.                5        IN        A        199.59.148.82
twitter.com.                5        IN        A        199.59.148.10

;; Query time: 194 msec
;; SERVER: 208.67.222.222#5353(208.67.222.222)
;; WHEN: Mon Jan 14 11:47:46 2013
;; MSG SIZE  rcvd: 88
       
可见,非标准端口还是可以得到正确结果的。但是这种穿墙并不能被应用程序直接使用,因为几乎所有的应用程序都不支持使用非标准端口查询。有很多种办法把端口变成53端口能用。

使用本地DNS服务器转发(dnsmasq,pdnsd)
用NetfilterQueue改写IP包
用iptables改写IP包:iptables -t nat -I OUTPUT --dst 208.67.222.222 -p udp --dport 53  -j DNAT --to-destination 208.67.222.222:5353

使用TCP查询

        这个实验就更加简单了,也是一条命令:

dig +tcp @8.8.8.8 twitter.com

        GFW在日常是不屏蔽TCP的DNS查询的,所以可以得到正确的结果。但是和非标准端口一样,几乎所有的应用程序都不支持使用TCP查询。已知的TCP转UDP方式是使用pdnsd或者unbound转(http://otnth.blogspot.jp/2012/05/openwrt-dns.html?m=1)。
使用单向代理

        严格来说单向代理并不是穿墙,因为它仍然需要在国外有一个代理服务器。使用代理服务器把DNS查询发出去,但是DNS查询并不经由代理服务器而是直接发回客户端。这样的实现在目前有更好的反劫持的手段(比如非标准端口)的情况下并不是一个有实际意义的做法。但是对于观测GFW的约束机制还是有帮助的。据报道在敏感时期,对DNS不仅仅是劫持,而是直接丢包。通过单向代理可以观测丢包是针对出境流量的还是入境流量的。
        客户端需要使用iptables把DNS请求转给NetfilterQueue,然后用python代码把DNS请求包装之后发给中转代理。对于应用程序来说,这个包装的过程是透明的,它仍然认为请求是直接发给DNS服务器的。
        客户端代码如下,名字叫smuggler.py(https://gist.github.com/4531012):

from netfilterqueue import NetfilterQueue
import subprocess
import signal
import traceback
import socket

IMPERSONATOR_IP = 'x.x.x.x'
IMPERSONATOR_PORT = 19840

udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

def smuggle_packet(nfqueue_element):
   try:
       original_packet = nfqueue_element.get_payload()
       print('smuggled')
       udp_socket.sendto(original_packet, (IMPERSONATOR_IP, IMPERSONATOR_PORT))
       nfqueue_element.drop()
   except:
       traceback.print_exc()
       nfqueue_element.accept()

nfqueue = NetfilterQueue()
nfqueue.bind(0, smuggle_packet)

def clean_up(*args):
   subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)

signal.signal(signal.SIGINT, clean_up)

try:
   subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)
   print('running..')
   nfqueue.run()
except KeyboardInterrupt:
   print('bye')

        服务器端代码如下,名字叫impersonator.py:

import socket
import dpkt.ip

def main_loop(server_socket, raw_socket):
21while True:
                packet_bytes, from_ip = server_socket.recvfrom(4096)
                packet = dpkt.ip.IP(packet_bytes)
                dst = socket.inet_ntoa(packet.dst)
                print('%s:%s => %s:%s' % (socket.inet_ntoa(packet.src), packet.data.sport, dst, packet.data.dport))
                raw_socket.sendto(packet_bytes, (dst, 0))

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
        server_socket.bind(('0.0.0.0', 19840))
        raw_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
        try:
                raw_socket.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1)
                main_loop(server_socket, raw_socket)
        finally:
                raw_socket.close()
finally:
        server_socket.close()
       
在路由器上运行的时候要把WAN的防火墙规则改为接受INPUT,否则进入的UDP包会因为没有对应的出去的UDP包而被过滤掉。这是单向代理的一个缺陷,需要在墙上开洞。把防火墙整个打开是一种开洞的极端方式。后面专门讨论单向代理的时候会有更多关于防火墙凿洞的讨论。
第二个运行的条件是服务器所在的网络没有对IP SPROOFING做过滤。服务器实际上使用了和GFW发错误答案一样的技术,就是伪造SRC地址。通过把SRC地址填成客户端所在的IP地址,使得DNS查询的结果不需要经过代理服务器中装直接到达客户端。
回复

使用道具 举报

 楼主| | 显示全部楼层

通过丢弃错误答案反DNS劫持

        前两种方式都是针对GFW的重建这一步。因为GFW没有在日常的时候监听所有UDP端口以及监听TCP流量,所以非标准端口或者TCP的DNS查询可以被放行。选择性丢包则针对的是GFW的应对措施。既然GFW发错误的答案回来,只要我们不认它给的答案,等正确的答案来就是了。有两篇相关文档
使用ipfilter过滤GFW滴DNS污染
AntiDNSPoisoning

        改写成python脚本是这样的(https://gist.github.com/4530465):

import sys
import subprocess

# source http://zh.wikipedia.org/wiki/%E5 ... 8%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
   '4.36.66.178',
   '8.7.198.45',
   '37.61.54.158',
   '46.82.174.68',
   '59.24.3.173',
   '64.33.88.161',
   '64.33.99.47',
   '64.66.163.251',
   '65.104.202.252',
   '65.160.219.113',
   '66.45.252.237',
   '72.14.205.99',
   '72.14.205.104',
   '78.16.49.15',
   '93.46.8.89',
   '128.121.126.139',
   '159.106.121.75',
   '169.132.13.103',
   '192.67.198.6',
   '202.106.1.2',
   '202.181.7.85',
   '203.161.230.171',
   '207.12.88.98',
   '208.56.31.43',
   '209.36.73.33',
   '209.145.54.50',
   '209.220.30.174',
   '211.94.66.147',
   '213.169.251.35',
   '216.221.188.182',
   '216.234.179.13'
}

rules = ['-p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 & 0x3C @ 14 = 0" -j DROP']
for wrong_answer in WRONG_ANSWERS:
   hex_ip = ' '.join(['%02x' % int(s) for s in wrong_answer.split('.')])
   rules.append('-p udp --sport 53 -m string --algo bm --hex-string "|%s|" --from 60 --to 180  -j DROP' % hex_ip)

try:
   for rule in rules:
       print(rule)
       subprocess.call('iptables -I INPUT %s' % rule, shell=True)
   print('running..')
   sys.stdin.readline()
except KeyboardInterrupt:
   print('bye')
finally:
   for rule in reversed(rules):
       subprocess.call('iptables -D INPUT %s' % rule, shell=True)

        本地有了这些iptables规则之后就可以丢弃掉GFW发回来的错误答案,从而得到正确的解析结果。
回复

使用道具 举报

技术贴,分析得好,不得不赞下楼主!!!!!!!
回复

使用道具 举报

高深啊……
回复

使用道具 举报

好帖要顶!
回复

使用道具 举报

技术贴,分析得好,不得不赞下楼主!!!!!!!
回复

使用道具 举报

来自手机 | 显示全部楼层
规则的更新很重要,但还是有些隐藏得很深
回复

使用道具 举报

来自手机 | 显示全部楼层
看了个大概明白,意思是说,即便设置了8.8.8.8作为dns服务器也无济于事咯?
回复

使用道具 举报

楼主,我按你这样设了下,PING推特都PING不通了,原先是返回一个错误的地址93.46.8.89
回复

使用道具 举报

来自手机 | 显示全部楼层
muziling 发表于 2013-1-16 21:56
楼主,我按你这样设了下,PING推特都PING不通了,原先是返回一个错误的地址93.46.8.89

推特正确的也是不能通的
回复

使用道具 举报

iptables -I INPUT -p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 &0x3C @ 14 = 0" -j DROP

这条语句一执行,PING百度都要反应半分钟,DNS解析明显变得很慢,哎
回复

使用道具 举报

回复

使用道具 举报

 楼主| | 显示全部楼层
muziling 发表于 2013-1-17 10:18
这条语句一执行,PING百度都要反应半分钟,DNS解析明显变得很慢,哎

是在路由器上执行的么?什么配置的?这条规则指定了sport是53,理论上不会影响ping的数据包的。
http://fqrouter.tumblr.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

欢迎大家光临恩山无线论坛上一条 /1 下一条

有疑问请添加管理员QQ86788181|手机版|小黑屋|Archiver|恩山无线论坛(常州市恩山计算机开发有限公司版权所有) ( 苏ICP备05084872号 )

GMT+8, 2024-5-21 12:41

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

| 江苏省互联网有害信息举报中心 举报信箱:js12377 | @jischina.com.cn 举报电话:025-88802724 本站不良内容举报信箱:68610888@qq.com 举报电话:0519-86695797

快速回复 返回顶部 返回列表