scapy でパケット送受信(DNS)

chestnut_eared_bunting02 linux

scapy で DNS パケットの送受信を行います。

※ここでは実際に設定、動作したものを掲載していますが、内容について保証するものではありません。流用される場合は各自の責任でお願いします。

scapy の他の機能については「scapy について」からご参照ください。

UDP で DNS の送受信

以下の例では、パブリック DNS サーバ(8.8.8.8)に www.google.com の IP アドレスを問い合わせています。

パケットを作成します。

[root@rocky8-client scapy]# scapy -H
WARNING: No alternative Python interpreters found ! Using standard Python shell instead.
Welcome to Scapy (2.6.1)
>>>
>>> # IP パケット作成
>>> pkt_ip  = IP(src=get_if_addr(conf.iface), dst='8.8.8.8')
>>>
>>> # UDP パケット作成
>>> pkt_udp = UDP(sport=RandShort(), dport=53)
>>>
>>> # DNS パケット作成
>>> pkt_dns = DNS(qd=DNSQR(qname='www.google.com.', qtype='A', qclass='IN'))

get_if_addr(conf.iface) は、デフォルトのインターフェースに割り当てられている IP アドレスを取得しています。

>>> conf.iface
<NetworkInterface enp0s3 [UP+BROADCAST+RUNNING+MULTICAST+LOWER_UP]>
>>> get_if_addr('enp0s3')
'10.0.2.44'
>>>

パケットを確認します。

>>> # IP パケットを確認
>>> pkt_ip.show()
###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = hopopt
  chksum    = None
  src       = 10.0.2.44
  dst       = 8.8.8.8
  \options   \

>>>
>>> # UDP パケットを確認
>>> pkt_udp.show()
###[ UDP ]###
  sport     = <RandShort>
  dport     = domain
  len       = None
  chksum    = None

>>>
>>> # DNS パケットを確認
>>> pkt_dns.show()
###[ DNS ]###
  id        = 0
  qr        = 0
  opcode    = QUERY
  aa        = 0
  tc        = 0
  rd        = 1
  ra        = 0
  z         = 0
  ad        = 0
  cd        = 0
  rcode     = ok
  qdcount   = None
  ancount   = None
  nscount   = None
  arcount   = None
  \qd        \
   |###[ DNS Question Record ]###
   |  qname     = b'www.google.com.'
   |  qtype     = A
   |  unicastresponse= 0
   |  qclass    = IN
  \an        \
  \ns        \
  \ar        \

>>>

DNS の問い合わせを実施します。

>>> r = sr1(pkt_ip/pkt_udp/pkt_dns)
Begin emission

Finished sending 1 packets

Received 4 packets, got 1 answers, remaining 0 packets
>>>

結果を確認します。

>>> r.summary()
'IP / UDP / DNS Ans 142.250.206.228'
>>>
>>> r[DNS].show()
###[ DNS ]###
  id        = 0
  qr        = 1
  opcode    = QUERY
  aa        = 0
  tc        = 0
  rd        = 1
  ra        = 1
  z         = 0
  ad        = 0
  cd        = 0
  rcode     = ok
  qdcount   = 1
  ancount   = 1
  nscount   = 0
  arcount   = 0
  \qd        \
   |###[ DNS Question Record ]###
   |  qname     = b'www.google.com.'
   |  qtype     = A
   |  unicastresponse= 0
   |  qclass    = IN
  \an        \
   |###[ DNS Resource Record ]###
   |  rrname    = b'www.google.com.'
   |  type      = A
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 110
   |  rdlen     = None
   |  rdata     = 142.250.206.228
  \ns        \
  \ar        \

>>>

同じことを Python のプログラムコードで実行します。
プログラムコードは以下のとおりです。

[root@rocky8-client scapy]# cat udpdns.py
#!/usr/bin/env python3

from scapy.all import *
conf.verb = 0

pkt = IP(
  src = get_if_addr(conf.iface),
  dst = '8.8.8.8'
)
pkt /= UDP(
  sport = RandShort(),
  dport = 53
)
pkt /= DNS(
  qd = DNSQR(qname='www.google.com.', qtype="A", qclass="IN")
)

r = sr1(pkt)

print(f"qname : {r[DNS].qd.qname.decode('utf-8')}, rdata : {r[DNS].an.rdata}")

[root@rocky8-client scapy]#

実行結果は以下のとおりです。

[root@rocky8-client scapy]# python3 udpdns.py
qname : www.google.com., rdata : 172.217.161.196
[root@rocky8-client scapy]#

TCP で DNS の送受信

TCP での送受信は少々やっかいです。単純に UDP() を TCP() に書き換えただけではうまくいきません。
TCP では通信を開始するにあたって 3 ウェイ・ハンドシェイク(*1)を行いますが、単純に scapy から Syn を送信しても、サーバからの返信(Syn/Ack)に対して、 OS が rst(リセット)をサーバに送信してしまうため通信に失敗します。これは OS のあずかり知らないところで(つまり OS は Syn を送信した覚えがないのに)突然外部から Syn/Ack が届いても、”そんなの知らない”ということでリセットするようです。なので本検証では OS の Socket を使用します。Socket を使用すればライブラリ内で TCP のコネクションを管理してくれます。

*1 TCP 開始時の 3 ウェイ・ハンドシェイク。
クライアント(scapy) ←→ DNS サーバ
→ Syn
← Syn/Ack
→ Ack

もう一点、TCP で DNS を送信する場合は、TCP のペイロードの先頭で DNS パケットの長さを指定する必要があります。dig 実行時のパケットで TCP と UDP の違いを確認します。

DNS クエリを TCP で実施します。

[root@rocky8-client scapy]# dig @10.0.2.40 www.test.co.jp +tcp +short
1.2.3.4
[root@rocky8-client scapy]#

上記クエリ時のキャプチャを以下に示します。

[root@rocky8-client scapy]# tshark -i 1 -f "port 53" -w /tmp/test.pcap
Running as user "root" and group "root". This could be dangerous.
Capturing on 'enp0s3'
10 ^C
[root@rocky8-client scapy]#
[root@rocky8-client scapy]# tshark -t a -r /tmp/test.pcap
Running as user "root" and group "root". This could be dangerous.
    1 15:16:17.198429553    10.0.2.44 → 10.0.2.40    TCP 74 38213 → 53 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM=1 TSval=2519851352 TSecr=0 WS=128
    2 15:16:17.199285321    10.0.2.40 → 10.0.2.44    TCP 74 53 → 38213 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=917345025 TSecr=2519851352 WS=128
    3 15:16:17.199422974    10.0.2.44 → 10.0.2.40    TCP 66 38213 → 53 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=2519851353 TSecr=917345025
    4 15:16:17.200091720    10.0.2.44 → 10.0.2.40    DNS 123 Standard query 0xe514 A www.test.co.jp OPT
    5 15:16:17.200962056    10.0.2.40 → 10.0.2.44    TCP 66 53 → 38213 [ACK] Seq=1 Ack=58 Win=29056 Len=0 TSval=917345026 TSecr=2519851354
    6 15:16:17.201987584    10.0.2.40 → 10.0.2.44    DNS 228 Standard query response 0xe514 A www.test.co.jp A 1.2.3.4 NS ns2.test.jp NS ns1.test.jp A 10.0.2.29 A 10.0.2.28 OPT
    7 15:16:17.202006204    10.0.2.44 → 10.0.2.40    TCP 66 38213 → 53 [ACK] Seq=58 Ack=163 Win=30336 Len=0 TSval=2519851356 TSecr=917345027
    8 15:16:17.203428012    10.0.2.44 → 10.0.2.40    TCP 66 38213 → 53 [FIN, ACK] Seq=58 Ack=163 Win=30336 Len=0 TSval=2519851357 TSecr=917345027
    9 15:16:17.204475589    10.0.2.40 → 10.0.2.44    TCP 66 53 → 38213 [FIN, ACK] Seq=163 Ack=59 Win=29056 Len=0 TSval=917345030 TSecr=2519851357
   10 15:16:17.204484712    10.0.2.44 → 10.0.2.40    TCP 66 38213 → 53 [ACK] Seq=59 Ack=164 Win=30336 Len=0 TSval=2519851358 TSecr=917345030
[root@rocky8-client scapy]#
[root@rocky8-client scapy]# tshark -nn -r /tmp/test.pcap  -Y '(frame.number==4)' -V | cat -n

<省略>
                                      <<↓TCP パケット
    46  Transmission Control Protocol, Src Port: 38213, Dst Port: 53, Seq: 1, Ack: 1, Len: 57
    47      Source Port: 38213
    48      Destination Port: 53
    49      [Stream index: 0]
    50      [TCP Segment Len: 57]
    51      Sequence number: 1    (relative sequence number)
    52      [Next sequence number: 58    (relative sequence number)]
    53      Acknowledgment number: 1    (relative ack number)
    54      1000 .... = Header Length: 32 bytes (8)
    55      Flags: 0x018 (PSH, ACK)
    56          000. .... .... = Reserved: Not set
    57          ...0 .... .... = Nonce: Not set
    58          .... 0... .... = Congestion Window Reduced (CWR): Not set
    59          .... .0.. .... = ECN-Echo: Not set
    60          .... ..0. .... = Urgent: Not set
    61          .... ...1 .... = Acknowledgment: Set
    62          .... .... 1... = Push: Set
    63          .... .... .0.. = Reset: Not set
    64          .... .... ..0. = Syn: Not set
    65          .... .... ...0 = Fin: Not set
    66          [TCP Flags: ·······AP···]

<省略>

    90      TCP payload (57 bytes)    <<TCP のペイロードサイズは 57 バイト(Length フィールド(2 bytes)+ DNS パケットサイズ(55 bytes))
    91      [PDU Size: 57]
    92  Domain Name System (query)    <<↓DNS パケット
    93      Length: 55                <<先頭(Lengthフィールド)には、Lengthフィールドを除く DNS パケットのサイズ(55 bytes)が格納されている
    94      Transaction ID: 0xe514
    95      Flags: 0x0120 Standard query
    96          0... .... .... .... = Response: Message is a query
    97          .000 0... .... .... = Opcode: Standard query (0)
    98          .... ..0. .... .... = Truncated: Message is not truncated
    99          .... ...1 .... .... = Recursion desired: Do query recursively
   100          .... .... .0.. .... = Z: reserved (0)
   101          .... .... ..1. .... = AD bit: Set
   102          .... .... ...0 .... = Non-authenticated data: Unacceptable
   103      Questions: 1
   104      Answer RRs: 0
   105      Authority RRs: 0
   106      Additional RRs: 1
   107      Queries
   108          www.test.co.jp: type A, class IN
   109              Name: www.test.co.jp
   110              [Name Length: 14]
   111              [Label Count: 4]
   112              Type: A (Host Address) (1)
   113              Class: IN (0x0001)

<省略>

[root@rocky8-client scapy]#

DNS クエリを UDP で実施します。

[root@rocky8-client scapy]# dig @10.0.2.40 www.test.co.jp +short
1.2.3.4
[root@rocky8-client scapy]#

上記クエリ時のキャプチャを以下に示します。

[root@rocky8-client scapy]# tshark -i 1 -f "port 53" -w /tmp/test.pcap
Running as user "root" and group "root". This could be dangerous.
Capturing on 'enp0s3'
2 ^C
[root@rocky8-client scapy]#
[root@rocky8-client scapy]# tshark -t a -r /tmp/test.pcap
Running as user "root" and group "root". This could be dangerous.
    1 15:14:39.130610077    10.0.2.44 → 10.0.2.40    DNS 97 Standard query 0x6edd A www.test.co.jp OPT
    2 15:14:39.132058505    10.0.2.40 → 10.0.2.44    DNS 202 Standard query response 0x6edd A www.test.co.jp A 1.2.3.4 NS ns2.test.jp NS ns1.test.jp A 10.0.2.29 A 10.0.2.28 OPT
[root@rocky8-client scapy]#
[root@rocky8-client scapy]# tshark -nn -r /tmp/test.pcap  -Y '(frame.number==1)' -V | cat -n

<省略>
                                      <<↓UDP パケット
    46  User Datagram Protocol, Src Port: 48363, Dst Port: 53
    47      Source Port: 48363
    48      Destination Port: 53
    49      Length: 63                <<UDP 全体の長さ(UDP パケット(8 bytes)+ DNS パケットサイズ(55 bytes))
    50      Checksum: 0x18a4 [unverified]
    51      [Checksum Status: Unverified]
    52      [Stream index: 0]
    53  Domain Name System (query)    <<↓DNS パケット(先頭に Length フィールド無し)
    54      Transaction ID: 0x6edd
    55      Flags: 0x0120 Standard query
    56          0... .... .... .... = Response: Message is a query
    57          .000 0... .... .... = Opcode: Standard query (0)
    58          .... ..0. .... .... = Truncated: Message is not truncated
    59          .... ...1 .... .... = Recursion desired: Do query recursively
    60          .... .... .0.. .... = Z: reserved (0)
    61          .... .... ..1. .... = AD bit: Set
    62          .... .... ...0 .... = Non-authenticated data: Unacceptable
    63      Questions: 1
    64      Answer RRs: 0
    65      Authority RRs: 0
    66      Additional RRs: 1
    67      Queries
    68          www.test.co.jp: type A, class IN
    69              Name: www.test.co.jp
    70              [Name Length: 14]
    71              [Label Count: 4]
    72              Type: A (Host Address) (1)
    73              Class: IN (0x0001)

<省略>

[root@rocky8-client scapy]#

上記を踏まえて scapy による TCP での DNS 送受信を実施します。

TCP のセッション管理は Socket にまかせるとして、TCP のペイロードの先頭に DNS パケットの長さを指定するために、パケット長を自動的に計算するクラスを準備します。これは、DNS パケットの先頭にパケット長(2 バイト)を追加しただけのクラスです。

>>> class DNSTCP(Packet):
...   name = "DNS over TCP"
...   fields_desc = [
...     FieldLenField("len", None, fmt="!H", length_of="dns"),
...     PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)
...   ]
...   def guess_payload_class(self, payload):
...     return DNSTCP
...
>>>

わかる範囲で簡単に説明します。

新しいパケットクラスを作成するには Packet を継承します。
例えば、TCP と UDP のスーパークラスは以下のとおりです。(help(TCP)、help(UDP) でも確認できます)

>>> TCP.__bases__
(<class 'scapy.packet.Packet'>,)
>>> UDP.__bases__
(<class 'scapy.packet.Packet'>,)
>>>

パケットのフィールドはリストとして fields_desc に登録します。DNSTCP に登録されたフィールドは以下のとおりです。

>>> ls(DNSTCP)
len        : FieldLenField                       = ('None')
dns        : PacketLenField                      = ('0')
>>>
>>> pkt = DNSTCP(dns=DNS(qd=DNSQR(qname="example.com")))
>>> ls(pkt)
len        : FieldLenField                       = None            ('None')
dns        : PacketLenField                      = <DNS  qd=[<DNSQR  qname=b'example.com.' |>] |> ('0')
>>

1つ目のフィールドは FieldLenField で定義されていますが、これは2つ目のフィールド(フィールド名は “dns”)の長さを自動計算し “len” という名前のフィールドに登録しています。長さは実際の送信の際に計算されるようです。パラメータは左から、フィールド名(”len”)、デフォルト値(自動計算されるので None でいいらしい)、フィールドのフォーマット(”!H” はビッグエンディアンの符号なし 2 バイト)、長さを算出する対象のフィールド名(”dns” フィールドが対象)です。

FieldLenField("len", None, fmt="!H", length_of="dns"),

2つ目のフィールドは PacketLenField で定義されていますが細かい仕様はよくわかりませんでした。DNSTCP インスタンス生成の際、コンストラクタで DNS パケットを引き渡しますが(DNSTCP(dns=DNS(…))))、DNS パケットはこのフィールド(”dns”)に登録され、登録された DNS パケットに基づき算出された長さが1つ目のフィールド(”len”)に登録されるようです。パラメータは左から、フィールド名(”dns”)、デフォルト値(自動計算されるので 0 でいいらしい)、コンストラクタで引き渡されるパケットのタイプ(ここで指定されたタイプに基づきコンストラクタで引き渡されたパケットが解析される)、長さを算出する関数等(具体的な指定方法はよくわかりませんでした。ネット上の例を参考にしています)です。

PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)

guess_payload_class メソッドは、後に続くと推測されるパケットのタイプを返すためのものらしいです。

ソケットを作成し、DNS サーバへ接続します。

# socket モジュールをインポート
>>> import socket
>>>
# ソケット作成
# AF_INET は IPv4 通信、SOCK_STREAM は ストリームソケット(TCP)の指定
>>> sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>>
# DNS サーバへ接続
>>> sck.connect(('10.0.2.40', 53))
>>>

接続したソケットを scapy のクラスでラップし、sr1 でクエリを実行します。

# ソケット(sck)を scapy.supersocket.StreamSocket でラップ
# ※ 2つ目のパラメータは、ヘルプによるとパケットを解析するための基本パケットクラスらしいです。受信したデータはここで指定したパケットとして処理されるようです。
>>> ssck = StreamSocket(sck, DNSTCP)
>>>
# DNS クエリ実行
>>> r = ssck.sr1(DNSTCP(dns=DNS(qd=DNSQR(qname="www.test.co.jp"))), timeout=5)
Begin emission

Finished sending 1 packets

Received 1 packets, got 1 answers, remaining 0 packets
>>>
>>> ssck.close()
>>> sck.close()
>>>

結果を確認します。

>>> r[DNSTCP].show()
###[ DNS over TCP ]###
  len       = 121
  \dns       \
   |###[ DNS ]###
   |  id        = 0
   |  qr        = 1
   |  opcode    = QUERY
   |  aa        = 0

<省略>
>>> r[DNS].show()
###[ DNS ]###
  id        = 0
  qr        = 1
  opcode    = QUERY
  aa        = 0
  tc        = 0
  rd        = 1
  ra        = 1
  z         = 0
  ad        = 0
  cd        = 0
  rcode     = ok
  qdcount   = 1
  ancount   = 1
  nscount   = 2
  arcount   = 2
  \qd        \
   |###[ DNS Question Record ]###
   |  qname     = b'www.test.co.jp.'
   |  qtype     = A
   |  unicastresponse= 0
   |  qclass    = IN
  \an        \
   |###[ DNS Resource Record ]###
   |  rrname    = b'www.test.co.jp.'
   |  type      = A
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 604800
   |  rdlen     = None
   |  rdata     = 1.2.3.4
  \ns        \
   |###[ DNS Resource Record ]###
   |  rrname    = b'test.co.jp.'
   |  type      = NS
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 604800
   |  rdlen     = None
   |  rdata     = b'ns1.test.jp.'
   |###[ DNS Resource Record ]###
   |  rrname    = b'test.co.jp.'
   |  type      = NS
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 604800
   |  rdlen     = None
   |  rdata     = b'ns2.test.jp.'
  \ar        \
   |###[ DNS Resource Record ]###
   |  rrname    = b'ns1.test.jp.'
   |  type      = A
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 604800
   |  rdlen     = None
   |  rdata     = 10.0.2.28
   |###[ DNS Resource Record ]###
   |  rrname    = b'ns2.test.jp.'
   |  type      = A
   |  cacheflush= 0
   |  rclass    = IN
   |  ttl       = 604800
   |  rdlen     = None
   |  rdata     = 10.0.2.29

>>>

同じことを Python のプログラムコードで実行します。
プログラムコードは以下のとおりです。

[root@rocky8-client scapy]# cat tcpdns.py
#!/usr/bin/env python3

from scapy.all import *
import socket
conf.verb = 0

class DNSTCP(Packet):
  name = "DNS over TCP"

  fields_desc = [
    FieldLenField("len", None, fmt="!H", length_of="dns"),
    PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)
  ]

  def guess_payload_class(self, payload):
    return DNSTCP

sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sck.connect(('10.0.2.40', 53))
ssck = StreamSocket(sck, DNSTCP)

r = ssck.sr1(DNSTCP(dns=DNS(qd=DNSQR(qname="www.test.co.jp"))), timeout=5)

ssck.close()
sck.close()

print(f"len : {r[DNSTCP].len}, qname : {r[DNS].qd.qname.decode('utf-8')}, rdata : {r[DNS].an.rdata}")

[root@rocky8-client scapy]#

実行結果は以下のとおりです。

[root@rocky8-client scapy]# python3 tcpdns.py
len : 121, qname : www.test.co.jp., rdata : 1.2.3.4
[root@rocky8-client scapy]#
タイトルとURLをコピーしました