flawopen.com/Teardowns/cve-2024-32896-android-factory-reset-wipe-bypass

● CVE-2024-32896 · CVSS 7.8 · 高
セキュリティ研究 · FlawOpen

CVE-2024-32896:Android の初期化は鍵が破棄される前に中断できた

CVE-2024-32896(CVSS 7.8、限定的な標的型攻撃で悪用):初期化は recovery へ再起動するだけで、消去が実行されるまで暗号鍵はそのまま残っていました。そのため端末を手にした者は再起動を中断し、データを復元可能な状態に保てました。修正では再起動の前に Keystore の鍵をすべて削除します。

💡 わかりやすい解説 (ELI5)

ある会社では、紛失したノートPCには「シュレッダー」命令を送る決まりです。ノートPCはシュレッダー室へ歩いて行き、そこで全ファイルが処分されます。ところが、ノートPCを押さえている人が途中の廊下をふさげば、ファイルはどこにも届かず無傷のままです。修正策は、出発する前にノートPCが鍵付き書類棚のたった一つの鍵を燃やしてしまうことです。途中で止められても、もう誰もその棚を開けられません。

主要な概念と専門用語

初期化(--wipe_data)
ユーザー、デバイス管理アプリや MDM アプリ、リモートワイプサービスが要求する消去。RecoverySystemService.rebootRecoveryWithCommand() が recovery 向けに --wipe_data コマンドを書き込んで再起動します。
ブートローダー制御ブロック(BCB)
Android がブートローダーと recovery への指示を残す小さなパーティション(misc)。setupOrClearBcb() が書き込みます。
FBE と合成パスワード
ファイルベース暗号化。ユーザーデータの鍵は合成パスワードから導出され、その保護用データは KeyMint の鍵に結び付いています。その鍵を破棄すればデータは復号できません。
KeyMint / Keystore
ハードウェアに裏付けられた鍵サービス。AndroidKeyStoreMaintenance.deleteAllKeys() はすべての KeyMint デバイスに全鍵の削除を指示します。

根本原因の分析 (Root Cause)

RecoverySystemService.java の rebootRecoveryWithCommand() は、--wipe_data をブートローダー制御ブロックに書き込んで再起動するだけで、その後 recovery が /data を消去することに頼っていました。消去が終わるまで、合成パスワードを保護する KeyMint の鍵や、DE・メタデータ暗号化用の鍵は無傷でした。再起動を中断したり消去を実行させなかったりすれば、暗号化データも鍵もそのまま残ります。処理の順序に関する論理エラーです。修正(AOSP 8b7b2c66、バグ 324321147)は再起動前に deleteSecrets() を呼び、AndroidKeyStoreMaintenance.deleteAllKeys() を実行します。これは CVE-2024-29748 として追跡される Pixel ファームウェア修正の、Android フレームワーク側の半分です。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

消去が要求される

所有者、デバイス管理アプリや MDM アプリ、リモートワイプサービスが初期化を要求し、最終的に rebootRecoveryWithCommand("--wipe_data ...") が呼ばれます。

ステップ 2

何も破棄せず再起動

setupOrClearBcb() がコマンドを保存し、pm.reboot(REBOOT_RECOVERY) が端末を再起動します。この時点で暗号鍵には手が付けられていません。

ステップ 3

消去が中断される

端末を物理的に手にした者が、たとえば音量ダウンを押し続けてブートローダーに入るなどして recovery の手前で再起動を止め、消去は一度も実行されません。

ステップ 4

データが初期化を生き延びる

暗号化されたユーザーデータとそれを守る鍵が端末に残るため、消去済みになる代わりに、後からフォレンジックツールで攻撃できる状態のままになります。

ソースコード比較:脆弱 vs 堅牢化

脆弱な実装
// services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
@Override // Binder call
public void rebootRecoveryWithCommand(String command) {
    if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
    synchronized (sRequestLock) {
        if (!setupOrClearBcb(true, command)) {
            return;
        }

        // BUG: for "--wipe_data" nothing is destroyed yet. The keys that
        // protect the user's encrypted data survive until recovery runs the
        // wipe. If the reboot is interrupted or the wipe is skipped, the data
        // is still recoverable.
        PowerManager pm = mInjector.getPowerManager();
        pm.reboot(PowerManager.REBOOT_RECOVERY);
    }
}
堅牢化されたセキュアパッチ
// services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
static final String RECOVERY_WIPE_DATA_COMMAND = "--wipe_data";

@Override // Binder call
public void rebootRecoveryWithCommand(String command) {
    if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");

    boolean isForcedWipe = command != null && command.contains(RECOVERY_WIPE_DATA_COMMAND);
    synchronized (sRequestLock) {
        if (!setupOrClearBcb(true, command)) {
            return;
        }

        // FIX: destroy the keys first. Deleting every KeyMint key (including
        // the synthetic-password protector keys and the keys protecting DE and
        // metadata encryption keys) makes FBE data unrecoverable even if the
        // wipe in recovery is interrupted or skipped.
        if (isForcedWipe) {
            deleteSecrets();
        }

        PowerManager pm = mInjector.getPowerManager();
        pm.reboot(PowerManager.REBOOT_RECOVERY);
    }
}

private static void deleteSecrets() {
    Slogf.w(TAG, "deleteSecrets");
    try {
        AndroidKeyStoreMaintenance.deleteAllKeys();
    } catch (android.security.KeyStoreException e) {
        Log.wtf(TAG, "Failed to delete all keys from keystore.", e);
    }
}

エンジニアリング&システム堅牢化チェックリスト

参考資料