
Pixel7/8 Pro 向け Android 14 カーネルエクスプロイト
この記事では、デフォルトのアプリケーションサンドボックスから到達可能な、Mali GPU内の2つのカーネル脆弱性について詳細に分析します。これらの脆弱性は私が独自に特定し、Googleに報告したものです。本記事には、任意のカーネル読み書き(r/w)機能を実現するカーネルエクスプロイトが含まれています。その結果、以下のAndroid 14バージョンを実行しているGoogle Pixel 7および8 ProモデルでSELinuxを無効化し、rootへの権限昇格を実現します:
google/husky/husky:14/UD1A.231105.004/11010374:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keysgoogle/panther/panther:14/UP1A.231105.003/11010452:user/release-keys(m4b4 (Marcel)による)このエクスプロイトは、gpu_pixel_handle_buffer_liveness_update_ioctl ioctlコマンドの不完全なパッチに起因する整数オーバーフローと、タイムラインストリームメッセージバッファ内の情報漏洩という2つの脆弱性を利用します。
Googleは、このコミットでgpu_pixel_handle_buffer_liveness_update_ioctl ioctlコマンドの整数オーバーフローに対処しました。当初、この問題を報告したとき、このバグは前述のパッチの問題が原因だと考えていました。レポートを確認した後、この脆弱性に関する私の分析が不正確だったことに気付きました。パッチが不完全であるという最初の想定に反して、このパッチは計算におけるアンダーフローを効果的に解決し、防止しています。このことから、プロダクションビルドではこの変更が適用されていないのではないかと疑うようになりました。しかし、計算ではアンダーフローを引き起こすことはできますが、オーバーフローを引き起こすことはできません。これは、ioctlコマンドが部分的に修正されていることを示唆しています。ただし、上記のパッチによる修正ではありません。IDAで確認したところ、プロダクションリリースには別の不完全なパッチが含まれており、このパッチはmali gpuカーネルモジュールのどのgitブランチにも存在しないことが明らかになりました。
この脆弱性は、最新のAndroidバージョンで最初に発見され、2023年11月19日に報告されました。Googleは後日、すでに社内でこの脆弱性を特定しており、2023年12月のAndroid Security BulletinでCVE-2023-48409として割り当て、重複問題としてラベル付けしたことを私に知らせました。
報告の数ヶ月前にこのバグが社内で特定されていたことを確認できましたが(コミット日は8月30日頃)、依然として混乱が残っています。具体的には、最新デバイスにおける10月と11月のセキュリティパッチレベル(SPL)が依然としてこの脆弱性の影響を受けていたのは奇妙です — これらの以前のバージョンは調査していません。したがって、これが本当に重複問題であり、適切なパッチが私の報告前に12月に予定されていたのか、それともこの脆弱性への対応が見落とされていたのかを、最終的に判断することはできません。
とにかく、このバグを強力にしているのは次のとおりです:
info.live_rangesは完全にユーザー制御下にあります。info.live_rangesポインタがbuffカーネルアドレスの先頭より前の任意のオフセットを指すように計算をオーバーフローさせることができます。この脆弱性は、2022年にiOS 15カーネルで私が発見し悪用したDeCxt::RasterizeScaleBiasData() バッファアンダーフロー脆弱性と類似点があります。
Mali GPUは、情報を収集してシリアライズし、その後特定の形式に従ってリングバッファに書き込むように設計されたカスタムのtimeline streamを実装しています。ユーザーはioctlコマンドkbase_api_tlstream_acquireを呼び出してファイルディスクリプタを取得し、このリングバッファから読み取ることができます。メッセージの形式は次のとおりです:
シリアライズされたメッセージバッファ。具体的な内容はメッセージIDによって異なります。
例えば、__kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait関数は、kbase_kcpu_command_queueおよびdma_fenceのカーネルポインタをメッセージバッファにシリアライズし、その結果、カーネルポインタをユーザースペースプロセスに漏洩させます。```c
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags); }
概念実証エクスプロイトは、新しい kcpu キューオブジェクトが割り当てられるたびに `kbasep_kcpu_queue_new` 関数によって送出されるメッセージ ID `KBASE_TL_KBASE_NEW_KCPUQUEUE` を監視することで、`kbase_kcpu_command_queue` オブジェクトのアドレスを漏洩させます。
Googleからは、この脆弱性は2023年3月に報告され、同社のセキュリティ速報で [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) に割り当てられたと通知されました。それにもかかわらず、私は10月と11月のセキュリティパッチレベル(SPL)を搭載して出荷された最新のPixelデバイスでこの問題を再現することができ、修正が正しく適用されていないか、まったく適用されていないことを示していました。その後、Googleはクレジットを付与せずに12月のセキュリティ更新速報でこの問題に迅速に対処し、後になってこの問題は重複と見なされたと私に知らせました。しかし、この問題を重複と見なした根拠には疑問が残ります。
## 悪用
---
というわけで、私には2つの興味深い脆弱性があります。1つ目は、割り当てられた ~buff~ アドレスの前に位置する任意の16バイト境界で整列されたカーネルアドレスの内容を変更できる強力な機能を提供します。2つ目の脆弱性は、カーネルメモリ内のオブジェクトの潜在的な場所に関する手がかりを提供します。
### buffer_count と live_ranges_count の値に関する注記
`buffer_count` と `live_ranges_count` フィールドを完全に制御できるため、ターゲットのslabと書き込み先の正確なオフセットを柔軟に選択できます。ただし、`buffer_count` と `live_ranges_count` の値を選択するには、いくつかの制約と要因があるため、慎重な検討が必要です。
- 両方の値は関連しており、新たに導入されたすべてのチェックをバイパスした場合にのみオーバーフローが発生します。
- 負のオフセットが16バイト境界で整列されなければならないという要件により、選択した任意の場所に書き込む能力が制限されます。ただし、これは一般的に大きな障害にはなりません。
- より大きなオフセットを選択すると、意図したターゲットではないメモリ領域に大量のデータが書き込まれることになります。例えば、割り当てサイズが `0x3004` にオーバーフローした場合、`live_ranges` ポインタは `buff` オブジェクトの割り当て領域から `-0x4000` バイトの位置に設定されます。`copy_from_user` 関数は、`update->live_ranges_count` を4倍した計算に基づいて `0x7004` バイトを書き込みます。その結果、この操作により、`live_ranges` ポインタと `buff` 割り当ての間のメモリ領域にユーザー制御のデータが上書きされることになります。したがって、その範囲内の重要なシステムオブジェクトが誤って上書きされないように慎重に確認することが不可欠です。この操作が `copy_from_user` 呼び出しを含むことを考慮すると、機密性の高い場所へのデータ書き込みを防ぐために、ユーザーソースバッファの後に続く不要なメモリ領域を意図的にマッピング解除して `EFAULT` をトリガーすることを検討するかもしれません。しかし、このアプローチは効果的ではありません。なぜなら、`raw_copy_from_user` 関数が失敗した場合、宛先カーネルバッファ内の残りのバイトをゼロで埋めるからです。この動作は、エラーによる部分的なコピーが発生した場合に、カーネルバッファの残りの部分に初期化されていないデータが含まれないようにするために実装されています。```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
これを考慮すると、上書きするオブジェクトと書き込むデータを慎重に選択する必要があります。
この厄介なチェックに縛られているため、私の戦略は、null にしても望ましくない結果を生じないオブジェクトを見つけることです。しかし、その前に、対処すべき別の問題があります。前回のパートで、任意の割り当てサイズを選択でき、したがって任意の汎用スラブキャッシュアロケータを自分の割り当てバッファに使用できると言ったのを覚えていますか? それは正しくありません。なぜなら、またしても copy_from_user が原因だからです! これは CONFIG_HARDENED_USERCOPY 緩和策によるものです。この緩和策は、カーネルの宛先バッファ(この場合ヒープオブジェクト)に対応するスラブキャッシュサイズを満たさないサイズの指定を禁止します。これは、バッファのページがスラブページであるかどうかを判定し、そうであれば対応する kmem_cache->size を取得して、ユーザー指定のサイズがそれを超えないかどうかを判定します。超える場合、カーネルはサイズ不一致によりクラッシュします。つまり、汎用アロケータに属するオブジェクトをターゲットにすることはできませんが、大きなサイズを持つオブジェクト(すなわち、ページアロケータによって直接処理されるオブジェクト)は依然としてターゲットにできます。
最初に思いついたのは、任意の読み書きプリミティブを取得するための非常にエレガントなテクニックである pipe_buffer テクニックを使うことでした。このテクニックの詳細には触れませんが、読者は Interrupt Labs の素晴らしいブログを読むことをお勧めします。パイプオブジェクトを構築するとき、pipe_buffer オブジェクトは最初は16要素の配列として作成されます。しかし、配列サイズは fcntl(F_SETPIPE_SZ) を使って調整できます。したがって、pipe_buffer 配列の割り当てを調整して、ページアロケータから取得されるようにすることができ、攻撃対象として完璧なオブジェクトになります。
ターゲット候補として pipe_buffer オブジェクトを選択した後、カーネルの r/w を達成するための次のステップは、アンダーフロー脆弱性を使ってその内容を上書きすることです。これにより、pipe_buffer->page フィールドに上書きされたページに対応する任意のメモリ位置への読み書きが可能になります。
この脆弱性により任意のデータを書き込めるため、pipe_buffer のコンテンツ全体(page フィールドを含む)を制御できます。そのためには、脆弱な kbuff オブジェクトの前に pipe_buffer 配列を割り当て、それらが隣り合っている必要があります。
私はカーネルメモリを多数の kbase_kcpu_command_queue オブジェクトでスプレーし、その後に多数の pipe_buffer 配列を配置しました。
pipe_max_size によって課される制限のため、スプレーの主要なソースとして pipe_buffer 配列だけを使うことはできません。そこで、まず kbase_kcpu_command_queue オブジェクトでスプレーすることにしました。kbase_kcpu_command_queue オブジェクトを選んだ理由は2つあります。その割り当てサイズは 0x38C8 であり、ページアロケータによって処理されること、そしてカーネル情報漏洩バグを使ってそのカーネルアドレスを決定的に取得できることです。そのため、スプレーに適したオブジェクトであり、攻撃ターゲットとしても適しています(次のセクションで説明します)。
前述のとおり、fcntl(F_SETPIPE_SZ) を使用して pipe_buffer 配列の割り当てサイズを増やし、ページアロケータから取得されるようにしました。具体的には、kbase_kcpu_command_queue の割り当てと整合性を取るために、割り当てサイズを ==0x4000 バイト(4 * PAGE_SIZE)== にしました。