LaunchAnyWhere 漏洞现世:Google Bug 7699048 复现与分析(Android4.3)

使用Android Studio和Android模拟器完整调试LaunchAnyWhere漏洞的整个过程,包括exp代码、Settings APP和system_server进程这三部分代码的调试与理解。

漏洞简介

launchAnyWhere: Activity组件权限绕过漏洞解析(Google Bug 7699048)

从效果上:恶意APP可向Settings系统APP发起intent调用,最终可以Settings APP的权限即system权限,发送任意intent,可进行直接拨打电话或者发送短信的恶意操作,或者启动各种未导出的Activity,进而绕过老密码,直接设置新密码。

image

从原理上:Settings系统APP使用的系统服务AccountManagerService(进程为system_server)的addAccount功能,其回传给Settings目标要启动的Activity(图中的step 4),可由恶意APP任意指定(图中的step 3),而AccountManagerService 未进行任何检查。

image

开发原理:

其他参考:

复现环境

使用android studio 自带的AVD,无论android版本选到4.1-4.3哪个版本均无法复现,在通过漏洞启动setting修改密码界面时会在logcat中发现如下错误,修了三个小时也没有修好(但其实我自己编译一个apk带有非导出activity是可以通过漏洞被启动的),因此我怀疑这个是模拟器模拟setting界面本身时出的问题:

Android Studio报错:E/EGL_emulation: tid 3197: eglSurfaceAttrib(1199): error 0x3009 (EGL_BAD_MATCH)_幻好的博客-CSDN博客

image

后根据https://github.com/EggUncle/LaunchAnyWhere/tree/master其中的截图,使用老版本genymotion 3.0.3 中下载的android 4.3镜像复现成功:

Genymotion 的旧版本 (Windows) Uptodown

image

通过点击button,即可自动跳转到修改pin码界面,推荐exp demo:

image

exp 相关

exp demo

exp 调用梳理

manifest中首先要注册一个service,其中的xml不能少:

image

xml中的accountType自定义设置好:

image

完成service对应的MyAccountService的类,在此类的onBind函数中返回另一个类MyAuthenticator的getIBinder,因为MyAuthenticator继承自AbstractAccountAuthenticator,所以其中就有getIBinder,不用管这个方法的实现。需要处理的是MyAuthenticator要处理的addAccount实现,这里要写最终要启动的目标intent,此函数返回的是一个Bundle,用攻击者视角这里就是在组织payload,所以这个payload不是主动发出去的,而是等着addAccount被调用时返回的。所以从通信的角度这可以看成:回包解析漏洞。

image

然后就是MainActivity后就调用AddAccountSettings触发自动的addAccount,因此主要代码就这两部分:

image

exp demo 的 bug

在网友EggUncle的exp demo中,无法通过点击触发第0步的com.android.settings.accounts.AddAccountSettings,原因是启动AddAccountSettings时,account_types参数传递错误:

image

经过排错分析,调用AddAccountSettings的目的是,让setting来访问本恶意app提供的账户服务,所以你传递的参数中必须得包含本恶意app信息,否则setting如何才能找回来,而account_types正是这么一个类似路由的参数。在本例中account_types需要与account_xml.xml中定义的android:accountType相同,而account_xml.xml在manifest.xml中注册,所以最终AddAccountSettings才能根据account_types找回来:

image

网友这么写错的原因是他照抄了retme7的原始exp,但又没抄全,retme7的account_types使用的Constants,并不是egguncle的使用的SyncStateContract.Constants(这应该是自动补齐的结果):

GitHub - retme7/launchAnyWhere_poc_by_retme_bug_7699048: source code & PoC file of launchAnyWhere problem

image

retme7的account_types使用的Constants是自己写的类,其中对account_types定义如下:

image

与其account_xml.xml中的一致:

image

exp 简化

因为漏洞本身的触发逻辑是一个service的回包,这导致exp代码要分散到多个类中,而不能完全只在MainActivity中单独完成。在本例中,主要的逻辑有两部分:

  1. MainActivity的触发逻辑(对应图中step0)
  2. MyAuthenticator 类中实现的addAccount的返回payload的组织逻辑(对应图中step3)

这两部逻辑在大部分的exp中完全没有任何代码的关系,MainActivity没有直接调用到addAccount,这也造成了我在理解这个攻击代码时的费解,我无法理解payload的去向。payload由addAccount组织,而addAccount就在那单摆浮搁,没有任何人调用,一度陷入迷茫。

理解漏洞后明白,MainActivity触发(step0)addAccount组织payload(step3) 之间的关联是由:外部Settings(系统app)AccountManagerService.java(运行在system_server进程中)来建立的,而不在本exp代码中。因此单独看exp不可能建立二者之间的关系:

image

所以我根据payload的组织与流向,改了一版对于攻击者来说易于理解的exp,主要是:

  1. 封装了两个函数:触发与组织payload
  2. 并允许触发并组织多次不同的payload
  3. 然后把payload放在了MainActivity中

这样就强行的将exp中的payload与主函数相关联,便可一目了然payload的组织与去向,虽然看起来payload仍然停在了addAccount函数中,但至少可以确定payload是从MainActivity送入addAccount函数中,那么接下来一定是从这个addAccount函数出去,时机应该为trigger之后:

image

调试方法

断app

对于这种android代码,很多逻辑都是框架来处理的,比如组织payload的addAccount,在我们的app没有直接调用这个函数,那这个函数是被谁调用的呢?可以使用android studio进行调试非常方便,可以将断点打在addAccount函数上,断下后观察调用栈:

不过看起来只能追到binder,还是没找到上家(即通过AddAccountSettings触发的setting)

image

断system_server

漏洞点位于AccountManagerService.java,源码也就是可以下载的sdk源码(目标模拟器环境为Android 4.3 API 18),其最终运行在system_server进程中:

https://android.googlesource.com/platform/frameworks/base/+/c6568719671206e726f260fad390680f7fb0ee9e/services/java/com/android/server/accounts/AccountManagerService.java

image

image

证明如下:

image

拽出来逆向:

image

因此希望断在漏洞点处:

https://android.googlesource.com/platform/frameworks/base/+/5bab9da^!/#F0

if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {

即直接调试system_server进程,方法如下:

  • 调试的目标进程system_server在AS中提示的进程名为system_process
  • 另外需要在android studio里手动打开漏洞的java源码,即API 18 的 SDK源码,然后打断点
  • 执行exp app,即可断下

D:\AS\Sdk\sources\android-18\com\android\server\accounts\AccountManagerService.java

image

断settings

调试settings程序有些麻烦,但却是最重要的,因为整个交互的调度的一大部分逻辑都在Settings中,最开始的触发是Settings干的,最后拉起目标Activity 还是 Settings干的,所以如果没有调试到Settings,那么对于漏洞的理解一定是空中楼阁。调试Settings程序需要源码和apk:

(1)settings的源码在AOSP中,如下,下载对应分支为android-4.3_r3的源码:

https://android.googlesource.com/platform/packages/apps/Settings/

➜  git clone https://android.googlesource.com/platform/packages/apps/Settings  --depth=1 --single-branch -b android-4.3_r3

(2)拿到settings程序的apk,apk路径可以通过pm path命令获得,位于:/system/app/Settings.apk

获取 Android 已安装应用的 .apk 安装包文件

root@vbox86p:/ # pm path com.android.settings
package:/system/app/Settings.apk

(3)调试方法如下,就是AS中导入apk并关联源码,调试目标选择和选择system_server过程类似:

调试预构建的 APK - Android Studio - Android Developers

image

执行exp,成功断到AddAccountSettingsonCreate中的startActivityForResult函数,但需要在f7单步一下,才能在变量窗口中看到此时的intent,即可跟踪到下一个函数中:

image

漏洞过程分析

目标就是把下图的调用过程,通过调试器,切切实实的看到一遍:

image

因为总体涉及到三部分的代码,所以可能需要开启多个窗口:

约定以下的标题格式大概为:进程名:JAVA类名:函数名:出口函数名,并且进程名有如下约定

  • exp的进程名实际为包名com.xuan.launchanywhere,简写为 exp
  • Settings的进程名实际为包名com.android.settings,简写为 Settings

整体调用过程大概如下:

image

(step 0) [exp]:MainActivity(Activity):onCreate:trigger:startActivity

首先是我们exp中的trigger函数发送的intent,调出到settings中的AddAccountSettings, 因为是intent我们自己构造的,所以调试窗口看到的变量信息没有什么特殊的:

如果关注后续的反序列化漏洞可以看到,此时的bundle还没有序列化(mParcelledData为空)

image

这个调用过程也正对应攻击流程图中的step 0:

image

所以解下来要断到Settings中,无需中断本次app的调试,直接在另一个调试settings的窗口打断,然后在本次app的调试窗口中继续执行,断点断下后,AS会自动切换到调试settings的窗口。

(………..) [Settings]:AddAccountSettings(Activity):onCreate:startActivityForResult

来到Settings的AddAccountSettings.java的onCreate函数,可以看到this.mintent就是启动此Activity所使用的intent,不过从此intent和this所有成员中均无法看出来是我的exp(com.xuan.launchanywhere)发起的此次调用,好像是在API22 (Android 5.1)才有this.getReferrer().getHost()方法获得调用者:

另外通过mIntent的mExtras也可以看出此时其中的bundle还未反序列化

image

在执行完142行的getStingArrayExtra后,bundle完成了反序列化,其实可以通过观察bundle类的源码得知,基本是对bundle进行任意的读取操作,都会触发整个bundle的反序列化,之后会详细分析:

core/java/android/os/Bundle.java - platform/frameworks/base - Git at Google

image

走到onCreate的最后,断到startActivityForResult时,调试窗口看不到intent变量,需要f7单步一下,才能正常显示。可见其使用startActivityForResult的启动了仍然在本包名下的ChooseAccountActivity,因此本次调用没有从Settings进程出去。并且原封不动的传递了我们exp中发送的account_type相关数据:

image

(………..) [Settings]:ChooseAccountActivity(Activity):onCreate:onAuthDescriptionsUpdated:finish

通过AddAccountSettings的startActivityForResult来到ChooseAccountActivity的onCreate函数,经过分析在ChooseAccountActivity中整个的调用流程为:

onCreate → updateAuthDescriptions → onAuthDescriptionsUpdated →  finishWithAccountType → finish 

最终通过finish函数,最后回到AddAccountSettings的onActivityResult,因此在整个漏洞利用的过程中,调用流经过ChooseAccountActivity的过程不太重要。

image

稍微值得注意的是,在ChooseAccountActivity中的onAuthDescriptionsUpdated 函数会调用AccountManager.*get*(this).getAuthenticatorTypes,控制流会从此进程直接出到AccountManagerSerivceAccountManager是服务接口java,即还在本进程中的代码)。调用出去的目的是查询我们传递的account_types是否又对应注册的认证服务,不过者对之后的回到AddAccountSettings没有什么太大的影响。总之控制流在ChooseAccountActivity逛了一圈 ,没干啥特别主要的事。

image

在最后的finishWithAccountType,对将要回到AddAccountSettings的onActivityResult设置的结果为:RESULT_OK,和将我们发送的account_type改了个名,但值没有变化传递回来了。

image

(step 1) [Settings]:AddAccountSettings(Activity):onActivityResult:addAccount:AccountManager.get(this).addAccount

之前从AddAccountSettings折腾到ChooseAccountActivity,又回到AddAccountSettings,其实还没有从settings进程出去,也其实还没到攻击流程图中的step1。现在回到AddAccountSettings的onActivityResult,这里终于要从Settings进程出去了:

  • 根据requestCode和resultCode的结果,其会进入addAccount函数
  • addAccount函数调用 AccountManager.get(this).addAccount,并传递mCallback函数指针
  • 因此mCallback应该会在 AccountManager.get(this).addAccount执行后回调
  • AccountManager 的完整类名为:android.accounts.AccountManager,并不在settings源码中,而在SDK源码中,源码AS可以直接下载
  • AccountManager.get(this).addAccount最终调用到AccountManager.java中的addAccount函数
  • AccountManager.java中的addAccount会调用mService.addAccount,这就从Settings进程出去了
  • 调用出去的服务就是运行在system_server进程中的AccountManagerService.java
  • 调用参数可以看到主要还是我们最开始传递的account_types

对这里调用的调用过程也可以看图中的调用栈进行观察:

http://androidxref.com/4.3_r2.1/xref/frameworks/base/core/java/android/accounts/AccountManager.java

image

调用过程对应图中的step 1,另外因为对android的service机制确实不熟,所以就没去在调试中证明此处出去的目标确实为AccountManagerService:

image

(step 2) [system_server]: AccountManagerService(IAccountManager.Stub):addAccount:new Session(){}:mAuthenticator.addAccount

调试system_server可以复用exp的窗口,也可以单独打开窗口加载对应的sdk源码进行调试

将断点断在AccountManagerService.java的addAccount函数开头处,如1456行,成功断下(1447行无法断下),在addAccount中经过一系列操作会走到1487行的new Session,这句写法有一些奇怪,仔细解释一下这种写法:匿名内部类(匿名类)

傻子都能看懂的匿名内部类

  • 写法的样子是:new的类后面还能加上大括号并且包裹代码
  • 写法的含义是:新建一个匿名类并实例化(Session是一个抽象类,正常使用需要被继承)
  • 写法的功能是:简化抽象类和接口的使用,可直接重写其方法,而不用新定义一个class
  • “内部”二字的解释:因为这个类的定义和实例化,只存在于当前函数的作用域中,除非此函数将其当成参数传递出去,否则别的函数无法直接使用这个类,因此这个类是“匿名”且“内部”的。

image

所以对于AccountManagerService.java中的new Session:

  • 新建一个匿名类并实例化(Session是一个抽象类,正常使用需要被继承)
  • 重写Session抽象类的run方法和toDebugString方法
  • 然后调用Session抽象类的bind方法
  • 所以这里并没有直接调用重写的run方法
  • 而是其实调用了两个函数:Session抽象类构造函数Session抽象类的bind函数

image

没有直接执行run函数,但也可以看到我们之前的断点也确实断在了run函数中,所以run也确实被执行了,通过调用栈可以看出,其父级函数onServiceConnected,而在AccountManagerService.java中并没有onServiceConnected函数的直接调用,因此run函数是回调回来的:

image

因此只能正向分析,即通过new Session调用的构造函数和bind,构造函数里没有什么特别的调用,但可以看到bind函数调用的bindToAuthenticator中有一些操作:

  • mAuthenticatorCache.getServiceInfo会通过我传递的account_types查出对应的包名和Service名
  • mContext.bindServiceAsUser将查出来的包名和Service名作为Intent的目标进行调用
  • 所以推测这里当成功与目标Service建立连接后,即可以回调执行onServiceConnected

image

回调执行onServiceConnected,调用run函数,通过run函数的mAuthenticator.addAccount,调用出到exp中的MyAuthenticator.addAccount:

image

此调用过程对应图中的step 2:

image

(step 3) [exp]: MyAuthenticator(AbstractAccountAuthenticator): addAccount:return

断点回到exp中的MyAuthenticator.addAccount,就是返回payload bundle:

image

此过程对应图中的step 3,这里虽然最简单,但其实是最重要通信过程,即发送payload的过程,也就是之前说的在通信的角度payload其实是回包:

image

(!bug) [system_server]:AccountManagerService(IAccountManager.Stub):Session(){}:onResult:response.onResult

回到AccountManagerService.java中Session抽象类的onResult,断点到此函数开头成功命中,参数就是从exp的addAccount返回的payload bundle。通过调试窗口可以观察返回变量名为result的payload bundle,可见此时bundle的mMap中还没有任何内容,而mParcelledData还是有值的,所以此时这个payload bundle还没有反序列化:

image

单步一下调试就会自动的跟入onResult第一句判断中调用的getString函数,这个是bundle类的函数接口,bundle类的get系列函数都是上来就会调用unparcel,对此bundle对象进行反序列化:

image

Android 反序列化漏洞攻防史话

image

执行完unparcel后,再观察调试窗口中解析的bundle对象,可见传递的payload intent已经成功解析出来,即已经执行了intent对象的反序列化函数。但这里其实有一个问题:我们序列化的任意类都可以被目标反序列化出来么?这个问题关乎对后续漏洞(拼多多所利用的CVE-2023-20963)理解。如果从原理回答这个问题,可以将其转化为另一个问题:传递过来的bundle里需要被反序列化的对象,其对应的反序列化函数位于对应的类中,那么unparcel是如何调用过去的呢?

image

我们可以重新断在system_server进程中AccountManagerService.java中Session抽象类的onResult函数开头,此时bundle还没有反序列化,然后将下一个断点打在intent的反序列化函数readFromParcel函数上,然后继续执行,断下后观察调用栈如下。可以看到unparcel确实能直接调用到intent的反序列化函数readFromParcel,所以推测序列化的bundle里应该包含了intent的完整类名,能调用过来的原理应该类似反射,通过类名,加载对应类的反序列化函数然后再进行调用:

image

可以在exp中,把发送的payload bundle的序列化后内容打出来,可见确实包含intent的完整类名 android.content.Intent

image

上面讨论反序列化主要是希望对后续的漏洞有更好的理解,现在回到本漏洞分析中,在Session类的onResult函数中,对于返回的result bundle反序列化后,没有对其中的intent对象进行任何检查,就继续调用了response.onResult,将整个bundle返回给了Settings,这就是bug所在:

image

这个response对象是创建new Session这个匿名类时的参数,类型是IAccountManagerResponse,再往上找是step1中Settings调用 AccountManager.get(this).addAccount,在AccountManager.java中传递过去的。所以此时Session类中onResult调用的response.onResult能找回到Settings:

image

此过程对应攻击流程图中如下部分:

image

(step 4) [Settings]:AddAccountSettings(Activity):mCallback:run:startActivityForResult

step1中Settings调用 AccountManager.get(this).addAccount时传递了一个回调函数mCallback,当AccountManagerService.java中Session抽象类onResult调用response.onResult时,返回到Settings中,即会触发mCallback函数的执行。断点断到这,即可看到这里会解析返回bundle中的intent

image

这个intent也没有任何检查,直接就作为startActivityForResult参数,即发起了最后的启动调用:

image

此过程对应图中的step 4:

image

bundle.get和bundle.getParcelable基本没有区别:

http://androidxref.com/4.3_r2.1/xref/frameworks/base/core/java/android/os/Bundle.java

image

补丁分析

补丁简介

https://android.googlesource.com/platform/frameworks/base/+/5bab9da^!/#F0

+   @Override
    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)) {

最重要的一句如下,是对authenticatorUidtargetUid的对应的程序是否具有相同签名的判定:

PackageManager.SIGNATURE_MATCH !=  pm.checkSignatures(authenticatorUid, targetUid))

补丁环境

模拟器环境使用Android Studio AVD提供的android 4.4(已经修复漏洞):

image

启动后执行exp,查看logcat打印,确实打印了补丁的检查失败的提示:

image

补丁调试

补丁在AccountManagerService.java中,所以仍然是调试system_server,断在Session抽象类的onResult的函数中,补丁中的authenticatorUid通过Binder.getCallingUid函数获得,即回传 payload bundle的exp程序对应的uid,结果为10052:

image

可以在adb中使用dumpsys package命令并提供exp的包名获得其对应的uid,与调试结果一致:

image

接下来,补丁会通过PackageManager的相关函数,从接收的bundle中解析出intent并确定intent中目标class对应的uid,即targetUid,结果为1000,这就是Settings进程对应的system权限:

image

最后通过pm.checkSignatures函数检查两个uid对应的签名,本次攻击中,这个判断结果为不必配,补丁会抛出一个异常,中止给Settings回传bundle,最后关于uid:

image image