
CVE-2017-13868: XNUにおける未初期化カーネルヒープデータの情報漏洩。
macOS High Sierra 10.13 の ctl_ctloutput 関数は、sooptcopyin の呼び出しの戻り値を無視します。これにより、初期化されていないカーネルヒープデータをユーザースペースに漏えいさせる競合ウィンドウが生じます。ctl_ctloutput-leak は、この情報漏えいを引き起こそうとする proof-of-concept エクスプロイトです。悪用には root 権限が必要です。
このエクスプロイトは、macOS High Sierra 10.13.1 Beta 17B25c および iOS 10.1.1 14B100(mach_portal 使用時)で動作することが確認されています。
macOS High Sierra 10.13 の ctl_ctloutput の関連部分は以下のとおりです。
if (sopt->sopt_valsize && sopt->sopt_val) {
MALLOC(data, void *, sopt->sopt_valsize, M_TEMP, // (a) data is allocated
M_WAITOK); // without M_ZERO.
if (data == NULL)
return (ENOMEM);
/*
* 4108337 - copy user data in case the
* kernel control needs it
*/
error = sooptcopyin(sopt, data, // (b) sooptcopyin() is
sopt->sopt_valsize, sopt->sopt_valsize); // called to fill the
} // buffer; the return
len = sopt->sopt_valsize; // value is ignored.
socket_unlock(so, 0);
error = (*kctl->getopt)(kctl->kctlref, kcb->unit, // (c) The getsockopt()
kcb->userdata, sopt->sopt_name, // implementation is
data, &len); // called to process
if (data != NULL && len > sopt->sopt_valsize) // the buffer.
panic_plain("ctl_ctloutput: ctl %s returned "
"len (%lu) > sopt_valsize (%lu)\n",
kcb->kctl->name, len,
sopt->sopt_valsize);
socket_lock(so, 0);
if (error == 0) {
if (data != NULL)
error = sooptcopyout(sopt, data, len); // (d) If (c) succeeded,
else // then the data buffer
sopt->sopt_valsize = len; // is copied out to
} // userspace.
このコードは次のことを行います:
getsockopt の data パラメータ用のカーネルヒープバッファを、割り当てられたバイトをゼロで埋める M_ZERO フラグを指定せずに割り当てます。sooptcopyin を使って userspace から getsockopt データをコピーします。この copyin は割り当て済みデータを完全に上書きすることを想定しているため、M_ZERO フラグは不要でした。しかし、sooptcopyin の戻り値はチェックされないため、copyin が失敗してバッファに初期化されていないデータが残る可能性があります。たとえば、プログラムが getsockopt に未マップのアドレスを渡した場合、copyin は失敗する可能性があります。getsockopt 実装を呼び出します。この実装は入力バッファを処理し、場合によっては変更や短縮を行い、結果コードを返すはずです。しかし、この実装は、与えられたバッファが(理論上ユーザースペースから来るため)すでに初期化されていると自由に想定でき、そのためいくつかの実装ではバッファをまったく変更しません。たとえば、NECP 関数の necp_ctl_getopt は、データバッファをまったく処理せずに単に 0 を返します。getsockopt 実装がエラーを返さなければ、ctl_ctloutput は sooptcopyout を呼び出してデータバッファをユーザースペースにコピーし直します。このように、getsockopt に未マップのデータアドレスを指定することで、制御されたサイズのヒープバッファを割り当てさせ、そのバッファの内容を初期化されないままにし、その後、そのバッファを未マップのアドレスに書き戻そうとする sooptcopyout の呼び出しへ到達させることができます。copyout を成功させるために必要なのは、sooptcopyin と sooptcopyout の呼び出しの間にそのアドレスを再マップすることだけです。それができれば、初期化されていないカーネルヒープデータをユーザースペースに漏えいさせることができます。
この競合は非常に獲得しやすいことが判明しました。2015 Macbook Pro でテストしたところ、競合に勝つまでの試行回数の平均は最大でも 600 回、中央値は最大でも 5 回でした。iPhone 7 の iOS 10.1.1 では競合にさらに勝ちやすく、通常 2 回以下で成功しました。(このテストは、printfs がエクスプロイトを劇的に遅くするため、DEBUG をオフにして実施しました。)
ビルドするには、make を実行します。さまざまなビルドオプションについては Makefile の先頭を参照してください。
コマンドラインで対象の漏えいサイズを指定してエクスプロイトを実行します:
$ sudo ./ctl_ctloutput-leak 128
000000: ef be ad de ef be ad de 00 00 00 00 00 00 00 00
000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000020: 00 00 00 00 00 00 00 00 01 00 00 00 40 80 00 00
000030: de 28 45 00 04 00 00 00 a0 ff 4a 26 80 ff ff ff
000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000050: 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000070: 00 00 00 00 00 00 00 00 ef be ad de ef be ad de
この問題を 2017 年 10 月 7 日に Apple へ報告しました。CVE-2017-13868 が割り当てられました。Apple は macOS 10.13.2 および iOS 11.2 でこの問題を修正しました。
ctl_ctloutput-leak のコードはパブリックドメインとして公開されています。このコードを参照または使用する際は、私への帰属表示をしていただけると幸いです。