android端手机虚拟机实现过程
以下是VirtualAPP的执行流程图:

我们通过这样的一个开源的virtualApp来了解一下,一个应用程序是怎么去实现在虚拟机里面运行的。
Virtual space
首先是应用程序的下载安装。实际上,在虚拟机中运行的应用程序是下载到的虚拟机空间(virtual space)中的。
Virtual Framework
虚拟机中的 App(比如 App1、App2)原本会直接调用系统 Framework,但是在虚拟机中的APP需要的就不同
- “以为”自己访问的是系统服务,
- 实际上却访问的是 VA 模拟的服务(Hook 之后的)。
Android Framework
Android Framework 是 Android 系统提供的一整套 API,它是 系统级服务(如 AMS、PMS、LocationService) 的接口,比如:
ActivityManagerService (AMS)
:管理 Activity 的启动与调度。
PackageManagerService (PMS)
:管理应用包的安装与信息。
LocationManager
:提供位置信息。
ClipboardManager
:剪贴板服务。
这些都属于 系统原生的 Android Framework,运行在系统进程(如 system_server
)中,不是用户 App 能随意改动的。而在虚拟机中的Framework,则是自开发的可改动的Framework。
而在Virtual Framework中,需要做的事情就是让APP觉得自己是在原生系统下执行的,根据Android Frameword中的应用程序的执行流程,我们可以知道的是
一个应用程序的执行流程大体是这样的:
- 点击桌面程序
- 将Activity顶上第一个设置为暂停状态,等待启动的应用
- 判断进程,先进程再程序
- 通过**Zygote开始fork()**应用程序进程了
- 执行ActivityThread.main()
- ActivityThread 通过 Binder 将自身的 ApplicationThread 传给 AMS,方便AMS通过ActivityThread启动应用
- AMS 通过 ApplicationThread.scheduleLaunchActivity() 通知启动 Activity
- ActivityThread 通过反射加载目标 Activity 类
- 调用 Instrumentation.callActivityOnCreate(),执行程序的OnCreate()函数
应用的真正启动是 ActivityThread
执行的,但整个过程是由 AMS
控制调度的,双方通过 Binder 建立通信桥梁。
虚拟 App vs 原生安装 App
方面 |
虚拟机中的 App(VA App) |
系统安装的 App(原生 App) |
安装方式 |
并未真正通过系统 PMS 安装,仅在 VA 虚拟空间中注册 |
通过系统 PMS 安装,注册了 Activity、Service 等 |
运行路径 |
安装路径、数据路径等都是被 重定向/虚拟的(如 /data/data/com.xxx -> /data/data/io.virtual.app/space/0/... ) |
使用 Android 系统真实的 /data/data/com.xxx 路径 |
访问系统服务 |
所有访问系统服务的调用(如 Location、Clipboard)都被 VA Hook 拦截和“伪造” |
调用系统原生 ServiceManager 提供的服务,直接连接 system_server |
AMS / PMS 管理 |
由 VA 模拟的 AMS/PMS 进行调度(Activity 启动、Service 调用等) |
由系统的 ActivityManagerService 和 PackageManagerService 控制 |
权限模型 |
权限请求和判断被 VA 接管(可以伪造授予,也可以强制拦截) |
权限由 Android 系统控制,用户手动授权 |
多开与隔离能力 |
可以任意多开,同一 App 多个实例之间相互隔离 |
系统默认一个包名只允许一个实例,数据也无法隔离 |
文件/IO 控制 |
VA Hook 底层文件系统访问,可实现只读/禁止/伪造/隔离等策略 |
系统文件访问直接作用于真实文件系统 |
运行时感知 |
可以“欺骗” App,使其以为自己运行在正常系统中(比如感知不到虚拟环境) |
真实环境,App 可以访问所有支持的设备资源 |
安全性控制 |
更灵活,可以模拟环境、劫持函数、限制行为 |
安全性高但开放度小,无法灵活控制运行环境 |
那么实际上在虚拟机下应用程序的执行流程也应该是这样的,但是不同的是,这里的Server服务不是Android 原生下的framework,而是虚拟机的framework。
而这里就应用到的是APP HOOK的主要实现了
APP HOOK
Hook 的目标是 系统服务类,主要分两种:
1. Java 层服务(运行在 system_server)
VA 会通过 反射、代理(比如 Binder 代理)或动态注入,替换系统服务对象。
举个例子:
- App 调用
Context.getSystemService("location")
拿到 LocationManager
。
- 正常系统下,它返回的是 Binder 连接到 system_server 的服务。
- VA Hook 后返回的是它自己实现的“假 LocationManager”,比如总是返回一个虚假的地理位置。
2. Binder 接口 Hook
- VA 使用类似 “Binder IPC 劫持” 的方式,在 Java 层模拟 AMS、PMS 等 Binder 服务。
- 这就像劫持了一条“电话线”,App 以为在和系统通信,其实是和 VA 自己通信。
细节的说一下可能出现的具体实现:
- App 调用
ActivityManager.startActivity()
- APP Hook 中的 AMS Hook 通过代理劫持系统服务调用(比如修改
IActivityManager
的 Binder)
- 它并不真的去调用系统 AMS,而是把调用重定向到了 VirtualApp 自己实现的 VA Server → AMS
- VA Server 的 AMS 执行启动流程(比如在 VirtualActivityStack 中注册一个 Activity 记录)
- 最后,伪装成系统返回正常的
ActivityResult
,App 觉得自己“真的启动了 Activity”
而在APP HOOK拦截完成之后,VA Server就需要开始去提供对应的服务来实现拦截之后的服务请求。
所以:APP Hook 是用来“拦截”App 调用系统服务行为的;而 VA Server 是用来**“虚拟提供”这些服务的实现端。**
Virtual Native
在这一层主要为了完成2个工作,IO重定向和VA APP与Android系统交互的请求修改。
IO重定向
比如在一些APP中,会把一些配置、缓存、数据库等文件的路径写死在代码里
1 2 | new File( "/data/data/com.example.app/shared_prefs/config.xml" )
fopen( "/data/data/com.example.app/files/config.dat" , "r" );
|
这种情况下的,用虚拟机打开的程序就无法获取适配对应路径的文件和配置信息。
所以说出现了 VA Native的存在的意义:
- 拦截所有
open
, fopen
, access
, stat
等系统调用
- 判断路径是否是虚拟 App 的目标路径
- 替换为虚拟路径,到真实的程序的虚拟路径的位置
不过这里应该算得上是Native层的HOOK了。
JNI Native函数
以上的APP HOOK 都是基于Java层进行的,但是相应的Android 有大量的系统行为是通过 Native 实现的。这时候对应的应用程序适配就需要Native HOOK来实现
情况 |
举例 |
解决方法 |
直接访问文件 |
open , fopen , access |
hook libc |
直接调用系统服务 |
getuid() , getpid() |
hook syscall |
访问特殊设备 |
/dev/* , /proc/* |
重定向路径 |
ART Runtime 优化 |
libart.so 调用 |
hook libart |
动态链接时 resolve |
dlsym 取函数地址 |
hook linker |
这里举例的程序应用就是需要通过HOOK so层来实现分析和获取适配对应路径的文件和配置信息。
源码分析部分
mirror
在虚拟机中很多的细节其实是通过HOOK,或者通过反射的操作调用实现的。比如拦截真实的AMS,PMS等服务,返回自写的各种服务。而在虚拟机框架中为了使得调用拦截等更加方便则出来对于反射等的封装。而在这里的封装就叫mirror
正常执行下去拦截改定位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | private void hookInstrumentation() {
try {
Class<?> activityThreadClass = Class.forName( "android.app.ActivityThread" );
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod( "currentActivityThread" );
currentActivityThreadMethod.setAccessible( true );
Object currentThread = currentActivityThreadMethod.invoke( null );
Field instrumentationField = activityThreadClass.getDeclaredField( "mInstrumentation" );
instrumentationField.setAccessible( true );
Instrumentation originalInstrumentation = (Instrumentation) instrumentationField.get(currentThread);
InstrumentationDelegate instrumentationDelegate = new InstrumentationDelegate(originalInstrumentation);
instrumentationField.set(currentThread, instrumentationDelegate);
} catch (Exception e) {
e.printStackTrace();
}
}
|
mirror拦截改定位
1 2 3 4 | private void hookInstrumentation(){
Object currentThread ActivityThread.currentActivityThread.call();
Instrumentation originInstrumentation ActivityThread.mInstrumentation.get(currentThread);
ActivityThread.mInstrumentation.set(currentThread, new InstrumentationDelegate(originInstrumentation));
|
这里实际上就去做了对应的函数封装,从而更加快速实现的代码逻辑
Java层 HOOK
举例应用调用AMS执行流程的过程来看看Java层 HOOK
Android 系统中,所有四大组件的管理都要通过系统的 ActivityManagerService(AMS) 完成,客户端通过 IActivityManager
接口 调用 AMS。
所以 Hook AMS = 控制所有组件的启动流程!得到IActivityManager
接口就可以拦截执行流程
Activity.startActivity(Intent)
↓
Instrumentation.execStartActivity(...)
↓
ActivityManager.getService().startActivity(...)
这里会走ActivityManager.getService() ,而在android8.0的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
return ActivityManagerNative.getDefault();
}
};
|
getService() 会返回 IActivityManagerSingleton.get();
所以在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class ActivityManagerStub extends MethodInvocationProxy<MethodInvocationStub<IInterface>> {
public ActivityManagerStub() {
super ( new MethodInvocationStub<>(ActivityManagerNative.getDefault.call()));
}
public void inject() throws Throwable {
if (BuildCompat.isOreo()) {
Object singleton = ActivityManager.IActivityManagerSingleton.get();
Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());
} else {
}
}
|
用反射将其 mInstance
替换成我们刚才构造的 代理对象,应用对 AMS 的所有调用(比如 startActivity()
),都会经过我们这个 Hook 代理对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static class StartActivity extends MethodProxy {
@Override
public String getMethodName() {
return "startActivity" ;
}
@Override
public Object call(Object who, Method method, Object... args) throws Throwable {
int res = VActivityManager.get().startActivity(args);
return res;
}
}
}
|
在这里去调用 int res = VActivityManager.get().startActivity(args); 此处直接将调用转发到 VActivityManager
中(VirtualApp 的虚拟 AMS)。
“拦截 AMS 服务” 的核心目的,实际上就是为了拿到 IActivityManager
的实例,并用我们构造的 代理对象去替换掉它。因为 Android 应用中的 组件调度(包括启动 Activity、Service、广播等)全都需要通过这个接口来向系统发送请求。
1 2 3 | ActivityManager.getService().startActivity(...)
ActivityManager.getService().broadcastIntent(...)
ActivityManager.getService().startService(...)
|
这些都是 IActivityManager
的方法。
native层hook
上面也已经说过了Native层HOOK的原理,这里就执行来看看 io重定向,实现修改的文件读写的路径的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @SuppressLint ( "SdCardPath" )
private void startIOUniformer() {
ApplicationInfo info = mBoundApplication.appInfo;
int userId = VUserHandle.myUserId();
String wifiMacAddressFile = deviceInfo.getwifiFile(userId).getPath();
NativeEngine.redirectDirectory( "/sys/class/net/wlan0/address" , wifiMacAddressFile);
NativeEngine.redirectDirectory( "/sys/class/net/eth0/address" , wifiMacAddressFile);
NativeEngine.redirectDirectory( "/sys/class/net/wifi/address" , wifiMacAddressFile);
NativeEngine.redirectDirectory( "/data/data/" + info.packageName, info.dataDir);
NativeEngine.redirectDirectory( "/data/user/0/" + info.packageName, info.dataDir);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
NativeEngine.redirectDirectory( "/data/user_de/0/" + info.packageName, info.dataDir);
String libPath = VEnvironment.getAppLibDirectory(info.packageName).getAbsolutePath();
String userLibPath = new File(
VEnvironment.getUserSystemDirectory(userId),
info.packageName + "/lib"
).getAbsolutePath();
NativeEngine.redirectDirectory(userLibPath, libPath);
NativeEngine.redirectDirectory( "/data/data/" + info.packageName + "/lib/" , libPath);
NativeEngine.redirectDirectory( "/data/user/0/" + info.packageName + "/lib/" , libPath);
}
VirtualStorageManager vsManager = VirtualStorageManager.get();
String vsPath = vsManager.getVirtualStorage(info.packageName, userId);
boolean enable = vsManager.isVirtualStorageEnable(info.packageName, userId);
if (enable && vsPath != null ) {
File vsDirectory = new File(vsPath);
if (vsDirectory.exists() || vsDirectory.mkdirs()) {
HashSet<String> mountPoints = getMountPoints();
for (String mountPoint : mountPoints) {
NativeEngine.redirectDirectory(mountPoint, vsPath);
}
}
}
NativeEngine.enableIORedirect();
}
|
可以看到这里利用redirectDirectory函数,去重定向了对应可能读取的位置,替换成了虚拟机对应位置的内容。
1 2 3 4 5 6 7 8 | HOOK DEF( int ,faccessat, int dirfd, const char *pathname, int mode, int flags){
int res;
const char *redirect_path relocate_path(pathname,&res);
int ret syscall(_NR_faccessat,dirfd,redirect_path,mode,flags);
FREE(redirect_path,pathname);
return ret;
}
|
该宏会生成一个以 new_
为前缀的函数,例如:
1 | int new_faccessat(...) { ... }
|
- 判断是否需要重定向(如
/data/data/com.xxx
→ /virtual/data/com.xxx
)
- 如果是,则返回新的路径;否则返回原路径
res
用于记录是否发生了实际替换
举个例子:
pathname = "/data/data/com.test/files/a.txt"
如果这个 app 被 VA 管控了,那路径会被转向
redirect_path = "/data/user/0/com.va.wrapper/files/a.txt"
从这里就对于开头的那个图片进行了总体的大致讲解,但是其中虚拟机下运行应用的很多问题以及配置环境更多的是需要在实践运用上去体现。
参考资料:
527K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7@1I4G2k6s2W2Q4x3V1k6h3K9i4u0@1N6h3q4D9b7i4m8H3
28dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6B7N6h3g2B7K9h3&6Q4x3X3g2U0L8W2)9J5c8Y4m8G2M7%4c8Q4x3V1j5%4x3o6t1^5x3e0t1@1z5e0f1%4x3e0b7I4z5o6V1K6x3e0f1H3i4K6t1K6K9r3g2S2k6r3W2F1k6#2)9J5k6o6p5I4
c14K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4N6W2K9i4S2A6L8W2)9#2k6U0x3K6z5o6p5$3x3K6l9H3i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8X3c8W2N6r3q4A6L8s2y4Q4x3V1j5^5z5o6M7%4x3U0b7$3x3R3`.`.
980K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3N6S2L8Y4W2S2L8K6V1K6z5e0f1@1x3K6b7H3y4g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3y4K6j5I4y4o6j5%4y4U0l9`.