特殊Action遇上Account:CVE-2020-0338漏洞分析

前言

比较古老的问题,记录下之前分析的过程。

漏洞描述

AccountManager LaunchAnyWhere漏洞就不解释了,直接看checkKeyIntent

/**
 * 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 arbitrary intents on the device via by tricking to click authenticator
 * supplied entries in the system Settings app.
 */
 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);
    }
}

这里面删除了和URI grant相关的四个flag,但是阅读Intent的源代码发现,如果使用一些特殊的Action,又会在startActivity流程中把flag加回来。关键点在Instrumentation里面调用的migrateExtraStreamToClipData方法

/**
 * Execute a startActivity call made by the application.  The default 
 * implementation takes care of updating any active {@link ActivityMonitor}
 * objects and dispatches this call to the system activity manager; you can
 * override this to watch for the application to start an activity, and 
 * modify what happens when it does. 
 *
 * <p>This method returns an {@link ActivityResult} object, which you can 
 * use when intercepting application calls to avoid performing the start 
 * activity action but still return the result the application is 
 * expecting.  To do this, override this method to catch the call to start 
 * activity so that it returns a new ActivityResult containing the results 
 * you would like the application to see, and don't call up to the super 
 * class.  Note that an application is only expecting a result if 
 * <var>requestCode</var> is >= 0.
 *
 * <p>This method throws {@link android.content.ActivityNotFoundException}
 * if there was no Activity found to run the given Intent.
 *
 * @param who The Context from which the activity is being started.
 * @param contextThread The main thread of the Context from which the activity
 *                      is being started.
 * @param token Internal token identifying to the system who is starting 
 *              the activity; may be null.
 * @param target Which activity is performing the start (and thus receiving 
 *               any result); may be null if this call is not being made
 *               from an activity.
 * @param intent The actual Intent to start.
 * @param requestCode Identifier for this request's result; less than zero 
 *                    if the caller is not expecting a result.
 * @param options Addition options.
 *
 * @return To force the return of a particular result, return an 
 *         ActivityResult object containing the desired data; otherwise
 *         return null.  The default implementation always returns null.
 *
 * @throws android.content.ActivityNotFoundException
 *
 * @see Activity#startActivity(Intent)
 * @see Activity#startActivityForResult(Intent, int)
 * @see Activity#startActivityFromChild
 *
 * {@hide}
 */
@UnsupportedAppUsage
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                ActivityResult result = null;
                if (am.ignoreMatchingSpecificIntents()) {
                    result = am.onStartActivity(intent);
                }
                if (result != null) {
                    am.mHits++;
                    return result;
                } else if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityTaskManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

这里面调用了migrateExtraStreamToClipData方法,这个方法的处理就很有意思。

/**
 * Migrate any {@link #EXTRA_STREAM} in {@link #ACTION_SEND} and
 * {@link #ACTION_SEND_MULTIPLE} to {@link ClipData}. Also inspects nested
 * intents in {@link #ACTION_CHOOSER}.
 *
 * @return Whether any contents were migrated.
 * @hide
 */
public boolean migrateExtraStreamToClipData() {
    // Refuse to touch if extras already parcelled
    if (mExtras != null && mExtras.isParcelled()) return false;
    // Bail when someone already gave us ClipData
    if (getClipData() != null) return false;
    final String action = getAction();
    if (ACTION_CHOOSER.equals(action)) {
        // Inspect contained intents to see if we need to migrate extras. We
        // don't promote ClipData to the parent, since ChooserActivity will
        // already start the picked item as the caller, and we can't combine
        // the flags in a safe way.
        boolean migrated = false;
        try {
            final Intent intent = getParcelableExtra(EXTRA_INTENT);
            if (intent != null) {
                migrated |= intent.migrateExtraStreamToClipData();
            }
        } catch (ClassCastException e) {
        }
        try {
            final Parcelable[] intents = getParcelableArrayExtra(EXTRA_INITIAL_INTENTS);
            if (intents != null) {
                for (int i = 0; i < intents.length; i++) {
                    final Intent intent = (Intent) intents[i];
                    if (intent != null) {
                        migrated |= intent.migrateExtraStreamToClipData();
                    }
                }
            }
        } catch (ClassCastException e) {
        }
        return migrated;
    } else if (ACTION_SEND.equals(action)) {
        try {
            final Uri stream = getParcelableExtra(EXTRA_STREAM);
            final CharSequence text = getCharSequenceExtra(EXTRA_TEXT);
            final String htmlText = getStringExtra(EXTRA_HTML_TEXT);
            if (stream != null || text != null || htmlText != null) {
                final ClipData clipData = new ClipData(
                        null, new String[] { getType() },
                        new ClipData.Item(text, htmlText, null, stream));
                setClipData(clipData);
                addFlags(FLAG_GRANT_READ_URI_PERMISSION);
                return true;
            }
        } catch (ClassCastException e) {
        }
    } else if (ACTION_SEND_MULTIPLE.equals(action)) {
        try {
            final ArrayList<Uri> streams = getParcelableArrayListExtra(EXTRA_STREAM);
            final ArrayList<CharSequence> texts = getCharSequenceArrayListExtra(EXTRA_TEXT);
            final ArrayList<String> htmlTexts = getStringArrayListExtra(EXTRA_HTML_TEXT);
            int num = -1;
            if (streams != null) {
                num = streams.size();
            }
            if (texts != null) {
                if (num >= 0 && num != texts.size()) {
                    // Wha...!  F- you.
                    return false;
                }
                num = texts.size();
            }
            if (htmlTexts != null) {
                if (num >= 0 && num != htmlTexts.size()) {
                    // Wha...!  F- you.
                    return false;
                }
                num = htmlTexts.size();
            }
            if (num > 0) {
                final ClipData clipData = new ClipData(
                        null, new String[] { getType() },
                        makeClipItem(streams, texts, htmlTexts, 0));
                for (int i = 1; i < num; i++) {
                    clipData.addItem(makeClipItem(streams, texts, htmlTexts, i));
                }
                setClipData(clipData);
                addFlags(FLAG_GRANT_READ_URI_PERMISSION);
                return true;
            }
        } catch (ClassCastException e) {
        }
    } else if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
            || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
            || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
        final Uri output;
        try {
            output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
        } catch (ClassCastException e) {
            return false;
        }
        if (output != null) {
            setClipData(ClipData.newRawUri("", output));
            addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
            return true;
        }
    }
    return false;
}

这里面对于下列action,会从特定的extra中取出URI,并且放入CilpData字段,然后自动加上相关flag,具体逻辑可以看代码,这里面我们就使用MediaStore.ACTION_IMAGE_CAPTURE这个action,可以绕过AccountManager的补丁再次获取读写权限。

Intent intent = new Intent();
intent.setClassName("com.wrlus.poc.extragrant",
        "com.wrlus.poc.extragrant.ExpActivity");
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
        Uri.parse("content://com.huawei.searchservice.fileprovider/root/data/system/packages.xml"));

这里面我们并不需要加flag,因为即使加了也会被checkKeyIntent给删掉,我们就等着migrateExtraStreamToClipData方法自动给我们加就好了。配合一些URI allowlist的问题,即可在不使用Bundle mismatch的情况下绕过补丁成功利用漏洞。

修复

/**
 * 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 arbitrary intents on the device via by tricking to click authenticator
 * supplied entries in the system Settings app.
 */
 protected boolean checkKeyIntent(int authUid, Intent intent) {
    // Explicitly set an empty ClipData to ensure that we don't offer to
    // promote any Uris contained inside for granting purposes
    if (intent.getClipData() == null) {
        intent.setClipData(ClipData.newPlainText(null, null));
    }
    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);
    }
}

如果getClipData为null则手动设置一个ClipData,虽然也是没有东西但是会破坏migrateExtraStreamToClipDatagetClipData() != null的条件,就没有这个问题了。

2 条评论

  1. 小路老师你好,我是安卓框架层漏洞挖掘爱好者,有没有更多的类似本文的cve详细分析文章,网上这种资源好难找。现在好像遇到瓶颈了,挖掘思路枯竭。有的话可否麻烦你分享一下,万分感谢。

评论已关闭。