ncを使って名前解決してみたらこうなった
2020年05月08日 金曜日
CONTENTS
背景
ある日のチャットにて
先輩「ゆるぼ NetBSDのsbin/, 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)にbuiltinのechoも\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にはこの文字列をそのまま入れれば良いわけではなく、以下のルールで変換して入れる必要があります。
- ホスト名をドットで分割する (ラベルと言います)
- 1byte目には最初のラベル(ここでは24)の長さを入れます。つまり0x02を入れます。
- 2byte目以降にはラベルの文字列をそのまま入れます。24はASCIIでは0x32 0x34です。
- その次の4byte目には2つ目のラベルの長さを入れます。
- 5byte目には2つ目のラベルの文字列を入れます。
- 以下を繰り返してルートゾーンを表す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には知りたいリソースレコードの番号を入れます。RFC1035の3.2.2を見ると、PTRは12(=0x000c)であることがわかります。
QCLASSはIN固定で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のリソースレコードの数 |
RCODEはNOERRORで、問題なく名前解決出来ているようです。
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はそれぞれクエリで指定したものと同じくPTR、INです。TTLは10進数に直すと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でネタ記事として投稿したものが元ネタです。
皆様もRFCとnc(1)を片手にプロトコルを勉強してみてはどうでしょうか。意外と面白いですよ。
ちなみにnc(1)を使わない方法として、以下の3つがコメントで寄せられました。
- getent(1)を使う
# getent hosts 203.180.155.24 203.180.155.24 eng-blog.iij.ad.jp
# 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)
- 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)使わなくてよかったじゃん!