2013年01月23日
Python(Scapy)を使って'DNS SERVER (Answering + Spoofing Machine)'を作ってみる
本格的なDNSサーバーを構築する程では無いが、簡易的なネームサーバーを用意してアプリケーションの動作を検証・解析をしてみたい・・・・こんな状況って稀にありませんか?
僕は何度かそういった機会がありまして、勉強・復習ついでにPython/Scapyライブラリで簡易DNSサーバー(Spoofingあり)を実装してみることにしましたよ。
僕は何度かそういった機会がありまして、勉強・復習ついでにPython/Scapyライブラリで簡易DNSサーバー(Spoofingあり)を実装してみることにしましたよ。
DNS Answer + Spoofing Machineの概要
今回Scapyを使って実装するDNSサーバーは、受信したDNS(正引き/逆引き)リクエストに含まれる名前(ドメイン名)をローカル辞書(データベース)で検索し、あればそれを利用、無ければ外部DNSサーバーに再帰的な問い合わせを行います。
このローカル辞書には名前と本来とは異なるIPアドレスを関連付けておき、クライアントを騙すこと(Spoofing)が出来るか検証してみます。
Scapyのインストール
既にPythonは準備出来ている前提で、ここではScapyライブラリのインストール方法だけを説明します。
本家サイトから最新版をダウロード、インストールする場合は次の通り。端末を開いて作業しましょう。
$ wget http://www.secdev.org/projects/scapy/files/scapy-latest.tar.gz$ tar xzf scapy-latest.tar.gz$ cd scapy-.x.x/$ sudo python setup.py install
もし、僕と同じくUbuntuを使っているのであれば、apt-getコマンドを使ってインストールすることも出来ます。
(aptリポジトリで管理されているバージョンは本家サイトと比較すると古いという傾向があります)
$ sudo apt-get install python-scapy
これで準備が出来ました。
DNS Answer Machine written in Python, Scapy
早速コードから。
#!/usr/bin/env python # encoding: UTF-8 from scapy.ansmachine import AnsweringMachine from scapy.all import * conf.verb = 0 conf.iface = "eth0" myMacAddr = get_if_hwaddr(conf.iface) QR_QUERY = 0 QR_RESPONSE = 1 DNS_TYPE_A = 1 DNS_TYPE_CNAME = 5 DNS_TYPE_PTR = 12 DNS_TYPE_MX = 15 NAME_SERVER = "8.8.4.4" class MyDNS_am( AnsweringMachine ): function_name = "my_dns" filter = "udp port 53" spoofdict = { 'www.apple.com.':'74.125.235.146', '146.235.125.74.in-addr.arpa.':'www.apple.com.'} def parse_options(self, joker="0.0.0.0", zone=None): if zone is None: zone = {} self.zone = zone self.joker = joker def is_request(self, req): global myMacAddr if req.dst != myMacAddr: return False if not req.haslayer(DNS) or not req.getlayer(DNS).qr == QR_QUERY: return False print "------ DNS ANSWER MACHINE -----" return True def make_reply(self, req): ip = req.getlayer(IP) orgId = req.getlayer(DNS).id qname = req.getlayer(DNS).qd.qname print "DNS REQUEST(%s): %s FROM %s" % (orgId, qname, ip.src) dns = None if self.spoofdict.has_key(qname): rdata = self.zone.get(qname, self.spoofdict[qname]) print "DNS SPOOF: %s -> %s" % (qname, rdata) qtype = req.getlayer(DNS).qd.qtype ans = DNSRR(rrname=qname, ttl=10, rdata=rdata, type=qtype) dns = DNS(id=orgId, qr=QR_RESPONSE, qd=req.getlayer(DNS).qd, an=ans) else: dns = self.forward_query(req.getlayer(DNS)) dns.id = orgId print "DNS RESPONSE(%s): %s to %s" % (orgId, dns.qd.qname, ip.src) res = IP(dst=ip.src, src=ip.dst)/UDP(dport=ip.sport, sport=ip.dport)/dns return res def forward_query(self, dns): print "START QUERY: %s" % (dns.qd.qname) ans = None while not ans: queryPkt = IP(dst=NAME_SERVER)/UDP()/dns # sr1: Send packets at layer 3 and return only the first answer ansPkt = sr1(queryPkt, verbose=0, timeout=1) if ansPkt.haslayer(DNS): if ansPkt.getlayer(DNS).an: if ansPkt.getlayer(DNS).an.type == DNS_TYPE_A: ans = ansPkt.getlayer(DNS) print "RESOLVED(A): %s -> %s" % (dns.qd.qname, ans.an.rdata) elif ansPkt.getlayer(DNS).an.type == DNS_TYPE_CNAME: print "CNAME QUERY: %s" % (ansPkt.getlayer(DNS).an.rdata) ans = ansPkt.getlayer(DNS) elif ansPkt.getlayer(DNS).an.type == DNS_TYPE_MX: ans = ansPkt.getlayer(DNS) print "MX QUERY: %s" % (ansPkt.getlayer(DNS).an.rdata) elif ansPkt.getlayer(DNS).an.type == DNS_TYPE_PTR: ans = ansPkt.getlayer(DNS) print "PTR QUERY: %s" % (ansPkt.getlayer(DNS).an.rdata) else: print "UNKOWN QUERY TYPE: %s" % (ansPkt.getlayer(DNS).an.type) break else: an = None ans = DNS(qr=QR_RESPONSE, qd=dns.qd, an=an) print "NOT FOUND: %s" % (dns.qd.qname) return ans locals()[MyDNS_am.function_name] = lambda *args,**kargs: MyDNS_am(*args,**kargs).run() my_dns()
このコードを理解するには、まず AnsweringMachine クラスについて理解する必要があります。
AnsweringMachineクラスはScapyライブラリに含まれGoFデザイン・パターンでいう、Template Methodパターンに基づいてデザインされたクラスで、このクラスを継承したクラスはparse_options()、is_request()、make_replay()メソッドを実装します。
class Concrete_am(AnsweringMachine): filter = "packet filter rule like tcpdump" def parse_options #implementation 1 def is_request #implementation 2 def make_reply #implementation 3
filter条件にマッチしたEthernetパケットを受信すると、AnsweringMachine は実装クラスのis_request()メソッドをEtherオブジェクトを引数に呼び出し、Trueが戻れば、続いてmake_replay()メソッドを実行・戻り値であるIPオブジェクトをネットワーク上に送信(send_replay)してくれます。
ちなみに、AnsweringMachine を継承した幾つかのクラスが標準で用意されています。
さあ、基本的な仕組みは理解できましたよね? 今度は具体的なDNSサーバー実装を見てみましょうか。
is_requestでは受信したDNSパケットの宛先が自身のネットワーク・インタフェース(MACアドレス)であること、種類が問い合わせである事を確認しています。
(宛先MACアドレスのチェックは外しても構いませんが、自身が送信するDNSクエリーにも反応しますよ)
def is_request(self, req): global myMacAddr if req.dst != myMacAddr: return False if not req.haslayer(DNS) or not req.getlayer(DNS).qr == QR_QUERY: return False print "------ DNS ANSWER MACHINE -----" return True
最後にmake_reply。ここではDNSロジックを記述しています。
def make_reply(self, req): ip = req.getlayer(IP) orgId = req.getlayer(DNS).id qname = req.getlayer(DNS).qd.qname print "DNS REQUEST(%s): %s FROM %s" % (orgId, qname, ip.src) dns = None if self.spoofdict.has_key(qname): rdata = self.zone.get(qname, self.spoofdict[qname]) print "DNS SPOOF: %s -> %s" % (qname, rdata) qtype = req.getlayer(DNS).qd.qtype ans = DNSRR(rrname=qname, ttl=10, rdata=rdata, type=qtype) dns = DNS(id=orgId, qr=QR_RESPONSE, qd=req.getlayer(DNS).qd, an=ans) else: dns = self.forward_query(req.getlayer(DNS)) dns.id = orgId print "DNS RESPONSE(%s): %s to %s" % (orgId, dns.qd.qname, ip.src) res = IP(dst=ip.src, src=ip.dst)/UDP(dport=ip.sport, sport=ip.dport)/dns return res
まず、名前(qname)が spoofdict にあるかどうかを確認し、あれば、ここからDNSレスポンスを生成します。spoofdict には www.apple.com に対する正引き・逆引き用の名前が辞書登録してあります。
実はここで www.apple.com のアドレスとして定義しているIPアドレスは実際には www.google.co.jp に対応するアドレス。うまくクライアントを騙せるでしょうか。
辞書に無い問い合わせはforward_query()メソッドで外部DNSサーバー(ここではGoogle DNS)に再帰的に問い合わせを行い、レスポンスからタイプに応じてA(正引き)、PTR(逆引き)、CNAME(エイリアス名)、MX(メール)などの情報を出力しています。
CNAMEの場合は、Aレコードが戻るまでDNSサーバー側で再問い合わせした方が良いのか?と迷っていたら無駄に複雑なコードになっちゃいました・・・。
Scapyは実際にTCP/UDP portを使って動作するのでは無く、あくまでインタフェースのパケットをsniff(キャプチャ)している点に注意する必要があります。
Scapyで実装したDNS Answer Machineを動かしてみる
Scapyは実際にTCP/UDP portを使って動作するのでは無く、あくまでインタフェースのパケットをsniff(キャプチャ)している点に注意する必要があります。
今回の例でいえば、UDP/ポート53番は実際にはオープンしていないので、そのままではOSからICMP/Destination unreachable(Type 3)が送信されてしまいます。
まずは、このICMP/Type 3をiptablesでDROPしておきましょう。
$ sudo iptables -A OUTPUT -p ICMP --icmp-type port-unreachable -j DROP
これで準備完了。 コードを dns_machine.py として保存・起動してみます。
$ sudo python dns_machine.py
別のPCからdigコマンド(又は nslookup )で www.google.co.jp を名前解決してみます。
dig @192.168.1.10(dns server address) www.google.co.jp
すると、サーバー側では次のように出力され、
------ DNS ANSWER MACHINE -----DNS REQUEST(29018): www.google.co.jp. FROM x.x.x.x (dns client address)START QUERY: www.google.co.jp.RESOLVED(A): www.google.co.jp. -> 74.125.235.151DNS RESPONSE(29018): www.google.co.jp. (74.125.235.151) to x.x.x.x (dns client address)Ether / IP / UDP / DNS Qry "www.google.co.jp." ==> IP / UDP / DNS Ans "74.125.235.151"
クライアント側でも無事に名前解決できました。
$ dig @x.x.x.x (dns server address) www.google.co.jp; <<>> DiG 9.2.4 <<>> @x.x.x.x (dns server address) www.google.co.jp; (1 server found);; global options: printcmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29018;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0;; QUESTION SECTION:;www.google.co.jp. IN A;; ANSWER SECTION:www.google.co.jp. 137 IN A 74.125.235.151www.google.co.jp. 137 IN A 74.125.235.152www.google.co.jp. 137 IN A 74.125.235.159;; Query time: 184 msec;; SERVER: x.x.x.x (dns server address)#53(x.x.x.x (dns server address));; WHEN: Tue Jan 22 19:25:21 2013;; MSG SIZE rcvd: 130
MXレコード問い合わせ($ dig @x.x.x.x gmail.com MX)も問題なく出来ましたよ。
さあ、www.apple.com への問い合わせは無事にイタズラ(spoofing)できるでしょうか?
ブラウザから www.apple.com にアクセスしてみると・・・、
うまくいきましたね!
サーバー側はこんな出力になります。
$ sudo python dns_machine.py------ DNS ANSWER MACHINE -----DNS REQUEST(42774): www.apple.com. FROM x.x.x.x (dns client address)DNS SPOOF: www.apple.com. -> 74.125.235.146DNS RESPONSE(42774): www.apple.com. to x.x.x.x (dns client address)Ether / IP / UDP / DNS Qry "www.apple.com." ==> IP / UDP / DNS Ans "74.125.235.146"------ DNS ANSWER MACHINE -----DNS REQUEST(19702): 146.235.125.74.in-addr.arpa. FROM x.x.x.x (dns client address)DNS SPOOF: 146.235.125.74.in-addr.arpa. -> www.apple.com.DNS RESPONSE(19702): 146.235.125.74.in-addr.arpa. to x.x.x.x (dns client address)Ether / IP / UDP / DNS Qry "146.235.125.74.in-addr.arpa." ==> IP / UDP / DNS Ans "www.apple.com."
うーん、アクセス先アドレスの分散機としても使えるかもしれませんね。
scapyを使うとネットワークの仕組み、プロトコルを楽しく勉強出来ます。興味が湧いたら是非一度遊んでみて下さい。
DNS & BINDクックブック―ネームサーバ管理者のためのレシピ集
posted with amazlet at 13.01.23
クリクット リュウ
オライリージャパン
売り上げランキング: 252,535
オライリージャパン
売り上げランキング: 252,535