ncを使って名前解決してみたらこうなった

2020年05月08日 金曜日


【この記事を書いた人】
草場 健

2018年新卒入社でルータのファームウェアを開発しています。デバイスドライバやネットワークスタックなどの低レイヤーに興味があります。

「ncを使って名前解決してみたらこうなった」のイメージ

背景

ある日のチャットにて

先輩「ゆるぼ NetBSDsbin/bin/usr.sbin/, usr.bin/にあるコマンドでPTRレコードを引く方法」
私「もしかしてnc(1)
先輩「えっと、それはどうやるんでしょう…?」
私「あっこう… DNSのクエリを自前で生成して

というリプライをしてしまったので、反省を兼ねてnc(1)を使って名前解決をしてみます。

クエリを作る

クエリの生成にはprintf(1)を使います。printf "\xde\xad\xbe\xef" とすることで0xdeadbeefのバイナリ列を標準出力へ出すことができます。
echo(1)
でもできそうですがNetBSD標準のecho(1)-eオプションがなく、sh(1)builtinecho\xに対応していないので注意が必要です。

パケットのフォーマットは最初に規定されたRFC1035を参照することにしましょう。RFC1035は他のRFCによって一部修正されていますが、気にせずやってみることにします。
RFC
を見てもよくわからない方はDNSパケットフォーマットと、DNSパケットの作り方を参考にするとよいでしょう。

今回はPTRレコードを引きたかったので、203.180.155.24を逆引きするクエリを生成していきます。

ヘッダを作る

まずはヘッダを作りましょう。

以下にRFC1035 Section 4.1.1からヘッダのフォーマットを引用します。

                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

では、各フィールドに入れる値を考えましょう。
RFC
を見ると各フィールドの役割は以下のようになっています。

フィールド名 説明
ID クエリとレスポンスを紐づけるために使われる値
クエリではランダムな値を割り当てる
QR メッセージがクエリかレスポンスかを示す値
クエリ=0、レスポンス=1
Opcode クエリの種類を示す値
基本的に0を入れておけばOK
AA レスポンスで使われるフィールド
TC ペイロードが分割されているかどうかを示す値
分割されていない=0、分割されている=1
RD 再帰解決を要求するかを示す値
要求しない=0、要求する=1
RA レスポンスで使われるフィールド
Z 将来的に使われることを見越して予約されているフィールドで0固定
現在はAD bitやCD bitが割り当てられています。
RCODE レスポンスで使われるフィールド
QDCOUNT Question sectionにあるリソースレコードの数を示す値
ANCOUNT Answer sectionにあるリソースレコードの数を示す値
NSCOUNT Authority sectionにあるリソースレコードの数を示す値
ARCOUNT Additional sectionにあるリソースレコードの数を示す値

今回は以下のように値を入れることにします。

  • ID=1
  • RD=1
  • QDCOUNT=1
  • 他のフィールドは0

では、このヘッダを生成するコマンドをprintf(1)で書きましょう。

printf "\x00\x01"; # ID=1
printf "\x01\x00"; # RD bit on
printf "\x00\x01"; # QDCOUNT=1
printf "\x00\x00\x00\x00\x00\x00"; # ANCOUNT=0, NSCOUNT=0, ARCOUNT=0

ペイロードを作る

次にペイロードを作りましょう。

以下にRFC1035 Section 4.1.2からQuestion Sectionのフォーマットを引用します。

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

こちらも入れる値を考えましょう。

フィールド名 説明
QNAME 名前解決したいドメイン名
QTYPE 要求するリソースレコードのタイプを示す値
QCLASS クエリのクラスを示す値

今回は203.180.155.24の逆引きをしたいので、QNAMEには24.155.180.203.in-addr.arpa.です。

しかしQNAMEにはこの文字列をそのまま入れれば良いわけではなく、以下のルールで変換して入れる必要があります。

  1. ホスト名をドットで分割する (ラベルと言います)
  2. 1byte目には最初のラベル(ここでは24)の長さを入れます。つまり0x02を入れます。
  3. 2byte目以降にはラベルの文字列をそのまま入れます。24はASCIIでは0x32 0x34です。
  4. その次の4byte目には2つ目のラベルの長さを入れます。
  5. 5byte目には2つ目のラベルの文字列を入れます。
  6. 以下を繰り返してルートゾーンを表す0x00まで入れます。

手作業でやりたくなかったので、Pythonでスクリプトを書きました。

hostname = input().strip()
labels = hostname.split('.')

# append root label
if labels[-1] != '':
    labels.append('')

for label in labels:
    command = "printf \""
    assert(len(label) <= 0x3f)
    command += f"\\x{len(label):02x}"
    for s in label:
        char = ord(s)
        assert(char <= 0x7f)
        command += f"\\x{char:02x}"
    
    if label == '':
        command += f"\"; # root"
    else:
        command += f"\"; #{label}"
    print(command)

当然Python3は今回の制限から溢れてしまいますが、ご勘弁ください。
ちなみに制限下のコマンドだけで変換する方法として以下のコマンドを同僚からアドバイスしてもらいました。

echo 24.155.180.203.in-addr.arpa. | tr . \\n | while read label; do dc -e "[$label]dZann";done;
printf "\x00";

QTYPEには知りたいリソースレコードの番号を入れます。RFC10353.2.2を見ると、PTR12(=0x000c)であることがわかります。

QCLASSIN固定でOKです。これもRFC1035 3.2.4に書いてあり、 1(=0x0001) です。

というわけでペイロード部分を出力するコマンドは以下になります。

printf "\x02\x32\x34"; #24
printf "\x03\x31\x35\x35"; #155
printf "\x03\x31\x38\x30"; #180
printf "\x03\x32\x30\x33"; #203
printf "\x07\x69\x6e\x2d\x61\x64\x64\x72"; #in-addr
printf "\x04\x61\x72\x70\x61"; #arpa
printf "\x00"; # root
printf "\x00\x0c"; # QTYPE=PTR
printf "\x00\x01"; # QCLASS=IN

これでクエリが出来ました。

クエリを送信する

クエリを生成するprintf(1)軍団を並べてnc(1)にパイプします。

NetBSD-current(執筆時の最新版: 9.99.56)で実行してみましょう。

# (
> printf "\x00\x01"; # ID=1
> printf "\x01\x00"; # RD bit on
> printf "\x00\x01"; # QDCOUNT=1
> printf "\x00\x00\x00\x00\x00\x00"; # ANCOUNT=0, NSCOUNT=0, ARCOUNT=0
> printf "\x02\x32\x34"; #24
> printf "\x03\x31\x35\x35"; #155
> printf "\x03\x31\x38\x30"; #180
> printf "\x03\x32\x30\x33"; #203
> printf "\x07\x69\x6e\x2d\x61\x64\x64\x72"; #in-addr
> printf "\x04\x61\x72\x70\x61"; #arpa
> printf "\x00"; # root
> printf "\x00\x0c"; # QTYPE=PTR
> printf "\x00\x01"; # QCLASS=IN
> ) | nc -w 1 -u <DNSサーバのIPアドレス> 53
24155180203in-addrarpa




                     eng-blogiijadjp

お、DNSサーバからちゃんと応答が返ってきてそうですね。
逆引き結果のようなもの(ドットで区切られていないもの)が見えていますが、これでは解読したことにならないので真面目に解読しましょう。

レスポンスを読む

標準入力からバイナリ列が読めればいいのでod(1)を使います。
ネットワークオーダーで見る必要があるので-t オプションで1byteずつ表示しましょう。

# (
> printf "\x00\x01"; # ID=1
> printf "\x01\x00"; # RD bit on
> printf "\x00\x01"; # QDCOUNT=1
> printf "\x00\x00\x00\x00\x00\x00"; # ANCOUNT=0, NSCOUNT=0, ARCOUNT=0
> printf "\x02\x32\x34"; #24
> printf "\x03\x31\x35\x35"; #155
> printf "\x03\x31\x38\x30"; #180
> printf "\x03\x32\x30\x33"; #203
> printf "\x07\x69\x6e\x2d\x61\x64\x64\x72"; #in-addr
> printf "\x04\x61\x72\x70\x61"; #arpa
> printf "\x00"; # root
> printf "\x00\x0c"; # QTYPE=PTR
> printf "\x00\x01"; # QCLASS=IN
> ) | nc -w 1 -u <DNSサーバのIPアドレス> 53 | od -t x1
0000000   00  01  81  80  00  01  00  01  00  00  00  00  02  32  34  03
0000020   31  35  35  03  31  38  30  03  32  30  33  07  69  6e  2d  61
0000040   64  64  72  04  61  72  70  61  00  00  0c  00  01  c0  0c  00
0000060   0c  00  01  00  00  01  90  00  14  08  65  6e  67  2d  62  6c
0000100   6f  67  03  69  69  6a  02  61  64  02  6a  70  00
0000115

はい、表示できました。では読んでいきましょう。

ヘッダを読む

ヘッダは以下の部分です。

0000000   00  01  81  80  00  01  00  01  00  00  00  00  XX  XX  XX  XX
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

このままだと分からないので、人力でパースしてみましょう。
各フィールドの説明も記載します。クエリの時と意味が変わらないものも再掲します。

Field Value 説明
ID 0x0001 クエリとレスポンスを紐づけるために使われる値
レスポンスではクエリのIDがコピーされる
QR 1 メッセージがクエリかレスポンスかを示す値
0=クエリ、1=レスポンス
Qpcode 0 クエリで利用されるフィールド
レスポンスではクエリのOpcodeの値がコピーされる
AA 0 権威がある応答かを示す値
0=権威がない、1=権威がある
TC 0 ペイロードが分割されているかどうかを示す値
0=分割されていない、1=分割されている
RD 1 クエリで利用されるフィールド
レスポンスではクエリのRD bitの値がコピーされる
RA 1 再帰名前解決可能であることを示す値
0=不可能、1=可能
Z 0 常に0
RCODE 0 レスポンスの状態を示す値
0=NOERROR、2=SERVFAIL、etc…
QDCOUNT 0x0001 Question sectionのリソースレコードの数
ANCOUNT 0x0001 Answer sectionのリソースレコードの数
NSCOUNT 0x0000 Authority sectionのリソースレコードの数
ARCOUNT 0x0000 Additional sectionのリソースレコードの数

RCODENOERRORで、問題なく名前解決出来ているようです。
AA bit
0ですが、今回はフルサービスレゾルバからの応答なので問題ありません。

ペイロードを読む

ではペイロードのリソースレコードを読んでいきましょう。

最初はQuestion sectionですが、これはクエリと同じなので割愛します。以下の部分です。

0000000   XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  02  32  34  03
0000020   31  35  35  03  31  38  30  03  32  30  33  07  69  6e  2d  61
0000040   64  64  72  04  61  72  70  61  00  00  0c  00  01  XX  XX  XX

次にAnswer sectionです。以下の部分です。

0000040   XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  XX  c0  0c  00
0000060   0c  00  01  00  00  01  90  00  14  08  65  6e  67  2d  62  6c
0000100   6f  67  03  69  69  6a  02  61  64  02  6a  70  00

ここにはリソースレコードがそのまま書いてあります。
というわけでリソースレコードのフォーマットを見てみましょう。 RFC1035 Section 4.1.3から引用します。

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

こちらも人力パースすると以下のようになります。

Field value 意味
NAME 0xc00c リソースレコードのドメイン名
TYPE 0x000c リソースレコードのタイプを表す値
CLASS 0c0001 RDATAのデータのクラスを表す値
TTL 0c00000190 リソースレコードのTTL
RDLENGTH 0x0014 RDATAフィールドの長さ
RDATA 残り全て リソースレコードのデータ

NAMEの値を見るとホスト名の1byte目の上位2ビットが11になっていますね。
このときはRFC1035 Section 4.1.4に記載の方法でホスト名が省略されています。
解読するには先頭から2bytesを持ってきて、0x3fff と論理積をとります。その数値分だけパケットの先頭からずれた部分にあるホスト名をみましょう。

今回の場合は先頭から0x0c bytesの部分を確認すれば良いので、以下から見ていけばOKです。

                                                          ↓ここから読む
0000000   00  01  81  80  00  01  00  01  00  00  00  00  02  32  34  03

おっと、これはQNAMEの部分ですね。ということはこのリソースレコードのNAMEは名前解決したかった24.155.180.203.in-addr.arpa.です。
TYPE
CLASSはそれぞれクエリで指定したものと同じくPTRINです。TTL10進数に直すと400ですね。
最後に、RDATAを逆変換すると…eng-blog.iij.ad.jp.ですね!

一応、dig(1)で答え合わせをしましょう。

$ dig 24.155.180.203.in-addr.arpa. ptr

; <<>> DiG 9.10.6 <<>> 24.155.180.203.in-addr.arpa. ptr
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12197
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;24.155.180.203.in-addr.arpa.   IN      PTR

;; ANSWER SECTION:
24.155.180.203.in-addr.arpa. 3600 IN    PTR     eng-blog.iij.ad.jp.

;; Query time: 25 msec
;; SERVER: XXX.XXX.XXX.XXX#53(XXX.XXX.XXX.XXX)
;; WHEN: Mon Apr 20 13:53:16 JST 2020
;; MSG SIZE  rcvd: 88

読み取った結果と一致していますね。

nc(1)で名前解決することが出来ました!

終わりに

今回の記事は私が社内SNSでネタ記事として投稿したものが元ネタです。
皆様もRFCnc(1)を片手にプロトコルを勉強してみてはどうでしょうか。意外と面白いですよ。

ちなみにnc(1)を使わない方法として、以下の3つがコメントで寄せられました。

  1. getent(1)を使う
# getent hosts 203.180.155.24
203.180.155.24    eng-blog.iij.ad.jp
  1. ftp(1)Google Public DNSの独自形式DoHを使う
# ftp -o- 'https://dns.google/resolve?name=24.155.180.203.in-addr.arpa&type=ptr'
Trying 8.8.4.4:443 ...
Requesting https://dns.google/resolve?name=24.155.180.203.in-addr.arpa&type=ptr
   284        8.73 MiB/s 
{"Status": 0,"TC": false,"RD": true,"RA": true,"AD": false,"CD": false,"Question":[ {"name": "24.155.180.203.in-addr.arpa.","type": 12}],"Answer":[ {"name": "24.155.180.203.in-addr.arpa.","type": 12,"TTL": 21599,"data": "eng-blog.iij.ad.jp."}],"Comment": "Response from 210.130.0.5."}284 bytes retrieved in 00:00 (6.94 MiB/s)
  1. ping(1)を使う
    ※NetBSDのping(1)は宛先アドレスの逆引き結果を表示してくれます
# ping -c1 203.180.155.24
PING eng-blog.iij.ad.jp (203.180.155.24): 56 data bytes
64 bytes from 203.180.155.24: icmp_seq=0 ttl=54 time=12.936281 ms

----eng-blog.iij.ad.jp PING Statistics----
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 12.936281/12.936281/12.936281/0.000000 ms

( ゚д゚ハッ!
(
゚д゚) nc(1)使わなくてよかったじゃん!

草場 健

2020年05月08日 金曜日

2018年新卒入社でルータのファームウェアを開発しています。デバイスドライバやネットワークスタックなどの低レイヤーに興味があります。

Related
関連記事