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]#