
Android GKI 6.12カーネル向けのCVE-2026-43499エクスプロイト。rt_mutexのロールバックバグとpselectのスタック上書きを連鎖させ、SamsungおよびPixelデバイスでroot権限を取得します。
Android GKI 6.12 系(Samsung および Pixel デバイス)を対象とした Linux カーネルエクスプロイトで、CVE-2026-43499 を狙う: rt_mutex_start_proxy_lock() 内の欠陥のある remove_waiter() ロールバックが waiter::task ではなく current を使用してしまい、waiter の pi_blocked_on が(後にポップされる)カーネルスタック上の rt_mutex_waiter を指したままになる。これに pselect() の fd_set スタック上書きと、consumer 側の sched_setattr 駆動による偽 rb_tree 走査を組み合わせることで、決定論的なカーネル書き込みプリミティブと完全な root が得られる。
このエクスプロイトは LD_PRELOAD 共有ライブラリ(preload.so)として動作し、root 権限の su デーモンと壁紙を post-root 成果物としてインストールする。
ステータス: 活発に開発中。m1q (ZF1) ターゲットが立ち上げの焦点である。 pipei tmp_page-uname ブートストラップが現在のアクティブな経路である: 走査は 実機上でクリーンであることが証明されており(build #33: 子プロセスごとにシードされた rt_mutex 領域)、168 候補の完全なスイープが進行中である。configfs CFI 経路は m1q では行き止まりである(Rust ashmem — 注入可能な fops スロットが存在しない)が、 C-ashmem ターゲット向けの経路として残っている。ターゲットごとのオフセットは デバイスによって異なる — 信頼する前に実際のカーネルバイナリと照合して検証すること。
FUTEX_CMP_REQUEUE_PI が waiter を requeue し、requeue のデッドロック検出チェーン走査が -EDEADLK を返すと、__rt_mutex_start_proxy_lock() は remove_waiter() (rtmutex.c:1535) を介してロールバックする。このロールバックは waiter を待機ツリーから正しくデキューするが、waiter->task->pi_blocked_on ではなく requeue 呼び出し元の pi_blocked_on をクリアしてしまう。WAITER の pi_blocked_on はスタック上の rt_mutex_waiter を指したままダングリングし、futex がタイムアウトするとポップされる。
タイムアウト後、waiter のカーネルスタック領域は再利用される: core_sys_select() が 3 つの fd_set をそのスタックバッファにコピーする(ZF1 では nfds < 344 のスタック経路)。細工された fd_set ワード配列が、偽の rt_mutex_waiter / 偽タスク / 偽 rt_mutex(ポインタが制御された rb_tree ルートを持つ)を再構築する。その後 consumer スレッドが sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached を呼び出し、制御された値の任意アドレス書き込みを生み出す。
このプリミティブ全体は PI チェーンのサイクルを必要とする: オーナーは f_pi_target を保持しつつ、waiter が保持する f_pi_chain でもブロックするため、チェーン走査は owner → chain → waiter → target → owner に到達して -EDEADLK で失敗する。サイクルを除去すると requeue は成功を返し、カーネルへの影響はゼロになる。
sched_blocked_reason(m1q の主経路、実機で証明済み): ブロックされた kworker の保存済みリターン PC(stack_trace_save_tsk)をリングバッファから読み取り、コンパイル時に埋め込まれた worker_thread オフセットと比較する。boot_id 経路のワードが使われる前に実行されるため、データエイリアス書き込みターゲット(data_addr() = p0 エイリアス + slide_p0_offset)は非ゼロのスライドでも正しいままである。SLIDE_LOGGERS_0_1 を boot_id sysctl データに植え付け、リークされた値が stext を再構築する。m1q では保留中: その W1 ターゲット(SLIDE_RANDOM_BOOT_ID_DATA_OFF、低物理 RAM)はページ依存の書き込み可能性を持ち、スライドがブートごとにターゲットをページ境界をまたいで移動させるため、約 6/7 の実行でフォールトする。SLIDE_FORCE_BOOTID(メカニズムテスト)で明示的に実行される場合のみで、tree_pc/tree_left はスプレーされたページにリダイレクトされる。mm_struct サイズのオブジェクトをスプレーし、futex ハッシュ衝突を利用してヒープ上の mm_struct を特定し、偽オブジェクトスプレーのベースとして使われるカーネルヒープページアドレスをリークする。waiter はクリーンアップ時に join せずデタッチされる(#26 以前のビルドをクラッシュさせたカーネルスタック OOM を回避するため)。pselect の fd_set ワードが waiter のカーネルスタック上に偽の rt_mutex_waiter を再構築し、consumer スレッドの sched_setattr_tid(waiter) が rt_mutex_adjust_pi() を介してそれを走査する。ZF1 ワードマップ(逆アセンブリ検証済み): PSELECT_WAITER_WORD_SHIFT = 0、ワード 12 = task (@+0x50)、ワード 13 = lock (@+0x58)、ワード 14 = wake_state (@+0x60 = 3)。シードされた偽 rt_mutex があれば走査はクリーンに完了する: [7] の rb_erase W1 *(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left が唯一のカーネル書き込みであり、[11](setprio / dequeue_pi)と [9] の wake はどちらもスキップされる(prio 100 のローカル偽 waiter がスタックノードを top-waiter から除外し続ける)。build #19 以降のすべての走査進入完了は実機上で生存している。それ以前の決定論的クラッシュは 8 バイトのペイロード配置エラー(SKB_DATA_DELTA)であり、走査本体のフォールトではなかった。
オプションの STAGE3=1 フェーズで、ブリッジ子プロセスを fork し、その mm->pgd をステージングされた偽ページテーブル(3 レベル: PGD→L1→L2、RX ループリーフ + RW スタックリーフ)にスワップし、子プロセスにレジスタのみのアセンブリブロブを実行させる。このブロブは 2MB の物理スキャンウィンドウを通じて自身の cred をパッチする — configfs/pipe プリミティブなしの全 RAM 物理 R/W である。現在は m1q ターゲットにのみ配線されており、実機では未実行である。
src/targets/<codename>-<build>/ に 41 ターゲットがあり、それぞれ最低限 target.h(カーネルシンボルオフセット、KIMAGE_TEXT_BASE、direct-map ベース、構造体オフセット)を必要とする。Pixel ターゲット(comet、tokay、tegu、caiman、komodo、frankel、mustang、rango、stallion、blazer)は共有ソースを上書きし、Samsung ターゲット(m1q-*)はデバイス固有のロジックを追加する。
make list-projects # full list
カーネル構造体レイアウトはデバイスごとに異なる — オフセットは各ターゲットの実際のカーネルバイナリ / kallsyms と照合して検証する必要がある。
出力: build/<PROJECT>/bin/preload.so(LD_PRELOAD 経由でロードされる共有ライブラリ)。
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
ビルドは PIE の su_daemon バイナリ(src/su_daemon.c、build/embed/su_daemon_aarch64_pie にビルド)と assets/wallpaper.webp を、src/su_blob.S / src/wallpaper_blob.S を介して preload.so に埋め込む。
ビルド出力を /data/local/tmp にプッシュし、LD_PRELOAD の下でエクスプロイトを実行する。ライブの立ち上げターゲットでは tee なしで実行すること — tee は自身の stdio をバッファリングし、カーネルパニック時にログの末尾を失う(エクスプロイトの stdout はアンバッファリングなので、ファイルへ直接リダイレクトすればすべての行が保持される):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
成功するとデーモンは /data/local/tmp/temp_su.sock で待ち受け、su は /apex/com.android.virt/bin の下にインストールされる。
tools/run_pipei.sh はバイナリをプッシュし、実機上で sha256 検証し(古い preload.so はサイレントに読めない判定を生む — ラダーはかつてログ形式が異なる build < #22 を実行したことがある)、その後 168 候補の完全なスイープを 3x56 でチャンク化して実行するため、パニックで失われるのは最大 1 チャンク(約 10 分)である。エクスプロイトの stdout は adb を通じて直接ストリームされる(デバイス側リダイレクトなし — デバイス側の tee はパニック時に末尾を失う)。各チャンクのホスト側コピーは /tmp/m1q_pipei_chunk{1..3}_stream.log に保持される:
sh tools/run_pipei.sh
PASS = いずれかのチャンクが tmp_page uname changed + after uname='CatOS を示す。PIPEI_CHILD_REGIONS は少なくともチャンクサイズに設定する必要があり、そうでなければ子プロセスごとの rt_mutex 領域がシードされないままになり、スイープは候補 0 でパニックする。完全なカバレッジには TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 も必要である — これがないと解放された 8 ページの mm ブロックのうち 1 ページしかスイープされない。run_pipei_slim.sh は単一候補の診断変種であり、run_m1q_ladder.sh は走査/分離バッテリー(B16-2 オラクル、スライドメカニズム、スイープステップ)である。
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
ターゲットは Makefile の pick_src メカニズムを通じて共有ソースを上書きする: src/targets/<PROJECT>/<file>.c が存在すれば共有の src/<file>.c の代わりに使用され、そうでなければ共有版が使用される。
-EDEADLK(errno 35)で失敗することに依存している。サイクルが欠落すると success=1 でカーネルへの影響がゼロになる。PSELECT_WAITER_WORD_SHIFT は 0 でなければならない(ZF1 のスタック深さは ELF から検証済み)。他のターゲットは依然としてデフォルト 1 — 信頼する前にデバイスごとに検証すること。p0_data_alias() は DIRECT_MAP_BASE(線形マップ)を使用し、これは ZF1 では P0_PAGE_OFFSET と異なる。逆参照されるカーネルオブジェクトのデータエイリアスは線形マップを使用しなければならない。pr_error() は致命的(exit(-1))であり、警告/情報マクロとは異なる。SLIDE_NFULNL_LOGGER ではなく SLIDE_LOGGERS_0_1 を使用する; このワードは非ランダム化された線形マップエイリアスであるため、boot_id 経路はスライド非依存である(slide=0 でのみ使用 — 先に tracefs リークを実行すること)。SKB_DATA_DELTA は -0xe78(実機測定、build #19) — 実際の skb frag head は逆アセンブリ由来の -0xe80 より 8 バイト少ない; この 8 バイトの誤差が build #6-18 のすべての決定論的 pselect クラッシュ(走査進入 → ガベージ逆参照)を引き起こした。GKI 6.12 の共有値; ターゲットごとの util.c コピーは 環境 A/B ノブを持つ。焼き込まれた定数の上に を重ねないこと。これは Android デバイス向けのエクスプロイトコードであり、セキュリティ研究、許可された脆弱性テスト、および自身が所有するか明示的なテスト許可を得たハードウェアでの防御作業を目的としている。所有していないデバイスでの悪用は違法である。作者は文鎮化したデバイス、文鎮化したカーネル、または無効化された保証について責任を負わない。
write_iter スロットがなく、ASHMEM_SET_NAME ヒープオブジェクトは KVec であり、そのレイアウトは configfs_bin_write_iter の private_data と一致しない。m1q は代わりに kmalloc 書き込みへとピボットする: リークされた mm の order-3 ブロックを pipe_inode_info オブジェクトとして再利用し、pselect W1 書き込み(*(tree_left) = tree_pc)を使って候補の tmp_page スロット(+0x90)を UTS 名前空間ページ(init_uts_ns)で上書きする。ファンアウト書き込みが sysname オフセットにマーカー名を植え付け、uname() が "CatOS" を報告する — 静的オブジェクトに依存しない検証可能なカーネル書き込みである。C-ashmem ターゲットは configfs 経路を維持する。preload.c が埋め込みの su デーモン(/apex/com.android.virt/bin に tmpfs マウント、加えて adbd 名前空間およびローカル変種)をインストールし、壁紙を差し替える。| Variable | Default | Purpose |
|---|
PSELECT_ROUTE_SHIFT | compile-time | 主経路 (m1q) の A/B fd_set ワードシフト |
SLIDE_SHIFT | compile-time | スライド経路の A/B fd_set ワードシフト |
PSELECT_ROUTE_DELAY_USEC | 50000 | sched_setattr 前の consumer 遅延 (0 より大きい必要あり) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | スライド経路のタイミング/ピン留め |
SKIP_SLIDE | 0 | direct-map フォールバックを使用 (slide = 0) |
SLIDE_ONLY | 0 | KASLR スライドのみを実行して終了 |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | スライド p0 エイリアス / boot_id オフセットを上書き |
SLIDE_FORCE_BOOTID | 0 | tracefs スライド後に boot_id pselect 経路を強制 (メカニズムテスト) |
KSNITCH_COLLISIONS | 4 | KernelSnitch 衝突回数 |
STAGE3 | 0 | pgd-swap ブリッジ子プロセスフェーズを有効化 (m1q) |
STAGE3_DRYRUN | 0 | テーブルをステージ/検証してからスワップ前に中止 |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | スプレー/fd_set レイアウトの調整 |
TMP_PAGE_UNAME | 0 | m1q pipei tmp_page-uname ブートストラップを実行 |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | スイープモード; split_order は解放された mm ブロックの全 8 ページをスイープ |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | スイープ範囲 (チャンク開始 / カウント) |
PIPEI_CHILD_REGIONS | 0 | 子プロセスごとのシードされた rt_mutex 領域; スイープチャンク以上でなければならず、そうでないと未シード領域が候補 0 でパニック |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | pipe ペアの再利用/ドレイン; 再利用ミス時に増やす |
PIPEI_SPONGE_MB | 3072 | 再利用を通じて保持される order-0 フラッドスポンジ (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | top-waiter の prio; シード領域では無効 (レガシー stale-tree ガード、A/B のみ) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B ペイロードのページ内オフセット (デバイスが -0xe78 と異なる場合のみ) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | 成功後のマーカー名 / 保持時間 |
SKB_DATA_DELTAPSELECT_SKB_DELTA_OVERRIDEASHMEM_SET_NAME KVec エンベロープは configfs_bin_write_iter と一致しない。pipei tmp_page ブートストラップを使用すること。m1q target.h の PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF ガードは、走査の [11] 書き込みを RO 静的 fops からリダイレクトする。payload+0x6000 + idx*0x40)にリダイレクトする。共有 fake_lock スイープは候補 ~9-12 でパニックし(古いカーネルスタックノードがリサイクルされた)、build #32 の空領域は候補 0 でパニックした(0x41 で埋められた fake_task 上での [9] owner==NULL wake)。PIPEI_CHILD_REGIONS を設定しなければ領域は未シードのままになる。