
empty_list - p0 issue 1564 (CVE-2018-4243) iOS 11.0 - 11.3.1 のカーネル読み書き用エクスプロイト
@i41nbeer
バグ: getvolattrlist は、fgetattrlist システムコール経由でユーザー制御の bufferSize 引数を受け取ります。
attr リストをシリアライズするためのカーネルバッファを割り当てる際、次のコメントがあります:
/*
* Allocate a target buffer for attribute results.
* Note that since we won't ever copy out more than the caller requested,
* we never need to allocate more than they offer.
*/
ab.allocated = ulmin(bufferSize, fixedsize + varsize);
if (ab.allocated > ATTR_MAX_BUFFER) {
error = ENOMEM;
VFS_DEBUG(ctx, vp, "ATTRLIST - ERROR: buffer size too large (%d limit %d)", ab.allocated, ATTR_MAX_BUFFER);
goto out;
}
MALLOC(ab.base, char *, ab.allocated, M_TEMP, M_ZERO | M_WAITOK);
問題は、ユーザーが指定したバッファサイズが要求されたヘッダーサイズより小さい場合をコードが正しく処理していないことです。ATTR_CMN_RETURNED_ATTRS を渡すと、次のコードに到達します:
/* Return attribute set output if requested. */
if (return_valid) {
ab.actual.commonattr |= ATTR_CMN_RETURNED_ATTRS;
if (pack_invalid) {
/* Only report the attributes that are valid */
ab.actual.commonattr &= ab.valid.commonattr;
ab.actual.volattr &= ab.valid.volattr;
}
bcopy(&ab.actual, ab.base + sizeof(uint32_t), sizeof (ab.actual));
}
割り当てられたバッファが、少なくともそれを保持するのに十分な大きさであるかどうかのチェックはありません。
エクスプロイト: これについて、より長文の記事を公開したいと考えています。以下は、エクスプロイトがどのように動作するかについての大まかなメモです:
このバグにより、kalloc.16 アロケーションの終端を越えて 8 バイトのゼロを書き込むことができます。これらのバイトの数ビットを制御できるように見えますが、実際にできるかどうかは確信がないため、NULL ポインタを終端の外に書き込むものとしてエクスプロイトに取り組みました。
これはかなり限定的なプリミティブなので、最初のステップとして、可能な利用方法を列挙してみます:
最終的に最初のオプションを選びました。その場合、さらに 2 つの要件があります:
私は struct ipc_port を標的に選びました。これは2番目の dword として参照カウントフィールドを持ち、最初の要件を満たします。ただし、kalloc.16 ではなく、独自のゾーン (ipc_ports) に割り当てられます。
つまり、kalloc.16 ゾーンブロックを ipc_ports のブロックの直前に配置し、kalloc.16 ブロック内の最後の kalloc.16 アロケーションからオーバーフローさせて、ipc_ports 内の最初のアロケーションに到達させる必要があります。
これを容易にするためのトリックが 2 つあります:
フリーリストの反転: ゾーンアロケーションは、まず intermediate (一部使用中) ページから行われます。つまり、groom の途中で k.16 オブジェクトの free と確保を始めただけでは、現在の intermediate ページが full か empty になるまで再利用されません。
これは課題を生みます。新しいページのフリーリストは半ランダムに埋められるため、そのアロケーションは内側から外側へと進みます:
| 9 8 6 5 2 1 3 4 7 10 | <-- example "randomized" allocation order from a fresh all-free page
つまり、最終的な intermediate の k.16 ページと ports ページは次のようになります:
| - - - 5 2 1 3 4 - - | - - - 4 1 2 3 5 - - |
kalloc.16 ipc_ports
オーバーフローを使ってフリーリストエントリを破損させると、それが割り当てられた場合にパニックが発生するため、それを回避する必要があります。
トリックは、アロケーションと解放の順序を制御することでフリーリストを反転させ、最終的な intermediate ページを次のように近づけることです:
| 1 4 - - - - - 5 3 2 | 2 5 - - - - - 4 3 1 |
kalloc.16 ipc_ports
この時点で、kalloc.16 を解放してオーバーフロー用に再割り当てし、ipc_port の最初の qword にヒットさせられる可能性がはるかに高くなります。
安全にオーバーフロー可能なアロケーション: ターゲット (最後、つまり ipc_port の直前) に到達する前にオーバーフローさせなければならない候補アロケーションが多数存在する可能性が高いため、kalloc.16 ページ上の割り当て済みオブジェクトが NULL ポインタで破損しても安全であることを確認する必要があります。
これには mach message の ool_port ディスクリプタを使用します。NULL は有効な値だからです。
エクスプロイトの流れ: kalloc.16 のフリーリストを反転させる groom を行い、ipc_port へのオーバーフローを試み始めます。
破損される予定のポートを含む mach port 名のおおよその範囲が分かっています。各オーバーフロー試行の後、これらのポートをそれぞれチェックして、ポートが破損したかどうかを確認します。破損が成功した場合の副作用として、ポートの io_active フラグがゼロに設定されます。これは、mach_port_kobject MIG メソッドを使用して副作用を起こさずに検出できます。
破損したポートを見つけたら、そのポートに対して参照の取得と解放を発生させる必要があります。さらに重要なのは、その処理を行うコードパスが io_active フラグをチェックしないことです。mach_port_set_attributes はこれを実現してくれます。
これで、kalloc.16 の終端への NULL ポインタ書き込みが、ダングリング mach port に変わります :)
ゾーン gc を発生させ、ポートのメモリが kalloc.4096 ページとして再利用されるようにします。まず、それを ool_ports ディスクリプタとして再利用させます。そこでは ip_context フィールドが、canary ポートに送信した send right と重なります。これにより、カーネル内のオブジェクトのおおよそのアドレスを知ることができます。次に、ool_desc をパイプバッファに置き換え、少し調整することで、ダングリング mach port がメモリ上のどこにあるかを突き止めます。
そこに偽のカーネルタスクポートを作成し、後片付けをします。
信頼性: エクスプロイトは実際に動作します。それが私の目標でした :) 信頼性はおそらく 30% 程度で、すべては最初のオーバーフローとテストループをどれだけ迅速に行えるかにかかっています。他の何かが割り込んで kalloc.16 内で割り当てや解放を行うと、フリーリストエントリや他の何かを破損する可能性が高まり、パニックになります。
このエクスプロイトはより信頼性を高められるはずです。私は、このバグが悪用可能であることを示すところまでしか到達していません。これを出発点として、信頼性を向上させる方法を示したいなら、ぜひブログ記事を読みたいです! それには、実際に kalloc.16 のアロケーションを監視し、失敗のケースとそれを防ぐ方法を理解することが含まれると思います。
成功率は、デバイスを再起動してしばらくアイドル状態にした場合に最も高くなるようです。
クリーンアップ: エクスプロイトが動作した場合、それ自体の後片付けを行い、デバイスをパニックさせないはずです。偽のカーネルタスクポートは生き続けます。
kmem.h の関数を使用してカーネルメモリを読み書きします。このプロセスの終了後もカーネルメモリアクセスを維持したい場合は、tfp0 への send-right をそこに永続化してください。
テスト済みデバイス: iPod Touch 6G、iPhone 6S、iPhone SE、iPhone 7、iPhone 8 iOS 11 から iOS 11.3.1 で動作するはずです。