LaunchAnywhere 绕过原理
1 绕过前提
1.1 IPC基础概念介绍
1.1.1 Serializable接口
Serializable是Java提供的一个序列化接口,它是一个空接口,为对象标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,一句话即可。
1 | public class User implements Serializable { |
1.1.2 Parcelable接口
Parcel内部包装了可序列化的数据,可以在Binder中自由传输,在序列化过程中需要实现的功能有序列化、反序列化和内容描述序列化功能由
writeToParcel
方法来完成,最终是通过Parcel中的一系列write
方法来完成的。
1.1.3 如何实现Parcelable接口
describeContents方法:默认返回0即可
writeToParcel方法:该方法将类的数据写入外部提供的Parcel中,即打包需要传输的数据到Parcel容器中保存,一遍从Parcel容器获取数据,声明如下:
1
2
3
4
5
6
7
8/**
* 将对象转换成一个 Parcel 对象
* @param dest 表示要写入的 Parcel 对象
* @param flags 示这个对象将如何写入
*/
public void writeToParcel(Parcel dest, int flags) {
}静态的Parcelable.Creator接口,接口有两个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface Creator<T> {
public T createFromParcel(Parcel source);
public T[] newArray(int size);
}
public static final Parcelable.Creator<Course> CREATOR = new Parcelable.Creator<Course>() {
//反序列化的方法,将Parcel还原成Java对象
public Course createFromParcel(Parcel source) {
return new Course(source);
}
//提供给外部类反序列化这个数组使用。
public Course[] newArray(int size) {
return new Course[size];
}
};
1.2 一个使用Parcelable的例子
1.2.1 封装数据
把实现parcelable接口的Person对象传递到TwoActivity里
1 | public class DemoActivity extends Activity { |
1.2.2 TwoActivity 获取数据
获取从DemoActivity传递的数据,并打印
1 | public class TwoActivity extends Activity { |
1.2.3 parcelable接口的实现
1 | public class Person implements Parcelable{ |
1.3 Bundle
可序列化的Parcelable对象一般不单独进行序列化传输,需要通过Bundle对象携带。 Bundle的内部实现实际是Hashmap,以Key-Value键值对的形式存储数据。例如, Android中进程间通信频繁使用的Intent对象中可携带一个Bundle对象,利用
putExtra(key, value)
方法,可以往Intent的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括int、Boolean、String和Parcelable对象等等,Parcel类中维护着这些类型信息。
1
2
3
4
5
6
7
8
9
10
11 > // Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
> 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魔数
0x4C444E42
和键值对BaseBundle.writeToParcelInner
1
2
3
4
5
6
7
8
9
10
11
12 > int lengthPos = parcel.dataPosition();
> parcel.writeInt(-1); // dummy, will hold length
> parcel.writeInt(BUNDLE_MAGIC);
> int startPos = parcel.dataPosition();
> parcel.writeArrayMapInternal(map);
> int endPos = parcel.dataPosition();
> // Backpatch length
> parcel.setDataPosition(lengthPos);
> int length = endPos - startPos;
> parcel.writeInt(length);
> parcel.setDataPosition(endPos);
>
1.4 简单总结
基于上一篇文章前面我们所知道的基础知识,可以知道一下内容:
存在Android 系统漏洞
Android-bug-7699048
,它允许我们在App使用AccountManagerService
的时候,在AddAccount
方法中返回任意Bundle,而如果在Bundle中可以实现任意的intent,以此达到ActivityLunchAnywhere的目的Bundle在App之间的通信是使用
Parcelable
完成序列化和反序列化并进行通信的,如下图所示google修复漏洞的原理是 检查了
AddAccount
中返回的intent所指向的Activity和AppB是否是有相同签名的。 如果不同将会抛出异常SecurityException
在android-7.1.2_r39版本中,AccountManagerService对于
AddAccount
方法进行检查的源码如下services/core/java/com/android/server/accounts/AccountManagerService.java
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/**
* 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 aribtrary intents on the device via by tricking to
* click authenticator
* supplied entries in the system Settings app.
*/
protected void checkKeyIntent(int authUid, Intent intent) throws SecurityException {
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);
ActivityInfo targetActivityInfo = resolveInfo.activityInfo;
int targetUid = targetActivityInfo.applicationInfo.uid;
if (PackageManager.SIGNATURE_MATCH != pm.checkSignatures(authUid, targetUid)) {
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).";
throw new SecurityException(
String.format(tmpl, activityName, pkgName, mAccountType));
}
} finally {
Binder.restoreCallingIdentity(bid);
}
}在Bundle在App之间进行通过Bundle传递数据,在传递数据的时候会进行两次序列化和反反序列化的操作,而在第一次进行序列化之后,会对传输的Intent进行权限检查,检查的时间点就在第一次序列化并反序列化之后。
2 绕过
在和CVE-2017-13289
同一批的漏洞中,有着一系列相同的问题,那就是,序列化和反序列化不匹配,利用构造的Bundle能巧妙的绕过system_service中对于intent
的权限检查。
- 通过发送和接收Intent对象,所有Android程序之间以及与操作系统之间都进行交互
- 这些对象可以在Bundle对象中包含任意数量的键/值对。
- 传输Intent时,将Bundle对象转换(序列化)为包裹在Parcel中的字节数组
- 在从序列化Bundle中读取键和值后自动将其反序列化。
- 在Bundle中,键是字符串,值几乎可以是任何值。可以是原始类型,字符串或具有原始类型或字符串的容器,它也可以是一个Parcelable对象。
- 因此,Bundle中可以包含实现Parcelable接口的任何类型的对象。
- 我们需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。
Bundle在序列化之后的结构:
1 | bundle ---> { |
可以看到序列化之后的Bundle具有以下特点:
- 所有的键值对都是按顺序写入
- 在每个值之前都有指示值的类型
- 可变长度的数据大小在数据之前显示(字符串的长度,数组的字节数)
- 所有值都是4字节对齐的
2.1 trouble
所有键和值都按顺序写入Bundle,以便在访问序列化Bundle对象的任何键或值时,后者将完全反序列化,还初始化所有包含的Parcelable对象。
问题在于,实现Parcelable的某些系统类可能在
createFromParcel()
和writeToParcel()
方法中包含错误。在这些类中,在
createFromParcel()
中读取的字节数将与在writeToParcel()
中写入的字节数不同。如果将此类的对象放置在Bundle中,则重新序列化后,Bundle中的对象边界将更改。这为利用EvilParcel漏洞创造了条件。
eg:
1 | class Demo implements Parcelable { |
如果数据数组大小为0,那么在创造对象的时候,会在createFromParcel
中读取一个int(4个字节)
,同时在writeToParcel()
写入两个int(8字节)
,第一个int
通过显式调用writeInt()
写入,第二个int
在调用writeByteArray()
时写入(因为在写入数组的时候,数组长度始终在Parcel的数组之前)。
在这种情况下,尝试在Bundle中放入一个数组长度为0的Demo对象:
1 | Bundle bundle = new Bundle(); |
序列化之后:
反序列化:
1 | Parcel newParcel = Parcel.obtain(); |
在0x4C
的位置,有一个不能识别的数字0x31
我们看到在反序列化期间在createFromParcel方法中读取了一个int而不是两个int。因此,从捆绑中读取所有后续值不正确。读取0x48处的0x0值作为下一个键的长度。读取0x64处的0x1值作为键。读取0x54处的0x31值作为value类型。没有类型0x31的值,因此readFromParcel()精心报告了一个异常。
2.2 如何绕过LaunchAnywhere检查
再看一遍这个图,AppB将Bunde序列化之后的数据传递给system_server,然后在system_server中检查{KEY_INTENT: intent}
这个键值对。如果检查通过,那么就调用writeBundle进行第二次序列化,然后settings中反系列化后重新获得{KEY_INTENT: intent}
。
根据2.1所描述,如果第二次序列化和第一次反序列化过程不匹配,那么就有可能在system_server
检查时不出现{KEY_INTENT:intent}
键值对,而再第二次反序列化之后出现
3 一个案例
3.1 CVE-2017-13288
PeriodicAdvertisingReport .java
写为long
读为int
经历一次不匹配的序列化之后,会错位4个字节。构造如下:
重点:
序列化和反序列化之后的解析过程不一致
在解析txPower
的时候,序列化之后数值是Long
,而解析的时候读取的是int
,导致的后果就是,在txPower
之后的所有数据,反序列化之后都相隔4个字节。
poc如下:在 addAcount中构造不匹配的android.bluetooth.le.PeriodicAdvertisingReport
序列化对象
1 | Log.v(TAG, "addAccount"); |