
Xiaomi Pad 6 (pipa) における CVE-2023-6931 のカーネル LPE エクスプロイト。perf の read_size オーバーフローを連鎖させて root を取得し、データオンリーの netlink_sock sk_destruct 破壊により SELinux を無効化する。
最終的にデバイス上で使用したバージョンの exploit を、このディレクトリに単独で整理した。
デバイス Xiaomi Pad 6 / pipa / SM8250
システム HyperOS OS2.0.20.0.UMZCNXM
カーネル 4.19.157-perf-g240e1d0a5f52
起点 adb shell(shell ドメイン)
結果 一度きりの uid0 + グローバル SELinux permissive(再起動で無効)
exploit.c のファイルヘッダを参照。要約は以下の通り。
1) perf group read_size overflow -> 範囲外 u64 インクリメントプリミティブ
2) victim netlink_sock->sk_destruct をインクリメント -> netlink_sock_destruct_work(+0x28e8)
3) close(victim) -> sk_destruct(work) -> container_of(work, netlink_sock, 0x440)
-> fake = victim-0x440 -> sk_free(fake) -> __sk_destruct(&fake->sk_rcu)
-> fake->sk_destruct @ fake+704 = pivot(mov sp, x19)
4) スタックを fake 内の skb ペイロードへ移行(pt_regs1 @ +960、pt_regs2 @ +1120)
5) restore_tramp -> eret EL1h -> commit_creds(&init_cred)
6) restore_no_kpti -> eret EL0 -> post_root
SELinux の無効化は純粋なデータ書き換えで行う:fake+0x248 が selinux_state を指し、__sk_destruct の atomic_dec がそのワードの最下位ビットセグメント内の enforcing を 1 から 0 に減算する。
./build.sh # 本ディレクトリに exploit を出力
CC=aarch64-linux-gnu-gcc ./build.sh /tmp/exploit
駆動スクリプト .project/scripts/drive_selinux_off.sh が使用するコマンドライン:
adb push exploit_selinux8 /data/local/tmp/exploit_selinux8
adb shell 'chmod 755 /data/local/tmp/exploit_selinux8'
adb shell 'cd /data/local/tmp && setsid nohup /data/local/tmp/exploit_selinux8 \
--selinux-off --layout-interleave --tsec-sid 1 --alarm-us 5000000 \
> /data/local/tmp/run.out 2>&1 < /dev/null &'
引数の意味:
--selinux-off SELinux を無効化(純粋なデータ書き換えパス)、同時に cred 偽造を伴う
--layout-interleave 割り当てシーケンスを交互配置し、OOB インクリメントが対象オブジェクトの sk_destruct に着地するようにする
--tsec-sid 1 偽造 cred のセキュリティコンテキスト sid/osid(実際のドメイン sid が取得できない場合は 1 を使用)
--alarm-us 5000000 自己覚醒 ITIMER_REAL、スタックした試行は 5 秒後に終了
実行の前提と制限:
shell ドメインは perf_event と netlink_route に到達可能だが、アプリドメインは不可。そのため adb shell から起動する必要がある;perf_event_mlock_kb = 516 を消費し尽くす。同一 boot 内で繰り返し実行しないこと。前回のインスタンスが終了しないとイベントが解放されず、再実行では 4000 から 6000 個しか作成できない;kill -9 で古いインスタンスを消してからやり直す。そうでなければ 1 回の起動につき 1 回の有効な試行とみなす;pause() で停止する。root と permissive は今回の起動に対してのみ有効。--edl、--edl2)もあり、root ウィンドウから確定的に EDL に入るために使用する。本ディレクトリには最終的に使用したバージョンのみ収録。このディレクトリには第 1 段階のみを置く。チェーン全体は実際には 2 段階であり、接続点は第 1 段階が残す root コマンドチャネルである。
第 1 段階 exploit_selinux8 カーネル権限昇格 + SELinux 無効化、その後 root コマンドチャネルで停止
成果物:uid0 + permissive、チャネル = FIFO /data/local/tmp/root_in|root_out
(他に抽象 unix socket rootshell / rootshell2、および --root-client PORT モード)
第 2 段階 stage2-frida/ 上記チャネルを通じて root で frida-server を起動し、Settings に注入
成果物:アプリプロセス内で構築され並行送出されるバインド要求(TEE 署名)
第 1 段階の終了時に ROOTSHELL_OK を /data/local/tmp/root_proof.txt に書き込み、コマンド実行可能の証憑とする。以降、ホスト側のあらゆる root 操作は FIFO に 1 行のコマンドを書き込むことで行う。例えば第 2 段階で frida-server を起動する 1 行:
adb shell "echo 'killall -9 frida-server 2>/dev/null; rm -f /data/local/tmp/frida.log; \
setsid runcon u:r:shell:s0 /data/local/tmp/frida-server16 -D \
</dev/null >/data/local/tmp/frida.log 2>&1 & echo FRIDA_STARTED' > /data/local/tmp/root_in"
runcon u:r:shell:s0 は省略できない:SELinux を無効化した後、frida-server が kernel ドメインで動作すると Android Context が取得できず、PackageManager が NPE を起こす。
外部依存(本ディレクトリには同梱されていない):frida-server 16.7.19(54 MB、.project/tools/ext/frida/fs16 に配置、sha256 プレフィックス 4eebf1fbc66ff54aba9a9124)と対応する python クライアント(.project/tools/ext/fridavenv16、frida 16.7.19)。16.x を 17.x ではなく使用するのは、17 の agent には Java bridge がないためである。
完全な呼び出しチェーンは .project/scripts/drive_selinux_off.sh が編成する:root ウィンドウに命中した後の最初の動作が bash .project/scripts/exp_forge_send.sh であり、code 0 を取得すればループを停止する。
exploit_selinux8 は納品された原件であり、チェックサムは SHA256SUMS を参照。本ディレクトリのソースから再ビルドすると別のバイナリが得られる:
本ディレクトリのソースから再ビルド 1,853,744 B f8bb900bf4e197190d56bfc1437bcc9a025e49640a7fa2a78ff0f7fb0a6e7939
元のソースから再ビルド 1,853,752 B 96c7c3637682a2d5869ab4b6a732688a252f17f5e604b1695bc8fcc022113b3f
実際に動作したもの 1,853,720 B 279837d2ee028fbbb520120180156446ad8dc3286b723e28de300b544be2a21a
差異はシンボルごとに特定済みで、原因はただ 1 箇所:コマンドライン引数テーブルに 2 つのモードが追加されていることである。
--edl フレーム 2 で __qcom_scm_set_dload_mode(dev, 1) を呼び出す
--edl2 フレーム 2 で dload cookie を直接書き込む
つまり、納品されたものは EDL モードを追加する前にビルドされたバージョンであり、現在のソースは追加後のものである(.project/work/exploit_edl1、exploit_edl2 はこれからビルドされる)。EDL 分岐は --edl または --edl2 を渡した場合にのみ有効であり、edl_mode はデフォルトで 0、payload 構築で唯一関連する行は edl_mode ? 1ULL : rt(OFF_INIT_CRED) である。したがって最終的な --selinux-off コマンドラインを使用する場合、両者の動作は一致する。差異は機能追加に属し、修正や動作変更ではない。バイナリは原件のまま添付し、ソースは完全版を保持する。
.project/tools/strip_dev_comments.py で処理し、124 個の開発過程コメントブロック(v96、v127 などのバージョン反復マーカー、および「旧値/実測/再実行/スキップ」といった中国語のデバッグ口調)を削除し、313 箇所の構造的コメントを保持した。前処理結果の比較では空行のみが異なり、コードトークンは完全に一致する。実行ログには依然として vNNN: プレフィックス付きの出力テキストが含まれるが、これらはプログラム出力でありコメントではないためである。
idk fr what files are really needed, if you want to you might very need a LLM to understand the whole exp and write yours.
| ファイル | 説明 |
|---|
exploit.c | ソースコード。開発過程のコメントは削除済み。ファイルヘッダのチェーン説明、オフセットテーブル、構造体レイアウトなどの構造的コメントは保持 |
text_syms.h | デバイスの kallsyms ダンプから生成した .text シンボルテーブル。コンパイルに必須 |
ro_syms.h | 同上、読み取り専用データシンボルテーブル。コンパイルに必須 |
exploit_selinux8 | 実際にデバイス上で動作したバイナリ。駆動スクリプトが使用したものと同一 |
build.sh | ビルドスクリプト |
SHA256SUMS | 上記ファイルのチェックサム |
| ファイル | 説明 |
|---|
exp_forge_send.sh | frida-server 16.7.19 をプッシュして起動、Settings が動作中であることを確認、pid を取得、agent を実行 |
frida_run.py | frida クライアントラッパー。FRIDA_PID 直接接続と FRIDA_SPAWN 自動起動(am/pm を回避)をサポート |
forge_send.js | Settings プロセス内で getNonce を複製し、旧フィールド集合で applyBind に署名して送出 |
| 比較項目 | 結果 |
|---|
| 関数サイズが異なるシンボル | main のみ(shipped 17,744 B、再ビルド 18,004 B、差 260 B ≈ 65 命令) |
| 残り 1,503 個の関数 | サイズ完全一致、増減なし |
| 再ビルドにのみ現れるシンボル | edl_mode の 1 つ |
| コマンドライン引数テーブル | shipped 85 個、再ビルド 87 個、増えたのはまさに --edl と --edl2 |
.text 全体の命令数 | 126,533 と 126,597、差 64 命令、main の差と符合 |