
MediaTek IONアロケーターの型混同に関するAndroidカーネルのCVE解析とPoC。根本原因の差分解析、非特権でのトリガー、悪用可能性の評価をカバー。
概要. CVE-2023-20768 は、MediaTek の ION アロケータにおける型混乱 (CWE-843) です。私は、2022年7月のファームウェアを実行している Samsung SM-M325F (Galaxy M32, Helio G80) で、これが実際に悪用可能かどうかを確認しました。脆弱なコードは存在し、非特権プロセスからトリガーしたときにこのデバイス上で実行されることを実証しました。武器化には至りませんでした。ioctl パスは入力検証によって制限されており、実際の領域外読み取りを含むパスは、dma_buf_ops ポインタに対する2番目のチェックが私が偽造できるものをすべて拒否するため、本物の ION バッファだけを処理します。この記事では、その結論に至った方法を説明します。途中で間違っていたと判明した暫定的な結論も含みます。
この CVE は公開済みで、パッチ適用済みです。すべてのテストは、Magisk で root 化した自分のデバイスで行いました。
このバグは MediaTek の ION コードにあり、上流の Linux や Samsung が書いたものにはありません。MediaTek は、自社チップを採用するすべてのベンダーに提供する BSP 内に、Android ION アロケータの独自フォークを同梱しています。M32 は Helio G80 を搭載しているため、そのコードが使われます。同じ機種でも Exynos SoC 搭載版はまったく影響を受けません。
2022年(脆弱版)と2023年(パッチ適用版)の vmlinux イメージを取得し、IDA で差分を確認しました。変更された ION 関数は2つです:
| Function | 2022 | 2023 |
|---|---|---|
ion_drv_file_to_buffer | strstr(name, "dmabuf") | is_dma_buf_file() |
_ion_ioctl | strcmp(name, "ion") | is_dma_buf_file() |
is_dma_buf_file は 2022 年のイメージには存在せず、2023 年のイメージに登場します。つまり、両関数は struct file が dma_buf であるかどうかを名前で判定しており、修正によって実際の型チェックに置き換えられました。ここでオブジェクトを混同すると、カーネルは dma_buf ではないものを dma_buf であるかのように読み取ります。
2つのうち、非特権プロセスが到達できるのは _ion_ioctl です:
open("/dev/ion")
-> ion_ioctl (.unlocked_ioctl)
-> ION_IOC_CUSTOM (0xC0104906)
-> ion_custom_ioctl
-> _ion_ioctl
-> case 0: ION_SYS_CACHE_SYNC
-> find_vma(user_VA) (call site at _ion_ioctl+0x9f0)
-> strcmp(vma->vm_file...name, "ion")
リクエストは、120 バイトの ion_sys_data を指す ion_custom_data { u32 cmd = 0; u64 arg; } です:
| オフセット |
|---|
spoof.c はこれを構築します。
単純にトレースすることはできませんでした。このデバイスは、Samsung の SELinux ポリシーとカーネルのハードニングによって kprobe_events、set_ftrace_filter、function_graph をブロックしており、ION の printk はデバッグゲート付きなので dmesg は静かなままです。
そこで代わりに、戻り値をオラクルとして使いました。4つのリクエストを送り、その戻り値のパターンから実行がどこを通ったかが分かります:
C が成功を返すということは、switch が実際に sys_cmd でディスパッチしていることを意味します。A と B の結果が異なるのは、VA が処理されていることを意味し、実行が find_vma の内部に入ることを示します。これが脆弱なパスであり、root なしで到達できます。
チェックに到達することは、それを回避することと同じではありません。私は strcmp が実際に何を受け入れるかをテストしました:
ION_IOC_SHARE 経由でマップしてから mmap した本物の ION バッファは成功し、0 を返します。memfd:ion という名前の memfd は失敗し、-EFAULT になります。ion という名前の通常のファイルも失敗し、-EFAULT になります。つまり、vm_file+0x60 で比較されるフィールドはファイル名ではありません。これは dma_buf 内部のもので、ほぼ間違いなく dma_buf->exp_name であり、ION はこれを "ion" に設定します。このチェックは設計上、型の判定として不健全ですが、ユーザー空間から私が作成できるものではそのフィールドを設定できません。
次にこのパスをファジングしました: sync_type は 0 から 7、サイズは {0, 1, 0x1000, 0x100000, 0xffffffff}、VA は {実 ION バッファ, memfd, 0} の 120 ケースに加えて、use-after-free 用に解放済みハンドルのプローブも実行しました。クラッシュはなく、デバイスは起動したままでした。過大なサイズは find_vma の前に抜けて 0 を返します。sync_type 3 から 5 は m4u パスに入り -EPERM を返します。5 より大きい値は -EINVAL を返します。解放済みハンドルは -EINVAL を返すため、ION はそれを検証しており、そこに UAF はありません。
実際の領域外読み取りは ion_drv_file_to_buffer にあります。これは ldr [private_data+0x28] を実行し、dma_buf ではないオブジェクトの private_data を dma_buf であるかのように読み取ります。ops == &ion_dma_buf_ops の比較 (テーブルは 0xFFFFFF800A097F18 にあります) がありますが、これはその読み取りの後に行われるため、防ぐことはできません。後段では、__do_dump_share_fd が返されたバッファの +0x28、+0x48、+0x50、+0xb8、+0xe4 のフィールドを読み取って出力し、ldr x8, [buf+0x28]; ldr [x8+0x30] は混同されたオブジェクトに対するワイルドポインタ参照になります。
トリガーは ion_dump_all_share_fds で、iterate_fd を使ってすべての ION クライアントプロセスのファイル記述子を走査します。私の最初の結論は、これが ION の debugfs ノードを読み取ったときだけ実行され、このカーネルでは CONFIG_DEBUG_FS が無効になっているというものでした。これを3つの方法で確認しました: /proc/config.gz、/proc/filesystems に debugfs が存在しないこと、mount -t debugfs が ENODEV を返すことです。私はこのパスを構造上到達不能として切り捨てました。
それは間違いでした。OOM キラーのメモリダンプである dump_header には、0xffffff8008204b9c に直接の bl ion_mm_heap_memory_detail が含まれており、dump_header は out_of_memory と oom_kill_process から呼び出されます。debugfs は不要です。
memcg_oom.c がそれを確認しています。これは /dev/memcg の下に cgroup を作成し、memory.limit_in_bytes と memory.memsw.limit_in_bytes の両方を 8MB に制限し (最初の1つだけを上限設定すると、子は zram スワップに逃げ込めてしまう)、子プロセスを fork して、メモリを割り当て続けて死ぬまで実行します。すると dmesg に次のように表示されます:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
その後に、実際の gralloc dma_buf を解決する完全な ion_mm_heap_memory_detail と __do_dump_share_fd の出力が続きます。つまり、非特権プロセスが引き起こせる処理の最中に、この脆弱な関数が実行されます。3つ目のトリガーもあり、MediaTek ウォッチドッグの hang_detect_dump_thread からの ShowStatus です。
私は memfd:dmabuf という名前の memfd を32個保持した状態で、同じ memcg OOM をトリガーしました。もし私の memfd のいずれかが ion_drv_file_to_buffer に渡されれば、strstr は通過し、private_data は NULL になり、カーネルは KERN_ERR で [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL を出力するはずです。しかし、その行は決して現れませんでした。ダンプされたのはグラフィックスと gralloc クライアントだけでした。つまり、通常の /dev/ion クライアントの fd テーブルは、ここで iterate_fd が走査する対象ではないか、memfd がその出力の前に静かに失敗しているかのどちらかです。
いずれにせよ、OOM ダンプはシステムの正規の dma_buf だけを処理し、それらは問題なく通過します。また、memfd は、名前に "dmabuf" を含めるのに十分に制御できる唯一の fd タイプですが、フォールトさせることはできません。
脆弱性は存在します。脆弱なコードは到達可能で、このビルドでは実際に実行され、root なしでトリガーできます。ここではユーザー空間から武器化することはできません。ioctl パスは、ハンドル検証、サイズチェック、access_ok によって制限されています。ダンプパスは本物の ION バッファのみを扱い、偽造は ops == &ion_dma_buf_ops によってブロックされます。さらに先へ進むには、exp_name が "ion" である制御可能な非 ION オブジェクトか、ION バッファの UAF などの別のプリミティブ、または TOCTOU レースが必要です。
spoof.c — cache-sync ioctl の PoC と差分オラクルmemcg_oom.c — ダンプパス用の memcg OOM トリガーtrigger.c, oom_trigger.c — 以前のトリガー試行boot_images/ — 抽出したカーネルイメージ (2022 年版と 2023 年版) と 2022 年ビルドの IDA データベース| フィールド |
|---|
+0x00 | sys_cmd = 0 |
+0x08 | ion ハンドル (最初に1つ割り当てます。heap_id_mask = 0x1 で動作します) |
+0x10 | ユーザー仮想アドレス |
+0x18 | 下位半分 = サイズ、上位半分 = sync_type で {0,1,2} のいずれか |
| ケース | リクエスト | 結果 |
|---|
| A | sys_cmd=0, 偽装 VA | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |