VirtualApp 原理速览 - 总结篇
TokameinE 发表于 浙江 移动安全 857浏览 · 2025-05-05 12:40

前言的前言

本篇作为总集篇将对本系列全文进行总结,以下是本系列的速览规划:

1VirtualApp 原理速览 - 框架体系概述

2Activity 启动流程 - 我想我们需要先清楚正常的应用是如何启动的

3容器内 APP 启动流程 - 四大组件 Activity 是如何实现的

4容器内 Service 启动流程 - 四大组件 Service 是如何实现的

5Broadcast Receiver 容器内实现 - 四大组件 Broadcast Receiver 是如何实现的

6Content Provider 容器内实现 - 四大组件 Content Provider 是如何实现的

7路径重定向和 Xposed 注入时机分析 - 容器内应用的数据访问如何正常进行以及 Xposed 是如何被注入的

您可在如下仓库找到本系列文章的单篇内容:

VirtualAppQuickReview: https://github.com/ErodedElk/VirtualAppQuickReview

或在我的个人博客也将会以单篇的形式发布本系列文稿:

https://tokameine.top/

如果您在阅读过程中有任何疑问,或发现文稿中存在任何纰漏,欢迎通过邮件或其他方式与我联系,笔者相信技术正是要在相互的交流中才能够进一步向前的。您可通过 tokameine@gmail.com 与我联系。

框架体系概述

前言

从很早以前就一直很好奇 VirtualApp 的相关技术,但是一直抽不出时间。正巧最近想试着自己照猫画虎开发一个类似的容器化应用,并做一些定制化的需求,因此抽空把整个项目过了一遍,也正好帮我整理一遍过去一直对整个 Android 系统较为模糊的认知。

速览规划

以下是本系列的速览规划:

1VirtualApp 原理速览 - 框架体系概述

2Activity 启动流程 - 我想我们需要先清楚正常的应用是如何启动的

3容器内 APP 启动流程 - 四大组件 Activity 是如何实现的

4容器内 Service 启动流程 - 四大组件 Service 是如何实现的

5Broadcast Receiver 容器内实现 - 四大组件 Broadcast Receiver 是如何实现的

6Content Provider 容器内实现 - 四大组件 Content Provider 是如何实现的

7路径重定向和 Xposed 注入时机分析 - 容器内应用的数据访问如何正常进行以及 Xposed 是如何被注入的

技术框架

vafram.png


VA 框架对应用的操作涉及三个层面:

Java

Framework

Native 技术只在应用层进行,因此无需 root 。一言蔽之,欺骗系统让系统以为应用已安装,同时也欺骗应用,让应用以为自己被安装。

层次
主要工作
VA Space
由VA提供了一个内部的空间,用于安装要在其内部运行的APP,这个空间是系统隔离的。
VA Framework
这一层主要给Android Framework和VAPP做代理,这也是VA的核心。VA提供了一套自己的VA Framework,处于Android Framework与VA APP之间 1. 对于VAPP,其访问的所有系统Service均已被 VA Framework 代理,它会修改VAPP的请求参数,将其中与VAPP安装信息相关的全部参数修改为宿主的参数之后发送给Android Framework(有部分请求会发送给自己的VA Server直接处理而不再发送给Android系统)。这样Android Framework收到VAPP请求后检查参数就会认为没有问题。 2. 待Android系统对该请求处理完成返回结果时,VA Framework同样也会拦截住该返回结果,此时再将原来修改过的参数全部还原为VAPP请求时发送的。 这样VAPP与Android系统的交互也就能跑通了。
VA Native
在这一层主要为了完成2个工作,IO重定向和VA APP与Android系统交互的请求修改 1. IO重定向是因为可能有部分APP会通过写死的绝对路径访问,但是如果APP没有安装到系统,这个路径是不存在的,通过IO重定向,则将其转向VA内部安装的路径。 2. 另外有部分jni函数在VA Framework中无法hook的,所以需要在native层来做hook。

代码框架

考虑到现有开源的 VirtualApp 只支持老版本 Android,因此选择原理相同的 blackbox 进行参考 仓库地址:https://github.com/Monster-GM/sandbox

项目代码分为两个模块:

app模块:用户操作与UI模块

Bcore模块:此模块为秘盒空间的核心模块,负责完成整个应用的调度

src/main:VirtualApp框架代码

pine:Hook框架

其他

本系列文章会主要关心其中的 VirtualApp 框架实现部分。

Activity 原生启动流程

Activity 原生启动流程

系统界面 Launcher

为了解 VA 是如何启动 Activity 的,我们需要先知道 Android 是如何启动 Activity 的。在 Android 系统启动以后,系统已经启动了 Zygote,ServiceManager,SystemServer 等系统进程。

ServiceManager 进程中完成了 Binder 初始化;SystemServer 进程中 ActivityManagerService,WindowManagerServicePackageManagerService 等系统服务在 ServiceManager 中已经注册;最后启动了 Launcher 桌面应用。



Launcher 作为用户的交互界面,在用户点击 APP 图标的时候提供了打开应用的能力。不同的手机厂商可能会根据 Launcher 做一些定制,比如 miui 就是如此,但最终的原理是一致的。

应用安装的时候,通过 PackageManagerService 解析 apk 的 AndroidManifest.xml 文件,提取出这个 apk 的信息写入到 packages.xml 文件中,这些信息包括:权限、应用包名、icon、apk 的安装位置、版本、userID 等等。packages.xml 文件位于系统目录下/data/system/packages.xml。

启动应用流程

system_process.png


当用户点击桌面上的应用图标后,Launcher 会通过 service_manager 向 AMS 服务发出请求,查询对应的 APP 信息,然后由 Zygote 创建目标 APP 进程。

launcherapp.png


先梳理一下大致的流程:

1Launcher 通过 Binder 方式沟通 AMS

2AMS 先检查这个 APP 进程是不是已经创建了

3 如果已经创建,则直接调用 realStartActivityLocked 直接到第 7 步

4 否则,AMS 接到请求后让 Zygote 通过 fork 创建 APP 进程,完成 Application.onCreate 、创建应用的上下文和其他各种必要对象,这些对象会在 AMS 中留有备份进行保留。

5 新创建的 APP 进程通过 Binder 发送 ATTACH_APPLICATION_TRANSACTION 通知 AMS

6 AMS 接到 ATTACH_APPLICATION_TRANSACTION 后调用 realStartActivityLocked

7 设置进程为顶部 Activity,为新进程创建事务发送调度命令 H.EXECUTE_TRANSACTION

8进程处理命令消息时调用 Activity.onCreate 并且初始化应用自己的视图

具体到代码实现中,分为几个步骤,首先启动步骤从 Launcher 开始:

1检查将要打开的目标 APP 的 Activity 是否存在,如果存在就不需要打开了

Launcher.startActivitySafel -> Launcher.startActivity

1打开目标 Activity

Activity.startActivity

1通过 ATSM 服务调用该服务提供的 startActivity

Activity.startActivityForResult - Instrumentation.execStartActivity - ActivityTaskManager.getService().startActivity

ActivityManagerNative.getDefault() 会返回一个 ActivityManagerProxy 作为 Launcher 中使用 ActivityTaskManager 的代理,该代理的 startActivity 会发送 START_ACTIVITY_TRANSACTION 来通知 ActivityTaskManager

完成上述过程后,进程从 Launcher 切换到 system_server 中的 ActivityManagerService,也就是 AMS。

1在 startActivityAsUser 中会先获取用户的 UserID 作为参数然后往下调用 getActivityStartController 中的 starter

startActivity - startActivityAsUser

1创建新的 intent 对象,获取 ApplicationPackageManager

ActivityStackSupervisor.startActivityMayWait - resolveActivity

1获取 intent 所指向的 Activity 信息,并保存到 Intent 对象。

PackageManagerService.resolveIntent() - queryIntentActivities()

1 获取到调用者的进程信息,通过 Intent.FLAG_ACTIVITY_FORWARD_RESULT 判断是否需要进行 startActivityForResult 处理。检查调用者是否有权限来调用指定的 Activity

2 Activity 有多种启动模式,对 launchMode 的处理,创建 Task 等操作。启动 Activity 所在进程,已存在则直接 onResume(),不存在则创建 Activity 并处理是否触发 onNewIntent()

ActivityStackSupervisor.startActivityUncheckedLocked - startActivityLocked

1 若找到 resume 状态的 Activity,执行 startPausingLocked() 暂停该 Activity,同时暂停所有处于后台栈的 Activity,这里一般来说会把桌面,也就是 Launcher 暂停掉。

ActivityStack.resumeTopActivityInnerLocked

1 获取要启动的Activity进程信息,若成功,则表示进程已经启动了,通过 realStartActivityLocked 启动这个 activity;否则,通过 AMS 代理调用 startProcessAsync 去创建进程。前者的条件就是前面所述的目标 APP 已经启动过的情况,后者则是从头开始创建这个 APP 进程。

ActivityStackSupervisor.startSpecificActivity

我们考虑后者的情况,程序将会往下调用 startProcessAsync 创建新进程:

startProcessAsync 会通过消息的方式让 ATMS 服务在处理该消息时创建对应的进程,调用目标为 ActivityManagerInternal::startProcess

ActivityManagerInternal::startProcess 调用ActivityManagerService::startProcessLocked 调用 ProcessList::startProcessLocked 调用 ProcessList::startProcess

如果目标进程是 top app,设置 flag 保证启动的最高优先级,并最终在 startProcess 中创建对应的目标进程,也就是 APP 的进程。

在进程创建成功后,将当前进程切换到新进程,并将 ActivityThread 类加载到新进程,调用 ActivityThread.main

1 ActivityThread.main :创建主线程的 Looper 对象,创建 ActivityThread 对象,ActivityThread.attach() 建立 Binder 通道,开启 Looper.loop() 消息循环

2 ActivityThread.attach:创建 ActivityManagerProxy 对象,调用基于 IActivityManager 接口的 Binder 通道 ActivityManagerProxy.attachApplication()

Looper 会持续从消息队列中获取消息,然后处理指定的任务。其中,attach 函数调用时会发送 ATTACH_APPLICATION_TRANSACTION 通知 system_server 中的服务。

此时,应用的 ActivityThreadApplicationThread 已经被创建,并创建了消息循环机制。当调用 ActivityThread.attach 时,内部会调用 ActivityManagerProxy.attachApplication ,通过 Binder 来调用 AMS 中的 attachApplication 函数,此时会把 ApplicationThread 传递过去。

attachApplication - attachApplicationLocked 主要有两个关键函数需要关注:

bindApplication

ActivityTaskManagerService.LocalService#attachApplication

我们先关注 thread.bindApplication ,thread 就是刚刚由新进程传过来的。

函数先调用 bindApplication 向进程发送 H.BIND_APPLICATION 命令,进程收到该命令后,通过 handleBindApplication 处理:

handleBindApplication 初始化 context,然后初始化 Instrumentation 对象,创建 Application 对象,并调用该对象的 onCreate

初始化流程调用链为 makeApplication - newApplication :

然后是 makeApplicationInner 的细节

对于新创建的这个进程而言,当 callApplicationOnCreate 完成调用以后,这个进程的上下文,以及 Application 对象和 Instrumentation 对象都完成的创建和初始化。而在进程这波完成上述的初始化过程中,AMS 那边也没闲着,在发送完相应的命令以后, ActivityManagerService#attachApplicationLocked 继续往下调用 ActivityTaskManagerService.LocalService#attachApplication

可以注意到,最终这个函数将往下执行 ActivityTaskSupervisor#realStartActivityLocked 完成最后的步骤。而如果此前不需要创建新进程,那么刚打开 APP 的时候就会从这个地方开始恢复进程的状态了。

函数首先创建 Activity 事务,设置对应的 callback ,以及对应的生命周期 ActivityLifecycleItem,最终开始调度事务 lientLifecycleManager#scheduleTransaction

可以看到,最终由 AMS 向进程发出 H.EXECUTE_TRANSACTION 命令,这个命令同样会被进程那边接受并处理:

这个函数最终会往下调用 ClientTransactionHandler#handleLaunchActivity,最为抽象类的方法,实际调用 ActivityThread#handleLaunchActivity

handleLaunchActivity 最终回调目标 ActivityonConfigurationChanged,初始化 WindowManagerService,调用 ActivityThread.performLaunchActivity

callActivityOnCreate 中会回调 Activity.performCreate ,其中调用 ActivityonCreateActivity.setContentViewActivityThread.performResumeActivityperformResumeActivity 最终会回调 onResume

总之,到这里之后,新应用的进程算是创建完成了。

结余

弯弯绕绕一大圈,有不少的同名函数,在整理这些资料的时候也是被绕晕了好几次了,希望最终写出来的流程没有太混乱吧。如果有哪里写的不对,还请师傅们多多指教。

参考文章

https://blog.csdn.net/hgy413/article/details/100071667 https://blog.csdn.net/hgy413/article/details/95465321 https://zhuanlan.zhihu.com/p/151010577 https://juejin.cn/post/7028124957141893150 https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode/blob/master/article/android/framework/Android-Activity%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B.md https://blog.csdn.net/g984160547/article/details/120676574 https://blog.csdn.net/qq_14876133/article/details/141362098 VirtualApp拆解之二:Activity启动流程 - 简书

容器内 APP 启动流程

前言

在了解了正常环境下 app 的 activity 是如何被启动的以后,接下來我们希望能够了解一下 VirtualApp 中是如何启动目标 app 的。不过整个流程涉及到了对部分 Service 的 Hook,但这些内容却不是本节我们重点关心的内容,因此会有相应的介绍,但或许并不全面。

容器内 APP 启动流程

点击启动应用时发生了啥

当用户点击了视图上目标应用的图标后,触发点击事件并向下调用,在 blackbox 中将来到 launchApk 函数:

mClientConfiguration.isEnableLauncherActivity 是恒真的,因此最终会调用 LauncherActivity.launch ,在该函数中,blackbox 初始化了一个 Intent,然后调用原生的 startActivity 函数来进入 LauncherActivity,在进入时,会调用该对象的 onCreate 函数:

函数首先获取之前那个 splash ,然后从中读取出需要启动的目标应用的包名和用户 ID,并通过包名来读取包的相关信息,最后调用 BActivityManager.startActivity 来在 Blackbox 中启动应用。

getService 函数会返回对应的 Service ,在这个函数中将会返回 BActivityManagerService,这里涉及到了一个我们尚且没有关注过的问题,VirtualApp 是如何伪造各种系统 Service 的?

那些系统服务如何被 Hook

在应用的 Manifest 里声明了这么一段:

在启动 Blackbox 的时候,handleBindApplication 中会主动调用对应 ContentProvider 下的 onCreate 函数:

我们重点关注的是 mServices 这个成员,在注意到它将 BActivityManagerService 放入了数组,并调用对应的 systemReady

最终会为每个包注册一个 BroadcastReceiver

而这个 SystemCallProvider 本身也作为一个 IBinder,将它管理的这些 Service 暴露给其他应用使用:

ServiceManager.getService 可以能够根据参数来返回对应的 Service:

如果 ServiceManager 没初始化的话就先创建并初始化,将所有的 Service 都放入 mCaches,并在需要的时候返回该对象。最终其他需要使用这些服务的应用就都能够通过 Binder 拿到这些对应的对象了。

对这些获取 Service 的对象来说,他们本该获取到原生的 ActivityManagerService ,却被 BActivityManagerService 替换掉了,对应的去调用那些本该调用的方法时,自然这些方法也就一起被 Hook 掉了。

一般来说我们都是通过 getSystemService 来获取对应的服务的:

而在 Blackbox 的 HookManager 中注册了对各种对象的钩子:

我们主要看 IActivityManagerProxy 是如何对 ActivityManager 进行 hook 的:

这里有一个 _set_mInstance 实际上是 blackreflection 的语法糖,它通过反射的方式来修改 gDefault().mInstance 。我们在上一节中提到过启动应用时会通过 ActivityManagerNative.getDefault 来得到 ActivityManagerProxy ,这里会将结果给改成 Proxy,也就是用 IActivityManagerProxy 来代理原本的返回对象。

比如说 getServices 函数会被 hook 为:

可以注意到,在注入 Service Hook 的时候是有做进程判断的,因为主进程肯定还是需要和 Service 进行正常沟通的,如果全都 Hook 掉的话,主进程也无法正常通信了。所以在满足isBlackProcessisServerProcess 时才会注入那些代理,也就是那些需要启动的内部应用或是服务进程才会注入。

顺带一提,ServerProcess 中包含了这么几个:

BActivityManagerService.startActivity 如何启动应用

接下来我们回到 BActivityManagerService.startActivity 来看看它如何启动应用。

这里向下继续调用 startActivityLocked,不过这个函数有点长,这里主要关注两个几个关键步骤即可:

首先我们关注 startActivityInNewTaskLocked ,对于那些需要新启动的情况,使用该函数创建对应的任务:

该函数创建了一个 shadow ,它实际上是用来创建一个虚假的 Intent 的,我们往下跟踪 startActivityProcess

targetApp 初始化了我们将要启动的目标应用的相关信息:

可以看到主要就是一些 ID 的初始化,不过注意,其中 bpid 指的其实是对 Blackbox 来说的进程 ID ,因为对系统来说只有 Blackbox 这一个进程,但是对 Blackbox 来说却需要管理其中启动的不同应用。

其中还包括了一个 initAppProcessL 用来初始化 app:

这是一个通过 Binder 来调用 initprocess 函数的接口函数,对应调用为:

向下调用 initProcess 函数:

这里将 appConfig 设置到了 BActivityThread 对象里去。

然后再调用 getStartStubActivityIntentInner ,不过参数其实只有刚才的 bpid,对应参数中的 vpid

这个 vpid 参数会用来查找 Blackbox 提前在 Manifest 中占坑的 Activity :

这样的 Activity 总共有 50 个,相当于 Blackbox 最多能支持同时启动 50 个内部应用。

这个操作相当于构造了一个用于启动 ProxyActivityIntent,最终再将这个对象传给系统 AMS 来启动它:

AMS 收到这个请求后自然是正常启动这个 Activity 了,因为所有行为都合法。但是当 AMS 完成了相关启动后,在前文我们提到过,会给这个新的 Activity 发一个 H.EXECUTE_TRANSACTION 命令,而这个命令会被 handleLaunchActivity 处理,但是这个函数其实在之前是被 Hook 掉了的:

这个 HCallbackProxy 中是这样注入的:

最终是把 mCallback 对象用 HCallbackProxy 给替换掉了,从而把下面的消息处理函数 handleMessage 给换掉了。不过如果不是我们需要处理的消息,会重新调用原本的函数来处理。

最终调用自己实现的 handleLaunchActivity 函数:

看起来似乎有些复杂,这里稍微总结一下。

首先 handleLaunchActivity 这个函数会有多次调用,不只是收到 LAUNCH_ACTIVITY 时,还有 EXECUTE_TRANSACTION 的时候也一样会调用(似乎是兼容版本),因此看着流程里会有多次提前返回,是因为时机还没到。

以及我们知道,一个 APP 在启动时有可能会创建多个 Activity,第一个创建的 Activity 需要额外的调用 bindApplication 去绑定 Application 对象,这个也是我们前文正常流程里提到过的。

函数一样很长,总结一下内容:

1 获取 APK 信息 packageInfo

2 修改 LoadedApk 中的 mSecurityViolationmApplicationInfo 为目标应用

3 设置进程名和命令行中的参数名为目标函数 VirtualRuntime.setupRuntime

4 设置 TargetSdkVersion

5 初始化 Blackbox 自己的 sdk 动态库 NativeCore.init

6 路径重定向 IOCore.get().enableRedirect

7 调用 makeApplication 以构建子程序包的 Application 对象,并且替换原来通过 Host Stub 生成的 mInitialApplication。注意,这个时候新生成的 LoadedApk 代表了目标应用,其中的很多资源路径全都被替换为目标应用的路径了,加载资源时将会从被替换后的路径去查找。

8注册 Providers

9 通过 callApplicationOnCreate 调用 Application 下的 OnCreate ,这会创建或初始化对应的上下文、InstrumentationApplication,目标应用生命周期开始

10 初始化 seccomp,这是 Blackbox 后续提供的新功能,Virtualbox 是没有这个的 NativeCore.init_seccomp

handleBindApplication 完成后我们回到 handleLaunchActivity 继续往下:

这里有一个 onActivityCreated

将 Activity 指定。

最后将 mIntentmInfo 替换成目标应用。

这个函数从这一步结束后会返回一个 false,之前一直没注意到,但实际上当其返回 false 的时候,会回到原函数:

handleLaunchActivity 返回 false 后,程序继续往下执行 mOtherCallback.handleMessage ,这个 mOtherCallback 就是原本的那个处理对象,通过它来调用原本的那个 handleLaunchActivity

在原先的那个处理函数中:

注意这里的 ActivityClientRecord 被传进了 performLaunchActivity

. 由于我们预先已经把相关的资源路径全部替换成目标应用了,这里会创建目标应用的内存实例对象,获取的 classloader 也都是指向目标应用的路径,使用它们创建 Activity 并最终调用 callActivityOnCreate。以及目标的相关 dex 和动态库也都在这里被加载进内存。

另外,AppInstrumentation 把这个函数做了个 Hook:

mBaseInstrumentation.callActivityOnCreate 会调用原生的 callActivityOnCreate ,这个里面会去调用 ActivityOnCreate

流程图

最后贴一份流程图,来自于 alen17

startActivityFromLauch.png


参考文章

https://blog.csdn.net/ganyao939543405/article/details/76177392 https://zhuanlan.zhihu.com/p/151010577 https://www.cnblogs.com/revercc/p/16813435.html https://www.jianshu.com/p/f95fd575a57c

容器内 Service 启动流程

前言

在前文中我们分析了 VirtualApp 如何在容器内启动目标应用,但一个完整的 APP 除了它本身的代码外,它还有可能创建自己的服务。于是我想要试图了解它是否有在这个过程中做些什么。

与 Activity 不同的是,Service 的生命周期是代码手动管理的,光是这样就能省略掉很多复杂的流程。不过同 Activity 一样,想要创建 Service 仍然需要在 Manifest 中提前占好坑位,通过一些 Hook 等方式实现替换。

容器内 Service 启动流程

原生 Service 的创建流程

先贴一张大致的流程图吧,这里直接引用了 gityuan 大佬的博客内容,原文可在参考文章里找到。

ServiceCreate.png


1Process A 进程采用 Binder IPC 向 system_server 进程发起 startService 请求;

2system_server 进程接收到请求后,向 zygote 进程发送创建进程的请求;

3zygote 进程 fork 出新的子进程 Remote Service 进程;

4Remote Service 进程,通过 Binder IPC 向 sytem_server 进程发起 attachApplication 请求;

5system_server 进程在收到请求后,进行一系列准备工作后,再通过 binder IPC 向 remote Service 进程发送 scheduleCreateService 请求;

6Remote Service 进程的binder线程在收到请求后,通过 handler 向主线程发送 CREATE_SERVICE 消息;

7主线程在收到 Message 后,通过发射机制创建目标 Service,并回调 Service.onCreate() 方法。

嗯,相比 Activity 要简单多了。一言蔽之就是,进程调用 startService 让 AMS 给这个服务创建个新进程,然后再去调用这个服务中的初始化函数。

VirtualApp 环境中的兼容 startService

网上查了一些 VirtualApp 关于 Service 的实现,但当我在 Blackbox 中对照时发现,似乎后者的实现不太一样,这里我只写出我自己的理解。

startService 函数开始:

函数本身没做什么事,主要还是向下调用 BActivityManagerService.startService

为用户创建 userSpace ,然后再往下调用 ActiveServices.startService

先通过 resolveService 来获取到需要启动的 Service 的相关信息,接下来调用 createStubServiceIntent 来伪造 Intent

这里会将 Intent 替换成提前在 Manifest 中占坑好的 ProxyService$Pn,最后调用原生的 startService 去启动这个 ProxyService

于是乎流程就回到了正常的启动过程中去,但注意,这样一来,启动的不就是 ProxyService 了吗?

在上面的流程中可以知道,当 AMS 完成创建的相关步骤后会主动给主线程发 CREATE_SERVICE 的消息,而在被 Hook 了的主线程中会调用 handleCreateService

如果创建的目标不是 ProxyJobServiceProxyService 的话就会往下调用,但这里我们创建的目标就是 ProxyService,因此这里返回 false 后会调用原生的那个处理函数真正创建这个 Service 。

而在正常的创建流程中,最后会向下调用这个 ServiceOnCreateonStartCommand,其中后者这个函数是在服务每次启动时都会调用的,因此通过 Hook 它来实现替换服务:

在下面这个 onStartCommand 里去加载真正的目标服务相关的代码和对象:

重点关注调用的 createService 函数:

可以看到,程序在这里将服务的代码加载进内存并构建为 Service 对象。

最后再调用它本身的 onStartCommand 函数收个尾:

VirtualApp 环境中的兼容 bindService

启动服务还有另外一种通过 bindService 实现的方法,不过同前面的方法并没有太大变化。

函数执行过程中主要关注下面这个函数:

startProcessLocked 函数相信已经不陌生了,就是之前 Activity 时用到的那个,然后最后仍然是执行 createStubServiceIntent 来创建一个用来启动 ProxyServiceIntent

然后最后调用的是原生的那个 bindService:

然后在创建的服务完成以后会主动调用 onBind

可以看到也是一样在这个时候去加载真正的目标代码并启动该服务的。

总结

这套流程跟网上搜到的很多资料有些不同,笔者查到的很多资料都表示直接调用 scheduleCreateService 去创建对应服务,而不再经过原生的 AMS 。但在阅读 Blackbox 的代码后我们可以发现实际情况并非如此,启动 Service 跟启动 Activity 有很多地方很相似,它们同样需要提前占坑,并以类似伪造 Intent 的方式去启动对应的目标,然后通过 Hook 的方式来替换真正的目标。

参考文章

https://gityuan.com/2016/03/06/start-service/ https://blog.csdn.net/qq_26460841/article/details/118441738

Broadcast Receiver 容器内实现

前言

Broadcast Receiver 的容器内实现跟 Service 和 Activity 一样,都是需要提前在 Manifest 中声明才能够调用的,因此对于 VirtualApp 这种容器,就需要提供一种能够在容器内实现的操作。

对于写在 Manifest 中注册的 Broadcast Receiver 在应用启动时会静态注册,而这条途径现在不行了,因此我们需要考虑在容器内应用启动时动态注册它的那些 Broadcast Receiver。

Broadcast Receiver 容器内实现

Receiver 实现

首先我们来看其创建的时机:

VirtualApp 在创建 BActivityManagerService 时会附带着把 BroadcastManager 一起创建:

然后在 BActivityManagerService 调用 systemReady 的时候会触发 BroadcastManager.startup

这里在遍历整个 VirtualApp 已经安装过的所有应用,然后对每个包都调用一次 registerPackage

首先获取包里所有需要注册的 receivers,然后往下遍历去判断 action 是否为 BlackAction:

这里的 SYSTEM_ACTIONS 包括了这些:

如果属于上述的这些 action,那就需要额外加一层代理把它们替换掉:

最后再调用 registerReceiver 动态注册这些:

最后调用 addReceiver 把这个包的那些 receiver 放入 Hash 表里做个映射就算是注册完成了:

看起来好像一下子就结束了?总结一下流程就是:把容器内的那些包的每个 Receiver 都拿出来,然后把它们的 IntentFilter 用 VitualApp 自己注册一个一样的,这样就能让 VitualApp 来代管这些收到的 Message 了。

那么问题来了,VitualApp 代管了这些 Message 谁来处理呢?注意到当时注册的时候用到了一个 ProxyBroadcastReceiver 对象。

此处用到的 ProxyBroadcastReceiver 重载了 onReceive

可以看出,当 VirtualApp 收到 Message 时会往下调用 scheduleBroadcastReceiver

功能也很明显,根据传来的 Intent 去找到对应应用的 ProcessRecord 对象,并通过 scheduleReceiver 来向它们传递 Intent

这里就很明显了,通过上述的信息来找到目标应用中的处理函数,通过 loadClass 来加载对应的代码,最后调用它的 onReceive 来让真正的处理函数处理这个消息。

代码过程中一直有个 pendingResultData 在通过 sendBroadcast 发送:

主要是提供一个超时兜底,当广播超时时候会通知一个 PendingResult 表示结束,告诉发送方广播结束了。

Sender 的实现

除了接受部分需要这样适配,由容器内应用发送广播的过程同样也需要做些调整。

首先是通过 sendBroadcast 把真正需要发送的 Intent 包装起来:

将内部应用发送的 Intent 伪造成由 VirtualApp 发送的 proxyIntent ,然后再调用原生的发送函数把这个广播重新播出去。

不过如果这个发出去的广播最终还是由容器内的应用去处理,那之前注册好的那些 Receiver 就会接收到这些消息然后处理了。后续流程就跟前文一致了。

参考文章

https://blog.csdn.net/ganyao939543405/article/details/76229480

Content Provider 容器内实现

前言

四大核心组件之一的 Content Provider 同样需要在容器内单独做实现。我们主要考虑解决两个问题:

1容器内应用如何获取其他应用的 Content Provider

2容器如何为容器内的应用实现 Content Provider

很明显,直到 APP 被安装的那一刻,容器都不知道自己未来要实现怎么样的 Provider,因此我们需要考虑一种办法能够动态的安装这些组件。

Content Provider 容器内实现

在之前启动 Activity 时有一个函数 handleBindApplication 用来绑定 Application 对象,而在这个函数中会调用 installProviders 来安装所有的 Providers

其遍历了入参里的 ProviderInfo 数组,并调用 installProvider 来安装每个 Provider

这里就是直接调用原生的 installProvider 来完成安装了,并不需要有什么额外的操作。而且跟 Service 或 Activity 一样,VirtualApp 给它们提前占好了坑,用在 Blackbox 中是通过 ProxyContentProvider 来实现的:

这种描述其实有点问题,因为这些 ProxyContentProvider 其实并不是为了占坑而实现的,包括安装之类的操作其实都没有做相关的替换之类的操作。

然后在上面的操作完成以后,最后还有一个初始化的操作:


1 在这里先获取了目标进程里的 mProviderMap ,这个对象记录了进程中所有的 contentProvider。

2遍历这个 Map,然后把里面的 mProvider 全部替换成 ContentProviderStub

注意到这个地方相当于把那些注册好了的 mProvider 全部包了一层:、

然后对应的 invoke 函数:

相当于最终会得到一个 ContentProviderStub 来充当 ContentProvider,然后把其中的 invoke 做了点 hook。

而在那些需要使用 ContentProvider 的进程中,具体来说,VirtualApp 对那些需要使用 ContentProvider 的应用做了些手脚。在容器中,如果有哪个进程想要获取另外一个进程的 ContentProvider,就需要调用 getContentProvider


1 首先,尝试从 BPackageManager 拿到 ProviderInfo

2 如果拿到了,会尝试调用 initProcess 把目标进程唤起

3 通过 acquireContentProviderClient 来得到原先注册的那个 providerBinder

4 ProxyManifest.getProxyAuthorities 把入参替换成 "%s.proxy_content_provider_%d"

5最后去掉原生的那个函数去获取目标,这里应该会返回一个 ProxyContentProvider,不过它本来也是继承自 ContentProvider 的类,其实差不多

6 修改 infoProviderInfo ,修改 providerproviderBinder 从而将它伪造成真正 ContentProvider

7返回伪造后的结果

最后这里替换 providerproviderBinder 的时候有一层 ContentProviderStub 的包装。不过在调用它的 invoke 函数的时候会使用传入的 providerBinder 进行调用,因此没有问题。

综上所述,VirtualApp 实现了在容器内伪造 Content Provider 的能力。

参考文章

https://gityuan.com/2016/07/30/content-provider/ https://blog.csdn.net/ganyao939543405/article/details/76253562 https://juejin.cn/post/7028124957141893150

路径重定向和 Xposed 注入时机分析

前言

在过完了四大组件的容器内实现后,最后我们打算着重看看之前只是模糊提了几句的重定向问题,看看容器是如何把相关的资源和路径进行重定向的。

路径重定向和 Xposed 注入时机分析

在之前分析 Activity 时有这么一行代码用来支持路径的重定向:

代码注释中可以看到,应用本身的资源其实已经通过 Application 完成重定向了,但是仍有一些通过硬编码路径来访问文件的情况,为了避免这类访问引发崩溃,因此需要把它们重定向到新路径:

总的来说,先初始化了一些重定向规则,包括:

除此之位还有对进程路径的重定向:

相当于把对这些路径的访问全部过滤成后面的那个参数,然后再把它们注入到 Native 层去:

最后激活一下这些规则:

可以看到,容器对三个对象做了重定向,分别是文件系统、ClassLoader 以及 Binder。

文件系统 Hook

我们从文件系统看起:

可以看到主要就是对这些函数做了 Hook,以及 Hook 的方式也很朴素:

先用 GetStaticMethodID 或 GetMethodID 来获取原始函数的位置,然后把它回填到 orig_fun 中,最后调用 RegisterNatives 用新函数注册进去,替换结束。

而新函数基本上都是这样实现的:

先过滤一遍路径,然后调用原函数。过滤流程最终会调用这个函数:

就是遍历一下找到合适的规则然后根据规则做替换,没啥别的内容了。不过这里有一个疑问,看起来程序并没有对 open、stat 之类的 libc 函数做 Hook,那如果后续有硬编码写死去调用 open 的情况是不是就会出错呢?

同样的,除此之外还有很多各种各样的函数被调用时参数中都会附带路径,这些函数不需要一起过滤吗?以及同时过滤这么多函数也很容易被检查发现也是一个问题。或许后续需要完善。

ClassLoader Hook

挺朴素的,跟前文的方式是一样的,只是目标改成了 findLoadedClass 函数。

新函数的实现如下:

可以看到其实是在做一些过滤,把一些不希望被发现的 Class 过滤掉。

Binder Hook

同上,这次的目标是 getCallingUid 函数:

最终会先调用一遍原生函数,然后再用下面这个函数做处理:

如果是系统 UID 或不是用户应用的话就直接返回原本的值,而如果返回的结果是容器的 Uid ,那就要过滤一下替换成容器内目标应用的 Uid 了:

seccomp Hook

除此之位,Blackbox 还基于 seccomp 实现了对系统调用的 Hook:

触发上述的过滤条件时,会通过信号来回调:

可以看到,在这类对 openat 的系统调用做了拦截,不过目前还并没有做什么过滤,未来应该会有更好的支持。

Xposed 支持

算是最后一个笔者好奇的点。笔者之前并没有使用过 Xposed 的经历,因此对相关的内容其实不是很了解。通过网上的一些资料了解到,Xposed 是通过修改 app_process 的方式向进程里注入代码实现的,而这些操作需要它能够对 zygote 实现 Hook,那么对于 VirtualApp 来说要如何支持呢?

入口从创建应用时的 newApplication 开始:

因为之前创建启动 Activity 时已经用 AppInstrumentation 替换掉了 Instrumentation ,而在正常的创建流程中会调用该对象的 newApplication 函数,而 Xposed 的注入就发生在这个时机。这个时候应用对应的进程已经创建好了,后续就是等待绑定的流程了,而应用还尚且没有开始运行,因此算是合适的时机之一。

可以看到代码中在此刻加载了 Xposed,然后再带调用原生的 newApplication 恢复创建流程:

代码中遍历了容器内安装的所有模块,并通过 PineXposed.loadModule 来加载,这似乎是一个框架的内部实现,翻到了作者的原文笔记。里面提到的说是直接套了 SandHook 的 API ,继续往下翻似乎就是 SandVXposed ,不过最后也没咋找到我想看的,于是只好自己翻起其他的资料对照着源码试着啃啃看了。

首先是加载 Xposed 的相关代码:

然后从资源里读取代码然后加载:

然后下面有一个初始化 Xposed 的操作:

起初还以为是要注入 Zygote ,但是没有 root 也不能注入才对,翻了下源代码似乎是这样的:

看起来似乎不是注入 Zygote ,只是对其中的一些方法做了 Hook,既然如此就不需要真的去注入 Zygote 了,只需要在加载应用时把调一下这个方法把它们拦截下来就行了。

完成 Hook 以后再加载对应模块:

这个 sLoadedPackageCallbacks 只是暂存。在对每个模块做完上述操作以后,下面还有一段:

往下跟一下:

这里最后会去调用 XC_LoadPackage 下的方法:

最后这个 handleLoadPackage 方法就会调用到模块里那个重载后的 handleLoadPackage 来实现模块功能的加载了。

不过这里有一点很奇怪的是,为什么需要每个模块都调用一遍 initZygote 呢?岂不是在重复 Hook 那些方法么?看起来似乎每次都把模块信息传进去了,但是好像没有用到这个信息,那又为什么需要每次都调用一遍呢?

没想明白,如果有大佬知道的话还请多多指教。

重定向 Hook 总结和疑问

在最后一部分我们可以看到,如果通过 Seccomp 对 open 等系统调用做拦截的话,应该上层的很多重定向都可以放掉了?毕竟不管上层用了什么函数,底层对文件的访问都是需要系统调用去支持的,从这个层面说,只要在系统调用层面把路径全部做好过滤,是不是上层的重定向可以直接删掉?

不过 Seccomp 这一策略首先支持的版本比较新,老设备肯定是没有的,以及另一方面是,检测 Seccomp 是不是较为简单呢?笔者目前对这些问题尚没有答案,还需要请教各位大佬。

参考文章

https://blog.canyie.top/2020/04/27/dynamic-hooking-framework-on-art/ https://wufengxue.github.io/2019/11/01/get-3rd-xp-module-hookers.html https://blog.canyie.top/2020/02/03/a-new-xposed-style-framework/

0 条评论
某人
表情
可输入 255