
CVE-2018-4280: iOS 11.2.6上のlaunchdにおけるMachポート置換の脆弱性で、sandbox escape、privilege escalation、codesigning bypassに至る。
Blanket は iOS 11.2.6 を標的としたサンドボックス脱出ツールであり、主要な脆弱性は iOS 11.4.1 でのみ修正されました。このツールは launchd の Mach ポート置換の脆弱性 (CVE-2018-4280) と、その他のサービスにおけるいくつかの小さな脆弱性を悪用し、サンドボックス化されておらず、root で動作し、task_for_pid-allow エンタイトルメントを持つ ReportCrash プロセス内でコードを実行します。これにより、Blanket は amfid のようなセキュリティ上重要なプロセスを含む、電話機上のすべてのプロセスを制御できるようになります。
エクスプロイトは複数のステージで構成されています。この README では、主要な脆弱性とサンドボックス脱出の各ステージを段階的に説明します。
iOS のクラッシュレポートを調査中に、launchd に Mach ポート置換の脆弱性を発見しました。特定の方法でクラッシュすることにより、プロセスはカーネルに Mach メッセージを launchd へ送信させ、そのメッセージによって launchd が自身の IPC 名前空間内の Mach ポートへの送信権を過剰に解放してしまいます。これにより、攻撃者はシステムの他の部分に対して、自身が検索可能な任意の launchd サービスになりすますことができ、権限昇格への多くの経路が開かれます。
この脆弱性は macOS にも存在しますが、iOS で脆弱性を引き起こすのはより困難です。launchd には Mach 例外メッセージがカーネルから送信されたものであることを確認するチェックがあるためです。
Launchd はメインポート上で複数の異なる Mach メッセージハンドラを多重化しており、例外メッセージ用の MIG ハンドラも含まれています。プロセスが自身のブートストラップポートに対して mach_exception_raise または mach_exception_raise_state_identity メッセージを送信すると、launchd はそのメッセージをホストレベルの例外として受信し処理します。
残念ながら、launchd によるこれらのメッセージの処理にはバグがあります。例外タイプが EXC_CRASH の場合、launchd はメッセージ内で送信されたスレッドポートとタスクポートを解放し、その後サービスルーチンから KERN_FAILURE を返します。これにより MIG システムがスレッドポートとタスクポートを再度解放することになります。(前提として、サービスルーチンが成功を返した場合は Mach メッセージ内のすべてのリソースの所有権を取得したと見なされ、サービスルーチンがエラーを返した場合はどのリソースの所有権も取得していないと見なされます。)
以下は、mach_exception_raise メッセージに対する launchd のサービスルーチンのコードを、IDA/Hex-Rays で逆コンパイルし、読みやすく軽く編集したものです。```C
kern_return_t __fastcall
catch_mach_exception_raise( // (a) The service routine is
mach_port_t exception_port, // called with values directly
mach_port_t thread, // from the Mach message
mach_port_t task, // sent by the client. The
exception_type_t exception, // thread and task ports could
mach_exception_data_t code, // be arbitrary send rights.
mach_msg_type_number_t codeCnt)
{
__int64 __stack_guard; // ST28_8@1
kern_return_t kr; // w0@1 MAPDST
kern_return_t result; // w0@4
__int64 codes_left; // x25@6
mach_exception_data_type_t code_value; // t1@7
int pid; // [xsp+34h] [xbp-44Ch]@1
char codes_str[1024]; // [xsp+38h] [xbp-448h]@7
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( current_audit_token.val[5] ) // (b) If the message was sent by
{ // a process with a nonzero PID
result = KERN_FAILURE; // (any non-kernel process),
} // the message is rejected.
else
{
if ( codeCnt )
{
codes_left = codeCnt;
do
{
code_value = *code;
++code;
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
--codes_left;
}
while ( codes_left );
}
launchd_log_2(
0LL,
3LL,
"Host-level exception raised: pid = %d, thread = 0x%x, "
"exception type = 0x%x, codes = { %s }",
pid,
thread,
exception,
codes_str);
kr = deallocate_port(thread); // (c) The "thread" port sent in
if ( kr ) // the message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_port(task); // (d) The "task" port sent in the
if ( kr ) // message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( exception == EXC_CRASH ) // (e) If the exception type is
result = KERN_FAILURE; // EXC_CRASH, then KERN_FAILURE
else // is returned. MIG will
result = 0; // deallocate the ports again.
}
*__stack_chk_guard_ptr;
return result;
}
コードが行う処理は以下の通りです。
1. この関数は、`mach_exception_raise` 例外メッセージのための Mach サービルーチンです。launchd が `mach_exception_raise` Mach 例外メッセージを処理する際に、Mach システムによって直接呼び出されます。サービルーチンの引数は Mach メッセージから解析され、したがってメッセージの送信者によって制御されます。
2. (b) で、launchd は Mach 例外メッセージがカーネルによって送信されたことを確認します。送信者の監査トークンには、フィールド 5 に送信プロセスの PID が含まれており、カーネルの場合のみゼロになります。メッセージがカーネルによって送信されていない場合は拒否されます。
3. メッセージからのスレッドポートとタスクポートは、(c) と (d) で明示的に解放されます。
4. (e) で、launchd は例外タイプが `EXC_CRASH` であるかどうかを確認し、そうであれば `KERN_FAILURE` を返します。その意図は `EXC_CRASH` メッセージを処理しないようにすることであり、おそらく ReportCrash が遺体ハンドラとして呼び出されるようにするためです。しかし、この時点で `KERN_FAILURE` を返すと、後で例外メッセージがクリーンアップされる際にタスクポートとスレッドポートが再度解放されることになります。つまり、これら2つのポートは過剰に解放されることになります。
この脆弱性を有用にするためには、launchd が提供する Mach サービスへの送信権を解放し、それによってシステムの他の部分に対してそのサービスを偽装できるようにしたいと考えます。つまり、例外メッセージ内のタスクポートとスレッドポートが、実際に launchd 内で解放したい Mach サービスポートへの送信権である必要があります。そして、launchd に悪意のある例外メッセージを送信してサービスポートを解放した後、同じポート名が再利用されるようにしますが、今度は自分たちが受信権を持つ Mach ポートに対して再利用されるようにします。そうすれば、クライアントが launchd にサービスの Mach ポートへの送信権を要求したときに、launchd は代わりに自分たちのポートへの送信権を渡し、そのサービスをクライアントに対して偽装できるようになります。その後、システム特権を獲得するためのさまざまな方法があります。
### 脆弱性の引き金
実際に脆弱性を引き起こすには、メッセージがカーネルによって送信されたというチェックをバイパスする必要があります。なぜなら、例外メッセージを直接 launchd に送信しても破棄されるだけだからです。何とかして、カーネルに、実際のスレッドポートとタスクポートの代わりにシステムサービス用の Mach 送信権を含む「悪意のある」例外メッセージを送信させる必要があります。
実際には、`task_set_special_port` という Mach トラップがあり、特定の状況で真のタスクポートの代わりに使用されるカスタム送信権を設定するために使用できます。この状況の1つは、カーネルがタスクに代わって例外メッセージを生成するときです。カーネルは、例外メッセージに真のタスク送信権を配置する代わりに、`task_set_special_port` によって供給された送信権を使用します。具体的には、タスクが `task_set_special_port` を呼び出して、その `TASK_KERNEL_PORT` 特殊ポートにカスタム値を設定し、その後タスクがクラッシュすると、カーネルによって生成された例外メッセージの "task" フィールドには、真のタスクポートではなく、カスタムポートへの送信権が含まれます。同様の API `thread_set_special_port` を使用して、生成された例外メッセージの "thread" フィールドにカスタムポートを設定できます。
この動作により、カーネルにタスクポートとスレッドポートの代わりに Mach サービスポートを含む「悪意のある」例外メッセージを生成させることは実際には全く難しくありません。ただし、生成した例外メッセージが確実に launchd に配信されるようにする必要があります。
繰り返しになりますが、適切な API を知っていれば、カーネルに「悪意のある」例外メッセージを launchd に配信させることは難しくありません。`thread_set_exception_ports` 関数は、このスレッド上の例外メッセージが配信されるポートとして任意の Mach 送信権を設定します。したがって、`thread_set_exception_ports` をブートストラップポートで呼び出すだけで、生成した任意の例外によってカーネルが launchd に例外メッセージを送信するようになります。
パズルの最後のピースは、正しい例外タイプを得ることです。この脆弱性は `EXC_CRASH` 例外に対してのみ引き起こされます。少し試行錯誤すれば、標準の `abort` 関数を呼び出すことで `EXC_CRASH` 例外を簡単に生成できることがわかります。
したがって、要約すると、既存の文書化された API を使用して、カーネルに代わりに悪意のある `EXC_CRASH` 例外メッセージを生成させ、それを launchd に配信させることで、脆弱性を引き起こし、Mach サービスポートを解放できます。
1. `thread_set_exception_ports` を使用して、このスレッドの例外ハンドラとして launchd を設定します。
2. `bootstrap_look_up` を呼び出して、偽装したいサービスのサービスポートを launchd から取得します。
3. `task_set_special_port`/`thread_set_special_port` を呼び出して、例外メッセージ内の真のタスクポートとスレッドポートの代わりにそのサービスポートを使用します。
4. `abort` を呼び出します。カーネルは `EXC_CRASH` 例外メッセージを launchd に送信しますが、メッセージ内のタスクポートとスレッドポートはターゲットのサービスポートになります。
5. Launchd が例外メッセージを処理し、サービスポートを解放します。
### クラッシュ後のコード実行
上記の戦略には問題があります。`abort` を呼び出すと、私たちのプロセスが強制終了されます。脆弱性を引き起こした後でもコードを実行できるようにするには、別のプロセスでクラッシュを実行する方法が必要です。
(他の例外タイプでは、プロセスは実際に例外から回復できます。プロセスが回復する方法は、スレッド例外ハンドラを launchd に設定し、タスク例外ハンドラを自分自身に設定することです。launchd が例外を処理し、処理に失敗した後、カーネルは例外をタスクハンドラに送信し、タスクハンドラがスレッド状態をリセットして、例外が処理されたことをカーネルに通知します。ただし、プロセスは自分自身の `EXC_CRASH` 例外をキャッチできないため、確かに2つのプロセスが必要です。)