CVE-2020-0396 Telephony组件信息泄露漏洞分析

一、漏洞背景

  • 在AOSP的2020-09补丁中,披露了一个Telephony eSIM模块的漏洞,编号为CVE-2020-0396并且为Critical级别。成功利用漏洞可造成eSIM的信息泄露。

二、漏洞细节

  • 漏洞位于com.android.internal.telephony.euicc.EuiccController中,在构造一个PendingIntent时没有指定接收者,导致Intent变成了一个隐式Intent,任何注册了对应intent-filter的Activity都会进入选择列表。
// frameworks/opt/telephony/src/java/com/android/internal/telephony/euicc/EuiccController.java
/** Add a resolution intent to the given extras intent. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public void addResolutionIntent(Intent extrasIntent, String resolutionAction,
        String callingPackage, int resolvableErrors, boolean confirmationCodeRetried,
        EuiccOperation op, int cardId) {
    Intent intent = new Intent(EuiccManager.ACTION_RESOLVE_ERROR);
    // Missing receiver of PendingIntent
    intent.putExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_ACTION,
            resolutionAction);
    intent.putExtra(EuiccService.EXTRA_RESOLUTION_CALLING_PACKAGE, callingPackage);
    intent.putExtra(EuiccService.EXTRA_RESOLVABLE_ERRORS, resolvableErrors);
    intent.putExtra(EuiccService.EXTRA_RESOLUTION_CARD_ID, cardId);
    intent.putExtra(EuiccService.EXTRA_RESOLUTION_CONFIRMATION_CODE_RETRIED,
            confirmationCodeRetried);
    intent.putExtra(EXTRA_OPERATION, op);
    PendingIntent resolutionIntent = PendingIntent.getActivity(
            mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_ONE_SHOT);
    extrasIntent.putExtra(
            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT, resolutionIntent);
}
  • 向上梳理该函数的调用者,有好几个调用者,这里以onGetDefaultListComplete为例
// frameworks/opt/telephony/src/java/com/android/internal/telephony/euicc/EuiccController.java
@Override
public void onGetDefaultListComplete(int cardId,
        GetDefaultDownloadableSubscriptionListResult result) {
    Intent extrasIntent = new Intent();
    final int resultCode;
    switch (result.getResult()) {
        case EuiccService.RESULT_OK:
            resultCode = OK;
            List<DownloadableSubscription> list = result.getDownloadableSubscriptions();
            if (list != null && list.size() > 0) {
                extrasIntent.putExtra(
                        EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTIONS,
                        list.toArray(new DownloadableSubscription[list.size()]));
            }
            break;
        case EuiccService.RESULT_MUST_DEACTIVATE_SIM:
            resultCode = RESOLVABLE_ERROR;
            addResolutionIntent(extrasIntent,
                    EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM,
                    mCallingPackage,
                    0 /* resolvableErrors */,
                    false /* confirmationCodeRetried */,
                    EuiccOperation.forGetDefaultListDeactivateSim(
                            mCallingToken, mCallingPackage),
                    cardId);
            break;
        default:
            resultCode = ERROR;
            addExtrasToResultIntent(extrasIntent, result.getResult());
            break;
    }

    sendResult(mCallbackIntent, resultCode, extrasIntent);
}
  • sendResult方法中可以看到是将extrasIntent添加到了callbackIntent中(Intent.fillIn),并且发送了callbackIntent这个PendingIntent
// frameworks/opt/telephony/src/java/com/android/internal/telephony/euicc/EuiccController.java
/** Dispatch the given callback intent with the given result code and data. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public void sendResult(PendingIntent callbackIntent, int resultCode, Intent extrasIntent) {
    try {
        callbackIntent.send(mContext, resultCode, extrasIntent);
    } catch (PendingIntent.CanceledException e) {
        // Caller canceled the callback; do nothing.
    }
}
  • 通过上述代码可以梳理出来这个callbackIntent的结构,首先callbackIntent自身有Action等字段,这里以[Original PendingIntent Part]来表示,然后extrasIntent是通过Intent.fillIn方法添加进去的,这里因为没有重复的字段,所以就是简单的组合,而extrasIntent中包含了一个字段名为EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT的PendingIntent,也就是addResolutionIntent方法中的resolutionIntent,这个PendingIntent携带的Intent就是造成漏洞的那个Intent,Action为ACTION_RESOLVE_ERROR,extras域有六个字段,包含eSIM的Card ID、Confirmation Code,这些应该是敏感信息。
callbackIntent
(PendingIntent)
    |
    [Original PendingIntent Part]
    extrasIntent(Intent.fillIn)
    (Intent)
        Extras:
        EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT(resolutionIntent)
        (PendingIntent)
            |
            Action: ACTION_RESOLVE_ERROR
            Extras:
                EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_ACTION
                EXTRA_RESOLUTION_CALLING_PACKAGE
                EXTRA_RESOLVABLE_ERRORS
                EXTRA_RESOLUTION_CARD_ID
                EXTRA_RESOLUTION_CONFIRMATION_CODE_RETRIED
                EXTRA_OPERATION
  • 那么这个复杂的PendingIntent在send之后传递给谁了呢,查找EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT字段之后我们发现,是发送给了android.telephony.euicc.EuiccManager这边的startResolutionActivity方法,最终以startIntentSenderForResult的形式发送出去了
// frameworks/base/telephony/java/android/telephony/euicc/EuiccManager.java
public void startResolutionActivity(Activity activity, int requestCode, Intent resultIntent, PendingIntent callbackIntent) throws IntentSender.SendIntentException {
    PendingIntent resolutionIntent =
        resultIntent.getParcelableExtra(EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT);
    if (resolutionIntent == null) {
        throw new IllegalArgumentException("Invalid result intent");
    }
    Intent fillInIntent = new Intent();
    fillInIntent.putExtra(
        EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_CALLBACK_INTENT,
            callbackIntent);
    activity.startIntentSenderForResult(resolutionIntent.getIntentSender(), requestCode, fillInIntent, 0 /* flagsMask */, 0 /* flagsValues */, 0 /* extraFlags */);
}
  • 这个startIntentSenderForResult是什么接口呢?查看定义发现,如果是在Activity对象上调用的,其效果和startActivityForResult相似,只是参数变成了PendingIntent的内部实现IntentSender。
Like startActivityForResult(android.content.Intent, int), but allowing you to use a IntentSender to describe the activity to be started. If the IntentSender is for an activity, that activity will be started as if you had called the regular startActivityForResult(android.content.Intent, int) here; otherwise, its associated action will be executed (such as sending a broadcast) as if you had called IntentSender#sendIntent on it.
  • 所以最终eSIM的Card ID、Confirmation Code这些敏感信息,会以隐式Intent的形式被发送出去,这样一来,只要恶意应用编写一个Action为ACTION_RESOLVE_ERROR的intent-filter,就有机会拿到这些数据。

三、漏洞验证

  • 漏洞需要eSIM,现在Pixel 3、Pixel 4系列是支持eSIM的,然而国内并没有,所以没办法复现。

四、漏洞影响

  • 造成Card ID、Confirmation Code信息泄露,这个Card ID不知道是什么概念,如果类比实体SIM卡的话,应该类似于IMSI,也就是SIM卡的唯一ID,该ID可用于追踪用户,这可能是Google将漏洞定为Critical的原因。

五、漏洞补丁

  • com.android.internal.telephony.euicc.EuiccControlleraddResolutionIntent方法显式添加了接收者,使Intent成为显式Intent。
intent.setPackage(RESOLUTION_ACTIVITY_PACKAGE_NAME);
intent.setComponent(new ComponentName(RESOLUTION_ACTIVITY_PACKAGE_NAME, RESOLUTION_ACTIVITY_CLASS_NAME));
  • 这里可以看到实际的接收者应是com.android.phone.euicc.EuiccResolutionUiDispatcherActivity
/** Restrictions limiting access to the PendingIntent */
private static final String RESOLUTION_ACTIVITY_PACKAGE_NAME = "com.android.phone";
private static final String RESOLUTION_ACTIVITY_CLASS_NAME =
    "com.android.phone.euicc.EuiccResolutionUiDispatcherActivity";
  • 另外Google还给一大堆Telephony模块中的其他PendingIntent增加了PendingIntent.FLAG_IMMUTABLE标志,修改位置达到7处之多,该标志的作用是让PendingIntent在send时候添加的额外Intent失效,也就是不允许二次向原本的Intent中添加内容(不允许Intent.fillIn),这几个修改看起来和EuiccController并不直接相关,可能是为了消减一类问题,防止PendingIntent被二次编辑。另外给BluetoothPairingDialog加了一个PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS标志,防止蓝牙配对对话框被第三方应用使用顶部显示特权遮挡。