Android LaunchAnyWhere (Google Bug 7699048)漏洞详解及防御措施

时间:2022-03-09 13:19:31

开始

近日,Google修复一个组件安全的漏洞LaunchAnyWhere(Google Bug 7699048)。这个漏洞属于Intend Based提取漏洞,攻击者利用这个漏洞,可以突破了应用间的权限隔离,达到调用任意私有Activity(exported为false)的目的。

该漏洞影响Android 2.3至4.3固件。

漏洞分析

在分析这个漏洞之前,需要先介绍两个东西。

Account管理机制

从Android2.0开始,系统引入了Account管理机制,详细使用说明见Android官方文档。Account管理机制提供了集中化管理帐户API以及安全存储用户口令和令牌的功能。系统中,可以同时存在多个帐户(设置——添加帐户可以查看),比如Google、Miscrosoft Exchange、微信、支付宝、陌陌等等

Android LaunchAnyWhere (Google Bug 7699048)漏洞详解及防御措施

Account机制涉及AuthenticationService和Client两个组成元素,它们之间的的通讯统一由AccountManagerService调度,AccountManagerService是Android上的一个系统服务。当Client首次使用时,会向AuthenticationService发起addAccount请求,示意图如下:Android LaunchAnyWhere (Google Bug 7699048)漏洞详解及防御措施

最后AuthenticationService会把指定的Intent返回给Client,Client则通过startActivityForResult对最终结果进行处理。这部分源码比较有趣,我们先看看AccountManager.addAccount的代码:

public AccountManagerFuture<Bundle> addAccount(final String accountType,
final String authTokenType, final String[] requiredFeatures,
final Bundle addAccountOptions,
final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
if (accountType == null) throw new IllegalArgumentException("accountType is null");
final Bundle optionsIn = new Bundle();
if (addAccountOptions != null) {
optionsIn.putAll(addAccountOptions);
}
optionsIn.putString(KEY_ANDROID_PACKAGE_NAME, mContext.getPackageName());
return new AmsTask(activity, handler, callback) {
public void doWork() throws RemoteException {
mService.addAccount(mResponse, accountType, authTokenType,
requiredFeatures, activity != null, optionsIn);
}
}.start();
}


最后代码是通过一个AmsTask调用addAccount的,而AmsTask本身是一个异步任务,mRespone是一个Binder对象,当AuthenticationService指定Intent后,就是把intent保存到这个respone对象里,最后看看这个Respone的源码:

/** Handles the responses from the AccountManager */private class Response extends IAccountManagerResponse.Stub {    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) {                // this will only happen if the system process is dead, which means                // we will be dying ourselves            }        } else {            set(bundle);        }    }    public void onError(int code, String message) {        if (code == ERROR_CODE_CANCELED || code == ERROR_CODE_USER_RESTRICTED) {            // the authenticator indicated that this request was canceled, do so now            cancel(true /* mayInterruptIfRunning */);            return;        }        setException(convertErrorToException(code, message));    }}


从代码可见,Respone直接帮我们startActivity了。

关于System用户

在Android中,可以说System用户拥有相当高的权限,通过阅读源码可以发,所有permissoin的地方都是直接放行System用户的,见代码ActivityManagerService.checkComponentPermission

    /**     * This can be called with or without the global lock held.     */    int checkComponentPermission(String permission, int pid, int uid,            int owningUid, boolean exported) {        // We might be performing an operation on behalf of an indirect binder        // invocation, e.g. via {@link #openContentUri}.  Check and adjust the        // client identity accordingly before proceeding.        Identity tlsIdentity = sCallerIdentity.get();        if (tlsIdentity != null) {            Slog.d(TAG, "checkComponentPermission() adjusting {pid,uid} to {"                    + tlsIdentity.pid + "," + tlsIdentity.uid + "}");            uid = tlsIdentity.uid;            pid = tlsIdentity.pid;        }        if (pid == MY_PID) {            return PackageManager.PERMISSION_GRANTED;        }        return ActivityManager.checkComponentPermission(permission, uid,                owningUid, exported);    }


可见system用户可以完全无视权限检查,不管组件是否为exported,最后都直接返回PERMISSION_GRANTED

漏洞分析

通过上面对Account机制和System用户的分析,接下来进入主题吧。

在常规情况下,当某个Client发起addAccount请求时,AuthenticationService一般会返回一个指向同包下的Activity(系统提供了基类AccountAuthenticatorActivity),这个Activity主要负责完成帐户和密码的输入交互。

然而,理论上这个AuthenticationService是可以随意指定Intent的,比如指定其他包下的Activity。由于最终startActivity的调用发生在Client进程,因此AuthenticationService是可以调起所有Client能够打开的Activity,当然包含Client自身的exported为false的Activity了。

但Client往往只是一般用户,这种用途价值并不高。幸好系统的Setting帐户相关的功能,提供了一些可以利用的逻辑。漏洞利用原理:想办法令Setting调用addAccount方法,EvilAuthenService就可以指定任意的Intent,由于startActivity的调用发生在Setting,因此就具备了System用户的所有权限了。

通过下面代码, 我们可以直接让Setting发起一个AddAccount请求:

Intent intent = new Intent();intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.accounts.AddAccountSettings"));intent.setAction(Intent.ACTION_RUN);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);String authTypes[] = {Constants.ACCOUNT_TYPE};intent.putExtra("account_types", authTypes);//authType需要指定AuthenticatorActivity.this.startActivity(intent);


利用示意图如下:

Android LaunchAnyWhere (Google Bug 7699048)漏洞详解及防御措施

漏洞利用

LaunchAnyWhere漏洞,如果结合其他的漏洞,效果会非常惊艳。

结合Fragment注入漏洞,直接修改屏幕密码

Fragment注入漏洞详解见我之前的博客《Android框架攻击之Fragment注入》。这个漏洞目前大部分厂商已经修复了,但如果再配合LaunchAnyWhere漏洞,那么Fragment注入漏洞修复就形同虚设了。这里把Fragment注入漏洞利用的关键代码贴出来:

Intent intent = new Intent();  intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);  intent.setClassName("com.android.settings", "com.android.settings.Settings");  intent.putExtra(":android:show_fragment","com.android.settings.ChooseLockPassword$ChooseLockPasswordFragment");  intent.putExtra("confirm_credentials", false);  startActivity(intent); 


Android LaunchAnyWhere (Google Bug 7699048)漏洞详解及防御措施

漏洞利用POC, 见https://github.com/retme7/launchAnyWhere_poc_by_retme_bug_7699048

结合FakeID漏洞,实现InjectAnyWhere

FakeID漏洞详解见我之前的博客《Android FakeID(Google Bug 13678484) 漏洞详解》

FakeID漏洞利用的关键让目标进程运行Webview控件,加载Flash控件达到程序注入的目的。通过LaunchAnyWhere漏洞,我们可以直接打开目标进程的Webview。

FakeID和LaunchAnyWhere完美结合的做法是在一个APK中同时集成两个漏洞利用,大致过程如下:

  1. EvilApp首先令Settin*生一AddAccount请求,并把accountType指定EvilApp本身处理;
  2. EvilApp创建Intent,并把Intent指向攻击应用Webview的Activity(如微信的com.tencent.mm.plugin.webview.ui.tools.ContactQZoneWebView),url指向某个有flash元素的网页;
  3. 由于FakeID,WebView则会加载EvilApp中so,注入完毕;
  4. 在so中再直接加载EvilApp.apk文件,实现Java注入,这部分更详细的内容,见我之前的博客《攻击的Android注入术》

漏洞修复

这个漏洞在4.4上已经修复,看看修复的代码,可以找到防御的思路:

public void onResult(Bundle result) {             mNumResults++;-            if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {+            Intent intent = null;+            if (result != null+                    && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {+                /*+                 * 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");+                }+            }+            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)) {@@ -2223,6 +2276,7 @@             super(looper);         }


由于 Resopne是一个Binder对象,因此当onResult被回调时,可以通过Binder.getCallingUid()获取authenticatorUid,如果targetUid跟authenticatorUid不相同,则直接对AuthenticationService抛异常。

防御措施

应用自身的防御措施是保证expotred为false的Activity不被意外调起。我们通过添加一些检查点,比如B是一个expotred为fase的Activity,按正常逻辑,Activity的堆栈顺序是:

  • A1->B->C1
  • A2->B->C2

那么我们就可以在A1和A2中设置一检查点,当进入B时,发现检查点没有设置,则认为B被非法调起。沿着这个思路,如果我们把Activity的调用堆栈都记录下来,那么当进入B前,当前栈必然只能是A1或者A2,否则则认为B被非法调起。

Activity堆栈管理的一个实现,可以参考这里http://www.apkbus.com/blog-117210-44386.html