2013年01月23日

Python(Scapy)を使って'DNS SERVER (Answering + Spoofing Machine)'を作ってみる

本格的なDNSサーバーを構築する程では無いが、簡易的なネームサーバーを用意してアプリケーションの動作を検証・解析をしてみたい・・・・こんな状況って稀にありませんか?

 a0002_001091

僕は何度かそういった機会がありまして、勉強・復習ついでにPython/Scapyライブラリで簡易DNSサーバー(Spoofingあり)を実装してみることにしましたよ。

DNS Answer + Spoofing Machineの概要


今回Scapyを使って実装するDNSサーバーは、受信したDNS(正引き/逆引き)リクエストに含まれる名前(ドメイン名)をローカル辞書(データベース)で検索し、あればそれを利用、無ければ外部DNSサーバーに再帰的な問い合わせを行います。

DNS_Ans_Machine1

このローカル辞書には名前と本来とは異なる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 を継承した幾つかのクラスが標準で用意されています。

classscapy_1_1ansmachine_1_1AnsweringMachine__inherit__graph2


さあ、基本的な仕組みは理解できましたよね? 今度は具体的な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で実装した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.151
DNS 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.151
www.google.co.jp. 137 IN A 74.125.235.152
www.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 にアクセスしてみると・・・、

dns_machine2

うまくいきましたね!

サーバー側はこんな出力になります。

$ 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.146
DNS 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クックブック―ネームサーバ管理者のためのレシピ集
クリクット リュウ
オライリージャパン
売り上げランキング: 252,535

Scapy
Scapy
posted with amazlet at 13.01.23

Equ Press

Posted by netbuffalo at 21:30│Comments(0)TrackBack(0)ネットワーク | プログラミング


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4349178

コメントする

名前
URL
 
  絵文字