スマートフォン解析 top

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

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

Behind Behind the Masq : CVE-2017-14492

Behind the Maskといえば1979年に発表されたYMOの楽曲であり、多くのミュージシャンにカバーされてきた、長年愛されている名曲です。このレポートはそのMaskではなくdnsmasqの脆弱性の裏側を覗き見るものです。

CVE-2017-14492は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-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でDHCPv6機能を有効にしている場合、細工したルータ要請(Router Solicitation)パケットを受け取ることでバッファオーバフローを起こすものです。

Attackerdnsmasq細工したルータ要請(Router Solicitation Message)Heap-based buffer overflow

PoC実行結果

Google社が公開したPoCを実行してみた様子です。上のウインドウがdnsmasq、下がAttackerです。PoCを実行するだけでAddress Sanitizerがオーバフローを検知し、異常終了することがわかります。

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

パケット解析

検証に使用したPoCはこちらです。

生成されるパケットはDHCPv6のルータ要請です。DHCPv6パケットのヘッダフォーマットはrfc4861にて下記のように定義されています。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |     Code      |          Checksum             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            Reserved                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Options ...
+-+-+-+-+-+-+-+-+-+-+-+-

当てはまるのはこちらです。

	pkg = b"".join([
		 u8(ND_ROUTER_SOLICIT),    # type
		 u8(0),                    # code
		 b"X" * 2,                 # checksum
		 b"\x00" * 4,              # reserved
		 u8(ICMP6_OPT_SOURCE_MAC), # hey there, have our mac
		 u8(255),                  # Have 255 MACs!
		 b"A" * 255 * 8,
 ])

定数は、ここで定義されています。

ND_ROUTER_SOLICIT = 133
ICMP6_OPT_SOURCE_MAC = 1

Typeの定義はこちらにまとまっています。ND_ROUTER_SOLICIT = 133なのでRouter Solicitation(ルータ要請)です。Codeは0固定、Checksumには適当な2バイト、Reserved0x00埋めして、オプション領域に攻撃コードを入れています。

オプションのTypeここを参照してください。ICMP6_OPT_SOURCE_MAC = 1はSource Link-layer Address(送信元リンク層アドレス)です。送信元リンク層アドレスオプションのパケットフォーマットはrfc4861で定義されています。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |    Length     |    Link-Layer Address ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Lengthにはこのオプションのサイズを8オクテット単位でセットします。サイズにはTypeLengthの長さも含めなければならないので、例えば、11:22:33:44:55:66というMACアドレスを通知したい場合は、Type:1オクテット、Length:1オクテット、Address:6オクテット、合計8オクテットなので、Lengthは1になります。

ところが、PoCでは下記のように 255 * 8オクテットのオプション領域を通知しようとします。

        u8(ICMP6_OPT_SOURCE_MAC), # hey there, have our mac
        u8(255),                  # Have 255 MACs!
        b"A" * 255 * 8,

コメントに# Have 255 MACs!と書いてありますが、これは誤りです。ここのサイズは上で述べたようにオプション領域のサイズを通知するので、正しくは# Have a 2038 octets MAC!であり、Link-Layer Addressをセットするb"A" * 255 * 8も、TypeLengthの長さを減算してb"A" * (255 * 8 - 2)とすべきです(この長さの不一致は攻撃とは無関係です)。が、dnsmasqはそこまで厳密なパースを行わないので、攻撃は成立します。

ともあれ、このPoCは次のようなパケットを送信します。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -
| Type(133=RS)  | Code = 0      | Checksum = "XX"               | ^
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ICMPヘッダ
|                      Reserved = 0x00000000                    | v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -
| Type(0x01)    | Length = 255  |                               | ^
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               | |
/                                                               / |
/            Link-Layer Address = "A" * 255 * 8                 / | Link-Layer Address Option
/                                                               / |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|                               |                                 v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                                 -

このパケットのサイズは、ICMPヘッダが8バイト、オプションのヘッダが2バイト、オプションのデータが 8 * 255 = 2,040バイトなので全体で2,050バイトになります。このペイロードのポイントは、送信元リンク層アドレスオプションのサイズを長大なものにすることにあります。

罹患箇所

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

バッファオーバフローはprint_mac関数内のsrc/util.c#582で発生します。

char *print_mac(char *buff, unsigned char *mac, int len)
{
  char *p = buff;
  int i;

  if (len == 0)
    sprintf(p, "");
  else
    for (i = 0; i < len; i++)
      p += sprintf(p, "%.2x%s", mac[i], (i == len - 1) ? "" : ":");

  return buff;
}

まずは、オーバフローが発生するpのサイズを確認しましょう。

pのサイズ

pにはprint_mac関数の冒頭でbuffが代入されます。buffprint_mac関数の第一引数なので、呼び元を捜索します。

発症時、print_mac関数はsrc/radv.c#201icmp6_packet関数から呼び出されます。

  else if (packet[0] == ND_ROUTER_SOLICIT)
    {
      char *mac = "";
      struct dhcp_bridge *bridge, *alias;

      /* look for link-layer address option for logging */
      if (sz >= 16 && packet[8] == ICMP6_OPT_SOURCE_MAC && (packet[9] * 8) + 8 <= sz)
        {
          print_mac(daemon->namebuff, &packet[10], (packet[9] * 8) - 2);
          mac = daemon->namebuff;
        }

第一引数にはdaemon->namebuffが渡されています。daemon->namebuffsrc/dnsmasq.h#1038でグローバルに定義されており、型はchar *です。

  char *namebuff; /* MAXDNAME size buffer */

初期化はsrc/option.c#4623です。実際には、#4615buffに領域が確保され、そのアドレスが#4623daemon->namebuffに代入される形で行われます。

void read_opts(int argc, char **argv, char *compile_opts)
{
  char *buff = opt_malloc(MAXDNAME);
  int option, conffile_opt = '7', testmode = 0;
  char *arg, *conffile = CONFFILE;

  opterr = 0;

  daemon = opt_malloc(sizeof(struct daemon));
  memset(daemon, 0, sizeof(struct daemon));
  daemon->namebuff = buff;

opt_malloc関数は、callocシステムコールのラッパです。MAXDNAME定数は、src/dns-protocol.h#25に定義されています。

 #define MAXDNAME        1025            /* maximum presentation domain name */

ということで、発症時のpのサイズは1,025バイトであることがわかりました。

罹患箇所の動き

print_mac関数の呼び出し箇所をもう一度見てみましょう。

          print_mac(daemon->namebuff, &packet[10], (packet[9] * 8) - 2);

packetには、src/radv.c#164にて、iov_baseが代入されます。

  /* Note: use outpacket for input buffer */
  msg.msg_control = control_u.control6;
  msg.msg_controllen = sizeof(control_u);
  msg.msg_flags = 0;
  msg.msg_name = &from;
  msg.msg_namelen = sizeof(from);
  msg.msg_iov = &daemon->outpacket;
  msg.msg_iovlen = 1;

  if ((sz = recv_dhcp_packet(daemon->icmp6fd, &msg)) == -1 || sz < 8)
    return;

  packet = (unsigned char *)daemon->outpacket.iov_base;

iov_basereadvシステムコールで使用される構造体のメンバで、受信したパケットを格納する領域の先頭アドレスを指します。

したがって、&packet[10]はパケットの11バイト目、送信元リンク層アドレスオプションのLink-Layer addressフィールドの先頭アドレスです。

次に、packet[9]は、先頭から10バイト目の値なので、オプション領域のLengthを指しています。その値は255なので、Link-Layer Addressの長さ(packet[9] * 8) - 2は、(255 * 8) - 2 = 2,038となります。

以上を踏まえて、罹患箇所を再掲してその動作を確認します。

char *print_mac(char *buff, unsigned char *mac, int len)
{
  char *p = buff;
  int i;

  if (len == 0)
    sprintf(p, "");
  else
    for (i = 0; i < len; i++)
      p += sprintf(p, "%.2x%s", mac[i], (i == len - 1) ? "" : ":");

  return buff;
}

print_mac関数は、macを先頭から読み取り、16進数表現の文字列に変換し、2文字ずつ分割し、:で結合する、という処理を行なっています。例えば、0x0123456789ABというデータをMACアドレス表現の"01:23:45:67:89:AB"という文字列に変換します。

注目すべきは、この2行です。

for (i = 0; i < len; i++)
	p += sprintf(p, "%.2x%s", mac[i], (i == len - 1) ? "" : ":");

lenには2,038という値が渡されていますが、格納先のpは1,025バイトしか確保されていません。pには、元データ1バイトにつき3バイトずつ転記される(0x00"00:" = 0x30303Aになる)ので、342回目のループで1,024バイト目を先頭に3バイトを転記しようとしてバッファがあふれます。

修正内容

この脆弱性を修正するcommit diffを見てみましょう。

diff --git a/src/radv.c b/src/radv.c
index 1032189..9b7e52c 100644 (file)
--- a/src/radv.c
+++ b/src/radv.c
@@ -198,6 +198,9 @@ void icmp6_packet(time_t now)
       /* look for link-layer address option for logging */
       if (sz >= 16 && packet[8] == ICMP6_OPT_SOURCE_MAC && (packet[9] * 8) + 8 <= sz)
        {
+         if ((packet[9] * 8 - 2) * 3 - 1 >= MAXDNAME) {
+           return;
+         }
          print_mac(daemon->namebuff, &packet[10], (packet[9] * 8) - 2);
          mac = daemon->namebuff;
        }

print_mac関数を呼び出す前に、オプション領域に格納されたLengthから、Link-Layer Addressの変換後の文字列の長さを計算し、バッファサイズを超えた場合はreturnする処理が追加されました。これにより、print_mac関数内でバッファオーバフローが発生することはありません。

まとめ

今回は、CVE-2017-14492を検証しました。比較的単純なバッファオーバフローでしたが、PoCの生成するパケットが壊れていたので詳細な解析に手間が掛かりました。このPoCに限らず、攻撃パケットは壊れていることが多いです。破壊の原因は、攻撃を目的とした意図的な破壊や、PoC開発者の誤解に基づく破壊、あるいはFuzzerがプロトコルを無視して生成するものなど、様々です。今回検証したPoCには開発者の誤解がありましたが、そのような場合でもrfcや攻撃対象のコードとを照らし合わせることで、攻撃の意図を正しく読み解くことができます。

参考情報

[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] rfc4861 Neighbor Discovery for IP version 6 (IPv6) (IETF Tools)
https://tools.ietf.org/html/rfc4861

[6] Internet Control Message Protocol version 6 (ICMPv6) Parameters (IANA) https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml

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

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

修正履歴

  • 2017/12/13 : 軽微な誤字と表現の修正を行いました。
  • 2017/12/18 : シーケンス図を修正しました。

R&D部準備室 柏崎央士