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からヘッダのフォーマットを引用します。

では、各フィールドに入れる値を考えましょう。
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)で書きましょう。

ペイロードを作る

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

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

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

フィールド名 説明
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でスクリプトを書きました。

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

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

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

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

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

クエリを送信する

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

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

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

レスポンスを読む

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

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

ヘッダを読む

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

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

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ですが、これはクエリと同じなので割愛します。以下の部分です。

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

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

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

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です。

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

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

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

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

終わりに

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

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

  1. getent(1)を使う

  1. ftp(1)Google Public DNSの独自形式DoHを使う

  1. ping(1)を使う
    ※NetBSDのping(1)は宛先アドレスの逆引き結果を表示してくれます

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

草場 健

2020年05月08日 金曜日

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

Related
関連記事