スマートフォン解析 top

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

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

Behind Behind the Masq : CVE-2017-14494

Behind the Maskといえば1979年に発表されたYMOの楽曲であり、坂本龍一氏が作曲した、長年愛されている名曲です。このレポートはそのMaskではなくdnsmasqの脆弱性の裏側を覗き見るものです。

CVE-2017-14494は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-14495
Behind Behind the Masq : CVE-2017-13704
Behind Behind the Masq : CVE-2017-14496

攻撃の流れ

dnsmasqでDHCPv6機能を有効にしている場合、細工したリレー転送メッセージ(RELAY-FORW)を受け取ることで、メモリリークを起こします。リークしたデータには重要情報が含まれる可能性があり、メモリ保護機構を迂回するのに悪用される危険性もあります。

Attackerdnsmasq細工したDHCPv6 RELAY-FORWメッセージメモリリーク

PoC実行結果

Google社が公開したPoCを実行してみた様子です。上のウインドウがdnsmasq、下がAttackerです。40秒のgifアニメーションなのでご注意ください。

Attackerは、

  1. NICを起動して、dnsmasqからDHCP配布を受ける
  2. DHCPサーバのDUIDを調べる
  3. PoCのパラメータに渡すため、コロン区切りのDUIDからコロンを取り除いてゼロパディングする
  4. PoCをキックする
  5. response.binにリークしたデータが格納されるので、その中身を表示(cat/od/strings)する

ということをやっています。

パケット解析

検証に使用したPoCはこちらです。フォーマットに当てはめていきましょう。

ヘッダ部

まずは、PoCのヘッダ部を引用します。

	u8(12),                         # DHCP6RELAYFORW
	'?',
	# Client addr
	'\xFD\x00',
	'\x00\x00' * 6,
	'\x00\x05',
	'_' * (33 - 17), # Skip random data.

msg-typeに12がセットされています。msg-typeのアサインメントは、こちらを参照してください。12は RELAY_FORW なので、rfc3315で定義されているDHCPv6 Relay-Forward Messageです。フォーマットは下記の通りです。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    msg-type   |   hop-count   |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
|                                                               |
|                         link-address                          |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
|                               |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
|                                                               |
|                         peer-address                          |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
|                               |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
.                                                               .
.            options (variable number and length)   ....        .
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

hop-countには'?'がセットされており、link-addressには、fd00::05というアドレスがセットされます。peer-addressには、上記コードではコメントで# Skip random dataと書いてある部分が格納されます。

オプション部

続いて、オプション部です。コードはこちらと、

        gen_option(9, inner_pkg(duid), length=N_BYTES),

こちらです。

def gen_option(option, data, length=None):
    if length is None:
        length = len(data)

    return b"".join([
        u16(option),
        u16(length),
        data
    ])

option-codeには 9 がセットされています。アサインメントによると、OPTION_RELAY_MSGなので、フォーマットはRelay Message Optionです。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        OPTION_RELAY_MSG       |           option-len          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
.                       DHCP-relay-message                      .
.                                                               .
.                                                               .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

N_BYTES0x8000と定義されているので、option-lenには0x8000がセットされます。Relay Message Option の DHCP-relay-message は、本来は別のクライアントからのメッセージをそのまま格納するものです。PoCでは、inner_pkgにて生成しています。

DHCP-relay-message

コードはこちらです。

def inner_pkg(duid):
    OPTION6_SERVER_ID = 2
    return b"".join([
        u8(5),            # Type = DHCP6RENEW
        u8(0), u16(1337), # ID
        gen_option(OPTION6_SERVER_ID, duid),
        gen_option(1, "", length=(N_BYTES - 8 - 18)) # Client ID
    ])

msg-typeには 5 がセットされます。アサインメントは RENEW で、フォーマットはrfc3315のClient/Server Message Formatsです。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    msg-type   |               transaction-id                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
.                            options                            .
.                           (variable)                          .
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

transaction-idにはu8(0), u16(1337)がセットされるので、0x000539が入り、オプションには次の二つのレコードがセットされます。

Server Identifier Option

このコードで生成されるレコードです。

        gen_option(OPTION6_SERVER_ID, duid),

OPTION6_SERVER_IDは 2、Server Identifier Optionです。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        OPTION_SERVERID        |          option-len           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
.                                                               .
.                              DUID                             .
.                        (variable length)                      .
.                                                               .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

duidはPoC起動時の第二パラメータをunhexlifyしたものなので、上の動画の例では0x000100012189391e000c2938a5f2です。option-lenは14です。

Client Identifier Option

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

        gen_option(1, "", length=(N_BYTES - 8 - 18)) # Client ID

option-codeは1、Client Identifier Optionです。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        OPTION_CLIENTID        |          option-len           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
.                                                               .
.                              DUID                             .
.                        (variable length)                      .
.                                                               .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

gen_optionの第二引数に""が、キーワード引数lengthにサイズ(N_BYTES - 8 - 18)が指定されています。その結果、option-lenには0x7fe6が、DUIDには何もセットされずにパケットはここで終わります。

送信パケットのまとめ

まとめると、以下のようなパケットが送信されます。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -
| msg-type = 12 | hop-count=0x3f|                               | ^ DHCPv6 Relay Forward Message
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               | |
|                                                               | |
|                  link-address = fd00::5                       | |
|                                                               | |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| |
|                               |                               | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               | |
|                                                               | |
|                  peer-address = '_' * 16                      | |
|                                                               | |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | -
|                               |     OPTION_RELAY_MSG = 9      | | ^ Relay Message Option
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | -
|      option-len = 0x8000      | msg-type = 5  |  transaction- | | | ^ Renew Message
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | -
|        -id = 0x000539         |     OPTION_SERVERID  = 2      | | | | ^ Server Identifier Option
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | |
|      option-len = 14 = 0x0e   |                               | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               | | | | |
|                                                               | | | | |
|            DUID = 0x000100012189391e000c2938a5f2              | | | | |
|                                                               | | | | v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | -
|   OPTION_CLIENTID = 1         |    option-len = 0x7fe6        | v v v | Client Identifier Option
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - - -

このペイロードのポイントは、最後のレコードClient Identifier Optionのoption-lenに実際より大きいサイズが指定されていることにあります。

罹患箇所

まずは、クライアントへデータを返送しているところを見てみましょう。参照するコードは全てバージョン2.78test2のものです。

src/dhcp6.c#247sendto関数で送信しています。

  /* The port in the source address of the original request should
     be correct, but at least once client sends from the server port,
     so we explicitly send to the client port to a client, and the
     server port to a relay. */
  if (port != 0)
    {
      from.sin6_port = htons(port);
      while (retry_send(sendto(daemon->dhcp6fd, daemon->outpacket.iov_base,
			       save_counter(0), 0, (struct sockaddr *)&from,
			       sizeof(from))));
    }

問題は、sendtoの第二引数daemon->outpacket.iov_baseに必要以上のデータがコピーされている事にあります。第三引数のsave_counter(0)は、src/outpacket.c#41に定義されている、そこまで積み上げたパケットカウンタを返す関数です。

続いて、Client Identifier Optionを送信パケット領域にコピーする箇所を参照します。場所はsrc/dhcp3315.c#310put_opt6関数呼び出しです。

    if ((opt = opt6_find(state->packet_options, state->end, OPTION6_CLIENT_ID, 1)))
    {                                         /* (1) */
      state->clid = opt6_ptr(opt, 0);         /* (2) */
      state->clid_len = opt6_len(opt);        /* (3) */
      o = new_opt6(OPTION6_CLIENT_ID);
      put_opt6(state->clid, state->clid_len); /* (4) */
      end_opt6(o);
    }

まず、(1)opt6_find関数でClient Identifier Optionの先頭アドレスをoptに格納しています。

(2)opt6_ptr(opt, 0)は、参照しているオプション領域のoption-codeoption-lengthを読み飛ばしてoption-dataの先頭アドレスを返します。ところが、先に示した通り、このパケットのClient Identifier Optionにはoption-data領域がありません。したがって、この時点でstate->clidはパケットの最終アドレス+1のアドレスを指しており、これは確保された領域の外を示しています。

(3)opt6_len(opt)では、option-lengthの値を取り出します。このパケットでは0x7fe6がセットされています。

最後に罹患箇所(4)です。put_opt6関数は、第一引数のポインタを先頭に、第二引数のサイズ分だけdaemon->outpacket.iov_baseにコピーし、パケットカウンタを更新する処理です。 put_opt6(state-clid, state->clid_len)で、パケットが格納されているメモリ領域を一つ越えたアドレスから、0x7fe6 = 32,742バイト分のメモリ領域がコピーされます。

このようにコピーされたメモリ領域は、前述のsendtoシステムコールによってクライアントへ返送され、結果としてメモリリークが発生します。

修正内容

この脆弱性を修正するcommit diffを検討します。

--- a/src/rfc3315.c
+++ b/src/rfc3315.c
@@ -216,6 +216,9 @@ static int dhcp6_maybe_relay(struct state *state, void *inbuff, size_t sz,

   for (opt = opts; opt; opt = opt6_next(opt, end))
     {
+      if (opt6_ptr(opt, 0) + opt6_len(opt) >= end) {
+        return 0;
+      }
       int o = new_opt6(opt6_type(opt));
       if (opt6_type(opt) == OPTION6_RELAY_MSG)
        {

dhcp6_maybe_relay関数は、DHCPv6 Relay Messageを受け取った際の処理です。上記のコードは、現在着目しているオプションデータの処理を終えて、次のオプションを読み取り、処理振り分けをおこなうところです。

endは、受け取ったパケットが格納されている領域の末尾 + 1のアドレスを指し示すポインタです。end変数の生い立ちにご興味のある方はsrc/dhcp-common.c#39(v2.78)のrecv_dhcp_packet関数と、src/rfc3315.c#114(v2.78)を参照してください。

追加されたコードは、option-dataのアドレスにoption-lengthを足したアドレスがパケットの末尾を超えていた場合はreturnする、というものです。これにより、今回検証したClient Identifier Optionによるメモリリークに限らず、Relay Messageのオプション領域全般におけるリークを防ぐことができます。

まとめ

CVE-2017-14494を検証しました。以下、余談です。

この脆弱性を攻撃するためには、正当なserver-idが必要です。server-idの検証はsrc/rfc3315.c#317で行われています。したがって、攻撃者はなんらかの手順を踏んでserver-idを得る必要があります。通常の環境であればPCをネットワークに繋ぐだけでDHCPv6プロトコルに則ってアドレスが配布され、その過程でserver-idを得られます。

ところが、脆弱性検証はそう単純でもありません。検証は、その過程で攻撃パケットがネットワークを流れるため、論理的または物理的に隔離されている必要があります(特に今回の脆弱性は、DHCPに関わるものなので、隔離しなければ他のマシンにも悪影響があります)。私はネットワークを隔離する簡単な手段としてDockerを利用することが多いのですが、DockerのネットワークにおいてはDockerシステムがコンテナにIPアドレスを割り当てる仕組みが働くため、攻撃者のDHCPクライアントを稼働させることができませんでした。Googleが公開したPoCのinstructionsにも、Dockerを使った簡単なセットアップ方法はない旨が書かれています。そのため、今回の脆弱性の検証に当たってはVMware Fusionを利用して独立したネットワークを構成しています。

Dockerに慣れきっていると、VMのセットアップには時間もかかるしロールバックが面倒だしクラッシュアンドビルドのコスト高いし構築手順書残さなきゃいけないし、というツラみがあります(Ansible使えって?おっしゃる通りです)。脆弱性の内容によっては検証環境構築の手順も大きく変わる、というお話でした。

参考情報

[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] rfc3315 Dynamic Host Configuration Protocol for IPv6 (DHCPv6) (IETF Tools)
https://tools.ietf.org/html/rfc3315

[6] rfc6221 Lightweight DHCPv6 Relay Agent (IETF Tools)
https://tools.ietf.org/html/rfc6221

[7] Dynamic Host Configuration Protocol for IPv6 (DHCPv6) (IANA)
https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml

[8] 18.14. binascii — Convert between binary and ASCII (Python Documentation)
https://docs.python.org/2.7/library/binascii.html#binascii.unhexlify

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

修正履歴

  • 2017/12/18 : シーケンス図を修正しました。

R&D部準備室 柏崎央士