
CVE-2026-43499 を標的とした、Android 16 と MediaTek MT6993 SoC を搭載する Xiaomi 17T Pro (warhol) 向けのローカル権限昇格エクスプロイト。
CVE-2026-43499(IonStack)のローカル権限昇格エクスプロイトを、Xiaomi 17T Pro(warhol) — MediaTek MT6993、Android 16 向けに移植。
GKI 6.12 / android16 のみ対応。 waiter オーバーレイ、
pselectスタックレイアウト、および すべての構造体オフセットはそのブランチに固定されているため、ここにあるものは 6.6、6.1、5.10 には移行できません — それらには専用に構築されたベースが必要です。generate_target.pyは、デバイス上でフォールトするヘッダを出力する代わりに 他のバナーをすべて拒否します。
ステータス: 動作確認済み。 2026-07-26 にクリーンブートからデバイス上で、両方の preloader バリアントで検証済み —
uid=0(root) context=u:r:kernel:s0、SELinux permissive、suインストール済み。ルートは永続化されません。起動のたびにLD_PRELOAD行を再実行してください。 pselect レースは 100% ではありません。失敗した実行は端末をパニックさせる可能性があり、再起動後の再試行 は正常です。完全な記録はWARHOL_PORT.mdを参照してください。
このデバイスには kernel-MTE 修正が必要です。 ストックのアップストリーム popsicle ではルート化できません —
kernel page retry N/12で無限にループします。カーネル MTE を参照してください。
ビルドには 2 種類の preloader フレーバーがあります。PRELOADER=retail(デフォルト)と PRELOADER=eng です。これらは異なるターゲットディレクトリを選択するため、どちらも他方のアドレスを上書きできません — Preloader バリアント を参照してください。
以下の情報は製品版フル OTA パッケージから読み取ったものです — メタデータ、A/B ペイロードマニフェスト、および payload.bin から抽出した boot/vendor_boot/dtbo イメージです。
ここではカーネルが SoC よりも重要です。warhol はアップストリーム popsicle と同じ GKI ブランチ(android16-5、4K ページ)にあり、パッチレベルが 1 つだけ異なります — 6.12.38 に対して popsicle の検証済み 6.12.23。構造体オフセットはビルドごとに再生成する必要があります。引き継がれるのはエクスプロイトの構造だけです。異なる OS3.0.x ビルドには独自の target.h が必要です。
エクスプロイトチェーンは SoC ではなくGKI ブランチにバージョン固定されています。rt_mutex waiter レイアウト、pselect スタックオーバーレイ、pipe_buffer 構造体オフセットはすべてカーネルに追従するため、6.12/android16 ベースは古いブランチ上の同ベンダーベースよりも優れています。
x-spy/CVE-2026-43499-popsicle は、6.12/android16 GKI ライン(Xiaomi 17 / Pro / Ultra、カーネル 6.12.23-android16-5)で検証された唯一の公開実装です。そのため、source/ はそこから取得し、可能な限りアップストリームに近い状態を保っています — 2 行を除いて(このデバイスが動作するために必須の カーネル MTE を参照)。
MediaTek 差分のほとんどはビルド時のターゲット生成に限定されており、2 つの部分に分かれます:
xbl_config パーティションから p0_phys_offset と p0_kernel_phys_load を読み取りますが、warhol にはそれがありません。代わりに generate_target.py --dtb は vendor_boot FDT の /memory ノードから DRAM ベースを読み取り、arm64_memblock_init と同じ方法で切り捨てます。カーネルの物理ロードアドレスは --preloader で preloader の mb_kernel 予約から読み取ります。このデバイスでは差分は 0 となり、lk は他の場所に置かれたカーネルの起動を拒否します。Image ではなく boot.img 内に LZ4-legacy 圧縮して提供するため、ジェネレータは解析前にそれを展開します。WARHOL_PORT.md §2 に完全な導出過程があります。
MediaTek lk はカーネルをコンパイル時定数にロードするのではありません — mb_kernel という名前の DRAM 予約を検索し、それが正確にそこに配置されたことを検証します。その予約を行うのは preloader であり、そのために、端末が実行している preloader ビルドがそもそもここでのビルド入力になるのです。それは boot.img の外側で P0_KERNEL_PHYS_LOAD を動かし得る唯一のものであり、そこでの誤った値はエクスプロイトの失敗ではなく、誤ったリニアマップエイリアスと端末の破損を意味します。
したがって、別々のターゲットディレクトリとして 2 つのバリアントが用意されています:
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # both of the above, and print their sha256
両方のアーティファクトは異なる名前で並置されます。このファームウェアではバイト単位で同一になります — これは以下の解析の結果であり、それを回避するための近道ではありません。そのため、2 つは依然として別々にビルドおよび命名されます。
2 つの preloader のメモリレイアウトテーブルは、次のコマンドで自分で確認できます:
python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
このファームウェアでは 2 つのテーブルは同一で、両方とも mb_kernel.start = 0x80000000 です。そのため eng の target.h は retail のものとバイト単位で同一になり、両方のビルドが同じ preload.so を生成します。
実際にエクスプロイトに渡るのは P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET の差であり、その 2 つの項は同じ強さの根拠に基づいているわけではありません:
P0_KERNEL_PHYS_LOAD — 上記の mb_kernel.start として、eng イメージから測定。P0_PHYS_OFFSET — これは memstart_addr であり、カーネルはジェネレータが読み取る静的 FDT ではなく、DRAM 初期化後に preloader が報告する内容から lk が実行時に出力する /memory ノードからそれを取得するため、測定ではなく論証されました。論拠は、2 つのビルドが DRAM パス全体を共有していること(同じ dramc/emi/mblock/memory_layout ソース、eng 専用文字列の中にメモリレイアウトコードがないこと)、そして eng preloader の追加予約が 1 つだけ、mapping=1 を持つ 3 MiB の動的 security_fe_rsv であり、これが固定アドレスエントリを移動させることも DRAM の底部に穴を開けることもできないことでした。デバイスでの実行が決着をつけました。リニアマップエイリアスは eng で正しく配置されたため、この項は正しいのです。完全な導出過程は targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md にあります。
デバイスでの実行に関するメモ:
ro.boot.verifiedbootstate=green と flash.locked=1 は変更されませんでした。これは試されるまで未解決の疑問でした。これは efuse 化されたルートキーハッシュによって決定され、このビルドは retail と同じキーで署名されています。%s META DIS は retail 専用の文字列ですが、eng には retail にはない META 実装全体が含まれています(Enable fastmeta.、FAST META GPIO: %d、META_COM PORT: %d、read_meta_proinfo など)。ここでは Android に直接ブートしましたが、eng をフラッシュした端末が他の状態に陥った場合、それが原因である可能性が高く、target.h の値は関与していません。mb_kernel を移動した場合、2 つのターゲットは分岐します — それらを別々のディレクトリとして保持することで、その分岐が黙って見過ごされるのを防ぎます。このカーネルは KASAN_HW_TAGS で実行されます — /proc/cmdline に kasan.stack_ring_size=524288 が含まれます — そのため slab ポインタはビット 59:56 にアロケーションタグを持ちます。アップストリーム popsicle はタグなしポインタを前提としており、したがってこのデバイスをまったくルート化できません。[-] kernel page retry N/12 mode=1 で無限にループし、ログにはその理由を示すものは何もありません。
source/ 内の 2 行がこれを修正し、その両方が warhol-mte-fix.patch に含まれています:
タグを外すのはチェックだけです。ポインタ自体はタグ付きのままです。なぜなら、そのタグはポインタが指すメモリにとって正しいものであり、カーネルがそれらを参照解除するときにまさに必要なものだからです。
これについて arm64.memtag.bootctl を参照しないでください。そのプロパティはユーザースペースの MTE 制御であり、カーネルが自身のアロケーションにタグ付けしているかどうかについては何も示しません。
このリポジトリはポートです。エクスプロイトチェーンの功績はアップストリームに帰属します。
source/ exploit core (upstream popsicle + the kernel-MTE fix; see warhol-mte-fix.patch)
src/ main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
targets/ per-build generated target.h, one dir per fingerprint x preloader variant
warhol-OS3.0.304.0.WPSJPXM/ PRELOADER=retail
warhol-OS3.0.304.0.WPSJPXM-eng/ PRELOADER=eng
tools/
payload_dump.py extract partitions from an A/B payload.bin (verifies size+sha256)
kernel_banner.py read the Linux banner out of a boot.img
bootinfo.py dump boot / vendor_boot header fields
generate_target.py target.h generator; upstream's, MediaTek-only, plus kernel decompression
preloader_memlayout.py dump/diff a MediaTek preloader's static DRAM reservation table
lz4legacy.py LZ4 legacy-frame decompressor (kernel images)
out/ build artifacts (gitignored)
target.h が存在するまで何もビルドされません — Makefile は誤ったカーネル向けのバイナリを出力する代わりに明確に失敗します。
# 1. extract boot.img straight out of the OTA zip (payload.bin is stored uncompressed,
# so --base seeks into the zip; no need to unpack 7.6 GB first)
python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>
# 2. confirm the kernel banner — every offset downstream is pinned to it
python3 tools/kernel_banner.py <dir>/boot.img
python3 tools/bootinfo.py <dir>/boot.img
# 3. generate the target header. --preloader reads the kernel's physical load address
# out of the preloader's mb_kernel reservation; pass the preloader the phone runs.
python3 tools/generate_target.py \
--boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
-o targets/warhol-OS3.0.304.0.WPSJPXM/target.h
# 4. build
make preload # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail by default
手元に preloader イメージがない場合、--kernel-phys-delta 0 は古い形式で、このファームウェアでは同じヘッダが得られます — ただしこれは読み取りではなく主張であるため、--preloader を優先してください。両方を渡すと、ジェネレータはそれらを相互検証し、不一致があれば失敗します。
--base 5081 は、この特定の OTA における payload.bin のローカルヘッダオフセットです。他のパッケージでは META-INF/com/android/metadata の ota-property-files から読み取ってください。
Android NDK(NDK_ROOT / ANDROID_NDK_HOME)と llvm-objdump が必要です。
# <device> is the target name: <fingerprint> for PRELOADER=retail, <fingerprint>-eng for eng
adb push out/preload-<device>.so /data/local/tmp/preload.so
adb shell chmod 0644 /data/local/tmp/preload.so
adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
adb shell "/data/local/tmp/su -c id"
cred と SELinux の状態はメモリ内でパッチされるため、これは毎回の起動後に繰り返す必要があります。ブートローダーのアンロックは不要です。
自分が所有するハードウェアでの研究用です。これを実行するとブートループやデバイスのブリックが発生する可能性があり、保証は無効になります。いかなる種類の保証もありません。
アップストリームプロジェクトはそれぞれの作者によってライセンスされており、このポートはそれらの条件を引き継いでいます。
| 項目 | 値 | ソース |
|---|
| コードネーム | warhol | pre-device=warhol |
| ビルドフィンガープリント | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| インクリメンタル | OS3.0.304.0.WPSJPXM(JP グローバル) | post-build-incremental |
| Android | 16、SDK 36 | post-sdk-level=36 |
| セキュリティパッチ | 2026-05-01 | post-security-patch-level |
| OTA タイプ | A/B(payload.bin、CrAU、39 パーティション) | ota-type=AB |
| SoC | MediaTek MT6993(Dimensity 9500) | vendor_boot DTB 内の mediatek,mt6993-* 互換ノード; cmdline bootopt=64S3,32N2,64N2 |
| カーネル | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k、clang 19.0.1 | 抽出した boot.img から読み取ったバナー |
| boot イメージ | ヘッダ v4、カーネル 18,898,125 B、LZ4-legacy 圧縮、ramdisk なし | tools/bootinfo.py |
PRELOADER= | 端末上の Preloader | ターゲットディレクトリ | ステータス |
|---|
retail(デフォルト) | 出荷時の preloader_<device>.bin(fastboot ROM の preloader_raw.img とバイト単位で同一) | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ 2026-07-26 にデバイスで検証済み |
eng | エンジニアリングビルド、preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ 2026-07-26 にデバイスで検証済み |
| ファイル | 変更内容 | 理由 |
|---|
src/util.c | kernelsnitch_setup(..., mte_enabled=1) | 0 の場合、mm_struct 検索はタグなし候補のみを試し、タグ付きポインタには決して到達できないため — 運が悪いのではなく構造的なミス |
src/util.c | is_kernel_ptr() / is_direct_ptr() が範囲チェックの前にタグを外す | そうしないと、カーネルメモリから読み出したタグ付きポインタがリニアマップ外として拒否されるため(direct-entry-fatal reason=bad-task-or-cpu) |
src/kernelsnitch/kernelsnitch.h | タグスイープ < 15 → < 16 | タグ 0xf はタグなし/全一致ポインタであるため、スイープは MTE なしのカーネルもカバーする — mte_enabled=1 はどちらの場合でも安全 |
| リポジトリ | このリポジトリでの役割 |
|---|
| https://github.com/MobiusM/CVE-2026-43499 | 元の CVE-2026-43499 PoC / クラッシュトリガー |
| https://github.com/x-spy/CVE-2026-43499-popsicle | このポートのベース。 Xiaomi 17 Pro Max(popsicle)、カーネル 6.12.23-android16-5。source/ と tools/generate_target.py はここから取得 |
| https://github.com/Kananosa/CVE-2026-43499-For-Xiaomi-17T-chagall | 最も近い兄弟デバイス(Xiaomi 17T、chagall、同じく MediaTek)。popsicle 由来で、プリビルドされた preload.so を同梱 — そのコンパイル済み定数が物理ロードオフセットを裏付けました |
| https://github.com/MiCode/Xiaomi_Kernel_OpenSource | Xiaomi カーネルソース。構造体レイアウトの相互確認用 |