VirtualApp (VA) 框架原理解析

一、VA 的多进程架构

VA 一共会运行在三种进程之中:

  • 宿主进程 (io.virtualapp):即 VA 自身的主进程。
  • Client App 进程 (io.virtualapp:pN):即在 VA 中运行的各种 App 进程。
  • VA Server 进程 (io.virtualapp:x):虚拟服务子进程。

关键特性:双开应用没有直接与真实系统服务交互,而是与虚拟服务进行交互。虚拟服务实现了真实系统服务的能力。

VA环境下的运作方式

二、核心通信机制:虚拟服务层

在 Client App 进程和原生 framework services 的通讯过程中,添加了新的一层 VA Server。

  • 实现方式:通过 V+原service 的命名方式新增了 VAMSVPMS 等 VA service。
  • 功能:它们仿造了原 framework 的部分功能,用于管理 Client App 的各种会话,并自身再与原生 framework services 通讯。

Hook 代理的作用
原生 AMmirror.AMVAM 的作用是 hook Client App 的各种方法,具体操作包括:

  • 替换方法参数(如包名、uid)。
  • 引导方法调用到 VA Server 中。

服务获取流程
VA Server 端所有的 service 集中由 ServiceFetcher 提供。Client 端通过 Provider.call 的跨进程方式获取 IServiceFetcherIBinder 句柄,再通过其获取其他 Service 来完成调用。

三、设计动机:以 Activity 启动为例

3.1 问题背景

为什么要设计中间的这层调用呢?我们用 Activity 举例说明:

看过 Activity 源码的同学知道,当我们启动一个新的 Activity 时,它会先经过 AMS。AMS 会处理一系列逻辑,包括:

  • 维护 record 记录注册,
  • 判断目标进程是否需要创建,
  • 处理启动模式 flags 等。

最终,AMS 会回调到 ActivityThread,调用 InstrumentationnewActivity 方法完成创建并执行其 OnCreate 生命周期。

3.2 核心矛盾

但是,我们通过 VA 运行的 Client App 存在一个根本性问题:

  • 包名/uid 和 VA 宿主是不一致的
  • 并且没有注册在宿主清单文件中

因此,如果直接启动,就会受到系统服务的严格校验而失败。

3.3 “偷梁换柱”解决方案

VA 通过一套精妙的两步走策略解决了这个问题:

  1. 第一步(欺骗 AMS):VA 通过预先注册 StubActivity(其包名就是宿主),然后 hook Client App 所有关联启动的方法,将方法中的参数(即启动 intent)替换为宿主的 StubActivity。这样,对于 AMS 的感知而言,它只知道启动了一个合法的宿主 StubActivity,校验得以通过。
  2. 第二步(还原 Intent):在 AMS 处理完毕后,VA 再拦截 mH 这个 Handler 的启动消息,将 intent 替换回原版 intent,从而完成“偷梁换柱”的过程。

架构优势:而多进程的框架设计,可以让子进程的 crash 不影响宿主进程的运行。

四、关键技术点详解

4.1 Hook 服务代理

  • 操作:Hook 服务代理,替换为自定义的系统服务代理。
  • 细节:pN 进程的目标应用 Hook 了所有的系统服务代理,应用进程没有直接与系统服务通信,而是与 :x 进程的虚拟服务进行通信。
  • 实现:与 H 实例注入 callback 对象实现 msg 消息拦截,进行数据替换。
  • 源码指引:可查看 InvocationStubManager 类。

4.2 :x 进程实现虚拟系统服务

  • 操作:x 进程实现虚拟系统服务。
  • 细节:VirtualApp 中运行的 apk 访问系统服务,均为 VirtualApp 在 :x 进程模拟的系统服务。
  • 流程:上述步骤中 Hook 了服务代理后,服务代理的实现会访问 :x 进程提供服务 bunder 代理对象。
  • 源码指引:可查看 BinderProvider

4.3 AMS 服务代理与 ActivityInfo 信息获取

  • 操作:AMS 服务代理,ActivityInfo 信息获取。
  • 细节:我们 Hook 了 ASM 服务代理,所以每次我们在启动 Activity 的时候都会获取目标应该的 ActivityInfo 数据,获取 ActvityInfo 数据这一个关键步骤。
  • 源码指引:可查看 MethodProxies

4.4 Activity 栈管理

  • 操作:Activity 栈管理。
  • 细节:VA 采用的也是通过占坑的方式,因为我们的双开应用并没有真实的安装在系统上,所以需要采用占坑的方式,系统启动的是 StubActivity,在应用进程的 H 的 Callback 进行替换为具体的 Activity。
  • 透明性:这一替换对系统服务是没有感知的,应为系统是通过 Binder token 来识别某一个 Activity 的。
  • 优化点:另外我们可以发现 VA 为每个应用配置了一个标准启动模式,亲和度为 com.lody.virtual.vt 的 StubActivity,与 DroidPlugin 不同不需要配置多个启动模式的 Activity。这主要依赖于 VAMS 的栈管理和 Intent Flag 的配合。

4.5 目标应用进程启动与 intent 数据替换(第一个 hook 点)

  • 操作:目标应用进程启动与 intent 数据替换(第一个 hook 点)。
  • 细节:我们在跳转到 :pN 目标应用进程需要将目标应用进程提前启动起来。VAMS 记录了启动的进程列表数据,这样就可以知道新启动的 APP 应该是 :pN 进程。进程启动之后我们就可以根据 targetApp.vpid 替换对应的 StubActivity。

4.6 HCallbackStub 通过 intent 数据将 StubActivityRecord 数据替换回来(第二个 hook 点)

  • 操作:HCallbackStub 通过 intent 数据将 StubActivityRecord 数据替换回来(第二个 hook 点)。
  • 细节:PN 进程的目标应用户对 H 实例添加 callback (HCallbackStub),这样我们就可以对消息进行处理,callback 会拦截 LAUNCH_ACTIVITY 消息。
  • 后续流程:上述步骤执行完成之后会调用系统正常的 handleLaunchActivity 方法,之后会调用 performLaunchActivity 方法传递 ActivityClientRecord 与 customIntent 而 customIntent 为 null。所以 Activity 实例化过程 ActivityClientRecord 是关键点。
  • 类加载:上述步骤由于我们已经替换为了插件的 ActivityInfo 信息,所以创建的 LoadAPK 对象为插件的 apk 内存实例对象,获取的 classLoader 对象也是指定了插件的代码路径与 so 路径等,所以使用这个 classLoader 就可以创建目标 Activity 了。
  • 资源加载:Activity 在 ContextImpl 对象创建是会创建 Resources 对象。创建 Resources 对象同样是使用 r.packageInfo 对象参数进行初始化,当然传递的均为插件 apk 资源路径数据。
  • 核心替换:上述整个过程通过替换 ActivityClientRecord 的 activityInfo 的 applicationInfo 对象为插件的 applicationInfo 信息以方便创建插件的 LoadAPK 对象以方便对类和资源进行加载。
  • Hook 实现:而 ActivityInfo 的替换是通过 HOOK AMSProxy 实现的。
  • 源码指引:可见 MethodProxies 类的 StartActivity 方法。

五、参考资料


©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容