virtualapp源码分析_virtualapp商业版

virtualapp源码分析_virtualapp商业版VirtualApp(以下简称VA)是一款运行在android系统中的沙盒产品,本篇主要就VA的实现原理来分析,源码虽然枯燥,却有很多闪光点值得我们学习。

作者:字节游戏中台客户端团队 – 聂伟

背景

VirtualApp(以下简称VA)是一款运行在android系统中的沙盒(或者叫轻量andorid虚拟机)产品。

项目地址:github.com/asLody/Virt…

VA相关的功能可以通过github介绍来了解,我们本篇主要就VA的实现原理来分析,源码虽然枯燥,却有很多闪光点值得我们学习。

运行机制

首先来看看VA环境下的运作方式(引自网图):

virtualapp源码分析_virtualapp商业版

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

  1. 宿主进程,即VA自身的主进程,子进程
  2. Client App进程,即在VA中运行的各种App进程
  3. VA Server进程

通过上图可以看到在Client App进程和原生framework services的通讯过程中,添加了新的一层VA Server,通过V+原service的命名方式新增了VAMS,VPMS等VA service,他们仿造了原framework的部分功能,管理Client App各种会话,自身再和原生framework services通讯。

此外我们还能看到,光AM示例,就有原生AM,mirror.AM,VAM三种,暂时只需要知道他们的作用是hook Client App的各种方法,替换方法参数(包名,uid),或引导方法调用到VA Server中。

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

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

看过Activity源码的同学知道,当我们启动一个新的Activity时,它会先经过AMS,AMS会有一些record记录注册,目标进程是否需要创建,启动模式flags等的处理,最终再回调回ActivityThread,调用Instrumentation的newActivity方法完成创建并执行其OnCreate生命周期。

但是我们通过VA运行的Client App,包名/uid和VA宿主是不一致的(并且没有注册在宿主清单文件中),那么直接启动就会受到系统服务的校验,VA通过预先注册StubActivity(包名就是宿主),然后hook Client App所有关联启动的方法,将方法中的参数(这里指启动intent)替换为宿主,对于AMS的感知就只知道启动了一个宿主的StubActivity,最终VA再拦截mH这个Handler的启动消息,将intent替换为原版intent,完成偷梁换柱的过程。

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

当然,大体原理是如此,实际情况会更复杂一些,接下来我们看看代码包结构:

代码包结构

virtualapp源码分析_virtualapp商业版

  1. android:通过建立和android相同的目录来引用一些系统隐藏类,达到欺骗编译器的目的,如android.content.pm.PackageParser。
  2. client:Client App所运行的环境,包含大量的hook代码。
  3. server:VA Server进程相关,仿造了原生framework services部分功能。
  4. mirror:android系统类的镜像包,和系统类同名,封装了反射过程,可以很便捷地直接调用系统类的一些字段和方法。
  5. jni:native hook相关,主要是虚拟机的hook和io重定向(将Client App中的io路径重定向到VA内部)。

其他如os是处理一些环境问题以及多用户的管理,remote是用于aidl传输的各种序列化bean。

从运行机制和代码包结构中,可以看到VA基本的轮廓,下面我们逐一进行分析。

源码分析

mirror

假设我们想hook掉Instrumentation,首先来看看普通的hook实现:

virtualapp源码分析_virtualapp商业版

然后是mirror实现:

virtualapp源码分析_virtualapp商业版

可以看到普通的实现方式不仅繁琐,而且有大量的模板代码,重复工作,而mirror大大简化了这个过程,仿佛就像是在直接调用系统方法一样。

我们看看mirror下的ActivityThread定义:

virtualapp源码分析_virtualapp商业版

所有使用的原ActivityThread的相关成员变量/方法,都以:

public static Ref/RefStatic+属性类型 同名成员变量名/方法名

的方式声明在了mirror.ActivityThread当中,并且在RefClass.load方法中传入当前Class对象和真正的className:

virtualapp源码分析_virtualapp商业版

load方法封装了反射获取字段的逻辑,映射同名的属性到mirror类下(正如mirror的英译:镜像),其中Ref/RefStatic+属性类型内部也有同名filed的赋值逻辑,不再赘述。

mirror以简洁而又优雅的设计大大简化了反射注入的成本,使用过程中只需要声明属性即可,而不需要关心其反射细节,使用简单,原理简单。

Java层hook

运行机制有说到,VA会hook原生的AM,来截断部分方法调用到VAM,VAM再去调用自实现的VAMS。我们简单以startActivity方法示例:

virtualapp源码分析_virtualapp商业版

原生AM中持有AMS的IBinder句柄,可以从这点入手,创建其动态代理对象:

public class ActivityManagerStub extends MethodInvocationProxy<MethodInvocationStub<IInterface>> {



    public ActivityManagerStub() {

        //基于mirror获取AMS的IActivityManager接口对象,并创建其动态代理对象

        super(new MethodInvocationStub<>(ActivityManagerNative.getDefault.call()));

    }



    @Override

    public void inject() throws Throwable {

        if (BuildCompat.isOreo()) {

            //8.x以上

            Object singleton = ActivityManager.IActivityManagerSingleton.get();

            //替换原AMS对象为我们的动态代理对象

            Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());

        } else {

           //8.x以下通过ActivityManagerNative.gDefault.get()获取

        }

    }



    @Override

    protected void onBindMethods() {

        //可以批量添加各种hook方法

        addMethodProxy(new StartActivity());

    }



    //方法hook实现

    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;

        }

    }

}

最后实例化此Stub类,并执行inject即可,startActivity方法在执行时会引导其调用到VA的VAM中。

VA将方法封装成MethodProxy对象,我们可以很方便的通过方法名来定义各种hook方法,处理方法参数,然后在onBindMethods进行批量的方法添加。

在Stub的基类中又封装了动态代理对象的创建过程,并管理了我们所有添加的MethodProxy,在创建动态代理对象的InvocationHandler中决定执行hook方法还是执行原方法(只会hook相关逻辑的方法,其他方法会正常执行原逻辑)。

最终,通过inject子类实现,真正实现动态代理对象的替换。

我们通过代码结构可以看到java层hook的全貌:

virtualapp源码分析_virtualapp商业版

  • VA中实现了大量了Stub来hook各种系统服务(proxies包),并最终添加到InvocationStubManager中统一进行inject调用。
  • 很多场景下的方法只需要替换固定位置的参数(如包名,uid),VA为此提供了数个可以快捷hook方法的衍生类,如ReplaceCallingPkgXXX,只需要简单继承即可。
  • 封装的思路并不难理解,难点在于如何找到hook点(并且还存在android多版本差异的兼容问题),VA几乎接管了整个framework层,任何没有hook到的地方都有可能引发crash,这也是其稳定性比较难做的原因之一。

我们通过下面的图,可以简单了解到大部分系统服务hook所需要做的工作(大多方法只需要替换包名即可):

virtualapp源码分析_virtualapp商业版

native层hook

native hook主要分为PLT hook和inline hook两种主流实现方式,VA开源代码中使用的是inline hook类型的开源三方库Cydia Substrate,而在后续的商业版代码中,VA也更新了whale(罗迪自研)和sandHook(目前也归罗盒所有)等native hook框架。

为什么需要native hook?这里以io重定向为例:

所有运行在VA中的Client App(包名即宿主)中对于文件/sp进行读写操作时,所指向的目录都是原本包名下的目录,而android系统对于访问非自身包名做了限制(比如android11的强制分区存储),因此需要把这些目录重定向到VA内部,同时也利用了Client App自身的包名作为父级目录,来达到各App之间文件数据隔离的目的。

而对于android系统来说,文件的读写操作最终都是通过libc.so库函数提供的方法,因此VA需要hook libc.so库函数,修改相关函数的输入参数,我们直接看具体的逻辑:

virtualapp源码分析_virtualapp商业版

该方法会在Clint App初始化其application时调用,通过redirectDirectory来添加重定向前后的目录,这里可以看到一部分是data/data为首的原始目录,还有一部分是外置存储的各种目录(VA完全按照其规则在内部也创建了相同的目录结构)。

然后通过NativeEngine.enableIORedirect()来调用到IOUniformer.cpp中的startUniformer方法:

virtualapp源码分析_virtualapp商业版

可以看下首个方法faccessat的替换方法声明:

virtualapp源码分析_virtualapp商业版

HOOK_DEF会给该方法添加new_前缀,而HOOK_SYMBOL最终宏替换为hook_function,使用MSHookFunction函数来调用三方库Cydia Substrate的hook能力。

当系统执行faccessat时,会被hook到我们声明的new_faccessat方法内,该方法最终完成了替换目录参数的功能,而为何会执行new_faccessat正是Cydia Substrate所做的事情。

因此可以看到这里的核心技术竞争力在于hook框架,剩下的都是利用框架所做的封装和使用。

而Cydia Substrate实际上出现的非常早期,并且已经商业化闭源,不能确保其开源稳定性,一些拓展功能可能需要更稳定的hook框架。

当然,VA的native层hook也不单单做了io重定向的事情,同时还有一些FileSystem和Android VM(如Camera,Audio,Meia,Runtime)相关的hook,感兴趣的同学可以再看看相关逻辑,这里不再进行分析。

安装

安装方法由VirtualCore提供,最终调用远程VAService中的VAppManagerService.installPackage():

public synchronized InstallResult installPackage(String path, int flags, boolean notify) {

 long installTime = System.currentTimeMillis(); 
 if (path == null) { 
 return InstallResult.makeFailure("path = NULL"); 
 } 
 // dex转二进制,安装时可以选择跳过 
 boolean skipDexOpt = (flags & InstallStrategy.SKIP_DEX_OPT) != 0; 
 File packageFile = new File(path); 
 if (!packageFile.exists() || !packageFile.isFile()) { 
 return InstallResult.makeFailure("Package File is not exist."); 
 } 
 VPackage pkg = null; 
 try { 
 //解析包结构为VPackage(四大组件,权限信息等)并序列化到磁盘中 
 pkg = PackageParserEx.parsePackage(packageFile); 
 } catch (Throwable e) { 
 e.printStackTrace(); 
 } 
 if (pkg == null || pkg.packageName == null) { 
 return InstallResult.makeFailure("Unable to parse the package."); 
 } 
 InstallResult res = new InstallResult(); 
 res.packageName = pkg.packageName; 
 //...省略检测该package是否需要更新代码 


 //安装模式,一种是手机中已经安装的app,另外一种直接是apk包 
 boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0 
 && VirtualCore.get().isOutsideInstalled(pkg.packageName); 


 if (existSetting != null && existSetting.dependSystem) { 
 dependSystem = false; 
 } 
 //拷贝so包 
 NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir); 


 //如果安装模式是安装apk包,则需要拷贝apk,这也是直接安装apk包速度更慢的原因 
 if (!dependSystem) { 
 File privatePackageFile = new File(appDir, "base.apk"); 
 File parentFolder = privatePackageFile.getParentFile(); 
 if (!parentFolder.exists() && !parentFolder.mkdirs()) { 
 VLog.w(TAG, "Warning: unable to create folder : " + privatePackageFile.getPath()); 
 } else if (privatePackageFile.exists() && !privatePackageFile.delete()) { 
 VLog.w(TAG, "Warning: unable to delete file : " + privatePackageFile.getPath()); 
 } 
 try { 
 FileUtils.copyFile(packageFile, privatePackageFile); 
 } catch (IOException e) { 
 privatePackageFile.delete(); 
 return InstallResult.makeFailure("Unable to copy the package file."); 
 } 
 packageFile = privatePackageFile; 
 } 
 if (existOne != null) { 
 PackageCacheManager.remove(pkg.packageName); 
 } 


 //sd卡上执行bin需要可执行权限 
 chmodPackageDictionary(packageFile); 


 //新建PackageSetting存储相关信息 
 PackageSetting ps; 
 if (existSetting != null) { 
 ps = existSetting; 
 } else { 
 ps = new PackageSetting(); 
 } 
 ps.skipDexOpt = skipDexOpt; 
 ps.dependSystem = dependSystem; 
 ps.apkPath = packageFile.getPath(); 
 ps.libPath = libDir.getPath(); 
 ps.packageName = pkg.packageName; 
 ps.appId = VUserHandle.getAppId(mUidSystem.getOrCreateUid(pkg)); 
 if (res.isUpdate) { 
 ps.lastUpdateTime = installTime; 
 } else { 
 ps.firstInstallTime = installTime; 
 ps.lastUpdateTime = installTime; 
 for (int userId : VUserManagerService.get().getUserIds()) { 
 boolean installed = userId == 0; 
 ps.setUserState(userId, false/*launched*/, false/*hidden*/, installed); 
 } 
 } 
 //保存pkg信息到磁盘中,内存中 
 PackageParserEx.savePackageCache(pkg); 
 PackageCacheManager.put(pkg, ps); 
 mPersistenceLayer.save(); 
 BroadcastSystem.get().startApp(pkg); 
 //广播通知安装已经完成 
 if (notify) { 
 notifyAppInstalled(ps, -1); 
 } 
 res.isSuccess = true; 
 return res; 
}

安装过程主要就是进行一些必要的拷贝(apk和so包),解析menifest中的各种信息(四大组件,权限等)并保存起来,以备需要时调用。

而当我们再次安装同一个的app时,不会再进行上述逻辑,仅仅是为该app添加一个新的userId:

virtualapp源码分析_virtualapp商业版

这里userId存在一个复用逻辑,比如已经安装了同一个app四次,userId分别为0,1,2,3,然后卸载了2,再重新安装会优先复用2(如果已使用就继续递增),最终通过VAppManagerService的installPackageAsUser方法,为该app添加一个userId记录并保存到磁盘即可。

我们安装oppo应用商店两次,来看看安装后的数据目录:

virtualapp源码分析_virtualapp商业版

  1. 拷贝该app的所有so包,如果直接安装apk还会在目录下存放拷贝的base.apk。
  2. package和签名信息序列化后保存的文件,包含该app的所有组件信息。
  3. 用户体系,userlist中保存了所有的userId信息,默认user为0。
  4. dex转二进制存放的目录,以及外置存储目录(会通过io重定向将app对外置存储的操作重定向到此目录中)。

启动

运行机制里说过,VA一共运行在三种进程中(最新的介绍中又添加了64位支持的插件进程),宿主进程自不必说,我们主要看看VA Server进程和Client App进程是如何运行起来的:

virtualapp源码分析_virtualapp商业版

清单文件中可以看到prcess定义了两种进程,x进程(即VA Server进程)和p进程(即Client App进程),p进程命名为p0,p1,p2等等,这是因为我们可能会运行多个app,或者不同进程的组件,在Client App启动以后,VA会将p进程修改为Client App真正进程的名字。

而每次新进程的创建,意味着Application的重新初始化:

virtualapp源码分析_virtualapp商业版

virtualapp源码分析_virtualapp商业版

VApp的多次初始化都会调用VirtualCore的startup方法,其中InvocationStubManager通过injectAll添加所有hook类,完成了java层hook的注入:

virtualapp源码分析_virtualapp商业版

那么,x进程是如果拉起的呢?这里VA比较巧妙地利用了Provider的call方法自动拉起进程的机制,来看看BinderProvider:

virtualapp源码分析_virtualapp商业版

我们发现BinderProvider中添加并初始化了所有的VA service(实际最终添加到ServiceFetcher中),并在call方法中put了ServiceFetcher这个IBinder句柄,ServiceFetcher正是Client端获取VA Service的钥匙。

virtualapp源码分析_virtualapp商业版

最后在Client端中可以进行调用获取:

  1. 通过Provider的call方法,获取BinderProvider返回的Bundle,而BinderProvider在清单文件中prcess定义正是:x进程,如果进程未拉起,此时默认会拉起x进程,所有的VA Service运行在x进程之上。
  2. 拿到Bundler以后,就可以获取BinderProvider中put进去的Binder,正是ServiceFetcher。
  3. ServiceFetcher管理着所有的VA Service,通过ServiceFetcher,即是拿到了所有VA Services的调用权。

p进程也是同理,当我们想要启动Client App中的某个四大组件,发现组件所在的进程不存在,那么首先肯定是拉起进程:

virtualapp源码分析_virtualapp商业版

这里也初始化了本地的一个VClientImpl,记录了token和vuid,并将Client打包到Bundle以便调用Client端的方法,最终同样也是通过Provider.call的方式进行唤起:

virtualapp源码分析_virtualapp商业版

其中VASettings.getStubAuthority(vpid)是用来匹配进程编号所对应的StubContentProvider,在p进程拉起后,会bindApplication进行子程序包的初始化:

 private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {

 //省略一部分代码 
 AppBindData data = new AppBindData(); 
 //获取安装时保存的apk信息,通过VPMS 
 InstalledAppInfo info = VirtualCore.get().getInstalledAppInfo(packageName, 0); 
 if (info == null) { 
 new Exception("App not exist!").printStackTrace(); 
 Process.killProcess(0); 
 System.exit(0); 
 } 
 data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid)); 
 data.processName = processName; 
 data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA); 
 Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")"); 
 mBoundApplication = data; 
 //设置进程的名字,此时如p0,p1等进程命名正式变为目标进程名字 
 VirtualRuntime.setupRuntime(data.processName, data.appInfo); 
 int targetSdkVersion = data.appInfo.targetSdkVersion; 
 if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) { 
 StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build(); 
 StrictMode.setThreadPolicy(newPolicy); 
 } 
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) { 
 mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion); 
 } 
 if (VASettings.ENABLE_IO_REDIRECT) { 
 //io重定向 
 startIOUniformer(); 
 } 
 //hook native 函数 
 NativeEngine.launchEngine(); 
 Object mainThread = VirtualCore.mainThread(); 
 //准备 dex 列表 
 NativeEngine.startDexOverride(); 
 Context context = createPackageContext(data.appInfo.packageName); 
 //设置虚拟机系统环境 
 System.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath()); 
 File codeCacheDir; 
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 
 codeCacheDir = context.getCodeCacheDir(); 
 } else { 
 codeCacheDir = context.getCacheDir(); 
 } 
 //硬件加速相关 
 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 
 if (HardwareRenderer.setupDiskCache != null) { 
 HardwareRenderer.setupDiskCache.call(codeCacheDir); 
 } 
 } else { 
 if (ThreadedRenderer.setupDiskCache != null) { 
 ThreadedRenderer.setupDiskCache.call(codeCacheDir); 
 } 
 } 
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 
 if (RenderScriptCacheDir.setupDiskCache != null) { 
 RenderScriptCacheDir.setupDiskCache.call(codeCacheDir); 
 } 
 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 
 if (RenderScript.setupDiskCache != null) { 
 RenderScript.setupDiskCache.call(codeCacheDir); 
 } 
 } 
 //修复一些信息 
 Object boundApp = fixBoundApp(mBoundApplication); 
 mBoundApplication.info = ContextImpl.mPackageInfo.get(context); 
 mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info); 
 VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion); 


 Configuration configuration = context.getResources().getConfiguration(); 
 Object compatInfo = CompatibilityInfo.ctor.newInstance(data.appInfo, configuration.screenLayout, configuration.smallestScreenWidthDp, false); 
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 
 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 
 DisplayAdjustments.setCompatibilityInfo.call(ContextImplKitkat.mDisplayAdjustments.get(context), compatInfo); 
 } 
 DisplayAdjustments.setCompatibilityInfo.call(LoadedApkKitkat.mDisplayAdjustments.get(mBoundApplication.info), compatInfo); 
 } else { 
 CompatibilityInfoHolder.set.call(LoadedApkICS.mCompatibilityInfo.get(mBoundApplication.info), compatInfo); 
 } 
 //这里配置了一个冲突app列表,有可能会延后进行AppInstrumentation的替换 
 boolean conflict = SpecialComponentList.isConflictingInstrumentation(packageName); 
 if (!conflict) { 
 InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class); 
 } 
 //利用LoadedApk构建ClientApp的Application 
 mInitialApplication = LoadedApk.makeApplication.call(data.info, false, null); 
 mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication); 
 ContextFixer.fixContext(mInitialApplication); 
 if (Build.VERSION.SDK_INT >= 24 && "com.tencent.mm:recovery".equals(processName)) { 
 //单独处理微信的一些问题 
 fixWeChatRecovery(mInitialApplication); 
 } 
 if (data.providers != null) { 
 //安装provider 
 installContentProviders(mInitialApplication, data.providers); 
 } 
 if (lock != null) { 
 lock.open(); 
 mTempLock = null; 
 } 
 VirtualCore.get().getComponentDelegate().beforeApplicationCreate(mInitialApplication); 
 try { 
 //ClientApp生命周期正式开始 
 mInstrumentation.callApplicationOnCreate(mInitialApplication); 
 InvocationStubManager.getInstance().checkEnv(HCallbackStub.class); 
 if (conflict) { 
 InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class); 
 } 
 Application createdApp = ActivityThread.mInitialApplication.get(mainThread); 
 if (createdApp != null) { 
 mInitialApplication = createdApp; 
 } 
 } catch (Exception e) { 
 if (!mInstrumentation.onException(mInitialApplication, e)) { 
 throw new RuntimeException( 
 "Unable to create application " + mInitialApplication.getClass().getName() 
 + ": " + e.toString(), e);

 } 
 } 
 //通知完成 
 VActivityManager.get().appDoneExecuting(); 
 VirtualCore.get().getComponentDelegate().afterApplicationCreate(mInitialApplication); 
}

Activity

Activity组件可能是我们最关心的了,运行机制里我们解释过如何欺骗AMS启动未注册在清单中的Activity,Java层hook也提到了startActivity方法是如何hook的,接下来我们直接看具体逻辑,hook代码在ActivityManagerStub中:



static class StartActivity extends MethodProxy {



 private static final String SCHEME_FILE = "file"; 
 private static final String SCHEME_PACKAGE = "package"; 
 private static final String SCHEME_CONTENT = "content"; 


 @Override 
 public String getMethodName() { 
 return "startActivity"; 
 } 


 @Override 
 public Object call(Object who, Method method, Object... args) throws Throwable { 
 int intentIndex = ArrayUtils.indexOfObject(args, Intent.class, 1); 
 if (intentIndex < 0) { 
 return ActivityManagerCompat.START_INTENT_NOT_RESOLVED; 
 } 
 int resultToIndex = ArrayUtils.indexOfObject(args, IBinder.class, 2); 
 String resolvedType = (String) args[intentIndex + 1]; 
 Intent intent = (Intent) args[intentIndex]; 
 intent.setDataAndType(intent.getData(), resolvedType); 
 IBinder resultTo = resultToIndex >= 0 ? (IBinder) args[resultToIndex] : null; 
 int userId = XUserHandle.myUserId(); 


 if (ComponentUtils.isStubComponent(intent)) { 
 return method.invoke(who, args); 
 } 
 
 if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction()) 
 || (Intent.ACTION_VIEW.equals(intent.getAction()) 
 && "application/vnd.android.package-archive".equals(intent.getType()))) { 
 //内部安装拦截自处理,此处代码省略 
 } else if ((Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction()) 
 || Intent.ACTION_DELETE.equals(intent.getAction())) 
 && "package".equals(intent.getScheme())) { 
 //内部卸载拦截自处理,此处代码省略 
 } else if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intent.getAction()) || 
 MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction()) || 
 MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(intent.getAction())) { 
 handleMediaCaptureRequest(intent); 
 } 


 String resultWho = null; 
 int requestCode = 0; 
 Bundle options = ArrayUtils.getFirst(args, Bundle.class); 
 if (resultTo != null) { 
 resultWho = (String) args[resultToIndex + 1]; 
 requestCode = (int) args[resultToIndex + 2]; 
 } 


 if (Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES.equals(intent.getAction())) { 
 //不需要申请安装apk权限,内部应用更新不走系统安装器,此处代码省略 
 } 


 if (BuildCompat.isAndroidLevel18()) { 
 args[intentIndex - 1] = getHostPkg(); 
 } 
 if (intent.getScheme() != null && intent.getScheme().equals(SCHEME_PACKAGE) && intent.getData() != null) { 
 if (intent.getAction() != null && intent.getAction().startsWith("android.settings.")) { 
 intent.setData(Uri.parse("package:" + getHostPkg())); 
 } 
 } 


 ActivityInfo activityInfo = VAppManager.get().resolveActivityInfo(intent, userId); 
 if (activityInfo == null) { 
 VLog.e("VActivityManager", "Unable to resolve activityInfo : %s", intent); 
 if (intent.getPackage() != null && isAppPkg(intent.getPackage())) { 
 return ActivityManagerCompat.START_INTENT_NOT_RESOLVED; 
 } 
 return method.invoke(who, args); 
 } 
 //调用远程VAMS的startActivity方法 
 int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, XUserHandle.myUserId()); 
 if (res != 0 && resultTo != null && requestCode > 0) { 
 VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode); 
 } 
 //处理StubActivity的主题和动画为ClientApp中的目标启动Activity的 
 if (resultTo != null) { 
 ActivityClientRecord r = VActivityManager.get().getActivityRecord(resultTo); 
 if (r != null && r.activity != null) { 
 try { 
 TypedValue out = new TypedValue(); 
 Resources.Theme theme = r.activity.getResources().newTheme(); 
 theme.applyStyle(activityInfo.getThemeResource(), true); 
 if (theme.resolveAttribute(android.R.attr.windowAnimationStyle, out, true)) { 


 TypedArray array = theme.obtainStyledAttributes(out.data, 
 new int[]{ 
 android.R.attr.activityOpenEnterAnimation, 
 android.R.attr.activityOpenExitAnimation 
 });



 r.activity.overridePendingTransition(array.getResourceId(0, 0), array.getResourceId(1, 0)); 
 array.recycle(); 
 } 
 } catch (Throwable e) { 
 // Ignore 
 } 
 } 
 } 
 return res; 
 } 


}

内部安装和卸载,是指如应用宝这样的应用商店安装其他app,或者app自身的升级更新,VA中拦截了该intent自己处理(安装和卸载都在VA内部进行)。

重点关注VActivityManager.get().startActivity这一行,正常的StartActivity方法最终会被引导到VAM,再从p进程到运行在x进程中的远程VAMS中,VAMS再调用自实现ActivityStack的startActivityLocked方法:



int startActivityLocked(int userId, Intent intent, ActivityInfo info, IBinder resultTo, Bundle options,

                        String resultWho, int requestCode) {

    optimizeTasksLocked();



    Intent destIntent;

    ActivityRecord sourceRecord = findActivityByToken(userId, resultTo);

    TaskRecord sourceTask = sourceRecord != null ? sourceRecord.task : null;



    //对启动Flag的处理,此处代码省略

    

    String affinity = ComponentUtils.getTaskAffinity(info);

    TaskRecord reuseTask = null;

    switch (reuseTarget) {

        case AFFINITY:

            reuseTask = findTaskByAffinityLocked(userId, affinity);

            break;

        case DOCUMENT:

            reuseTask = findTaskByIntentLocked(userId, intent);

            break;

        case CURRENT:

            reuseTask = sourceTask;

            break;

        default:

            break;

    }



    boolean taskMarked = false;

    if (reuseTask == null) {

        startActivityInNewTaskLocked(userId, intent, info, options);

    } else {

        boolean delivered = false;

        mAM.moveTaskToFront(reuseTask.taskId, 0);

        boolean startTaskToFront = !clearTask && !clearTop && ComponentUtils.isSameIntent(intent, reuseTask.taskRoot);



        if (clearTarget.deliverIntent || singleTop) {

            taskMarked = markTaskByClearTarget(reuseTask, clearTarget, intent.getComponent());

            ActivityRecord topRecord = topActivityInTask(reuseTask);

            if (clearTop && !singleTop && topRecord != null && taskMarked) {

                topRecord.marked = true;

            }

            // Target activity is on top

            if (topRecord != null && !topRecord.marked && topRecord.component.equals(intent.getComponent())) {

                deliverNewIntentLocked(sourceRecord, topRecord, intent);

                delivered = true;

            }

        }

        if (taskMarked) {

            synchronized (mHistory) {

                scheduleFinishMarkedActivityLocked();

            }

        }

        if (!startTaskToFront) {

            if (!delivered) {

                destIntent = startActivityProcess(userId, sourceRecord, intent, info);

                if (destIntent != null) {

                    startActivityFromSourceTask(reuseTask, destIntent, info, resultWho, requestCode, options);

                }

            }

        }

    }

    return 0;

}

这里其实就是对launch mode以及启动flags等的综合计算,来维护自己的一套Activity栈,然后到了startActivityProcess方法:



private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {

    intent = new Intent(intent);

    //1

    ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);

    if (targetApp == null) {

        return null;

    }

    Intent targetIntent = new Intent();

    //2

    targetIntent.setClassName(VirtualCore.get().getHostPkg(), fetchStubActivity(targetApp.vpid, info));

    ComponentName component = intent.getComponent();

    if (component == null) {

        component = ComponentUtils.toComponentName(info);

    }

    targetIntent.setType(component.flattenToString());

    StubActivityRecord saveInstance = new StubActivityRecord(intent, info,

            sourceRecord != null ? sourceRecord.component : null, userId);

    //3

    saveInstance.saveToIntent(targetIntent);

    return targetIntent;

}
  1. 当要启动一个所在进程并不存在的Activity时,首先得拉起其所在的进程,启动篇我们也分析过了p进程是如何拉起的。
  2. fetchStubActivity会根据当前的情况,根据vpid去清单文件寻找合适的StubActivity,同一进程下的StubActivity信息会被多次复用。
  3. 将ClientAPP要启动的目标Activity的信息保存在intent中,而此时的intent已经被包装为启动StubActivity的intent。

最终调用ActivityStack.realStartActivityLocked:



private void realStartActivityLocked(IBinder resultTo, Intent intent, String resultWho, int requestCode,

                                     Bundle options) {

    Class<?>[] types = mirror.android.app.IActivityManager.startActivity.paramList();

    Object[] args = new Object[types.length];

    if (types[0] == IApplicationThread.TYPE) {

        args[0] = ActivityThread.getApplicationThread.call(VirtualCore.mainThread());

    }

    int intentIndex = ArrayUtils.protoIndexOf(types, Intent.class);

    int resultToIndex = ArrayUtils.protoIndexOf(types, IBinder.class, 2);

    int optionsIndex = ArrayUtils.protoIndexOf(types, Bundle.class);

    int resolvedTypeIndex = intentIndex + 1;

    int resultWhoIndex = resultToIndex + 1;

    int requestCodeIndex = resultToIndex + 2;



    args[intentIndex] = intent;

    args[resultToIndex] = resultTo;

    args[resultWhoIndex] = resultWho;

    args[requestCodeIndex] = requestCode;

    if (optionsIndex != -1) {

        args[optionsIndex] = options;

    }

    args[resolvedTypeIndex] = intent.getType();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {

        args[intentIndex - 1] = VirtualCore.get().getHostPkg();

    }

    ClassUtils.fixArgs(types, args);



    mirror.android.app.IActivityManager.startActivity.call(ActivityManagerNative.getDefault.call(),

            (Object[]) args);

}

经过对参数的替换处理,最终将伪造的StubActivity启动intent交给了系统AMS。

从上面的代码里, 我们也看到了一些ProcessRecord,TaskRecord,ActivityRecord的逻辑,这是因为VA比较完整的仿照android源码自实现了ActivityStack的相关功能,当我们启动一个新进程,新taskAffinity的新Activity,VA中也会新建这些Record存放于内存中进行维护,finish同理。

可以简单看一下数据结构,他们之间也存在相互持有关系,为了方便溯源和处理:

virtualapp源码分析_virtualapp商业版

至此启动Intent的伪造包装完毕,系统AMS接收到的也是StubActivity的相关信息,经过AMS的各种处理,消息又回到了Client端主线程的mH Handler中,由Instrumentation实例化目标Activity,VA选择在在这个时机拦截消息,恢复Intent:

virtualapp源码分析_virtualapp商业版

处理逻辑来到handleLaunchActivity:

private boolean handleLaunchActivity(Message msg) {

    Object r = msg.obj;

    //StubActivity的intent

    Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);

    //StubActivityRecord中会获取之前保留的参数,包含原版intent信息

    StubActivityRecord saveInstance = new StubActivityRecord(stubIntent);

    if (saveInstance.intent == null) {

        return true;

    }

    //原版目标intent

    Intent intent = saveInstance.intent;

    ComponentName caller = saveInstance.caller;

    IBinder token = ActivityThread.ActivityClientRecord.token.get(r);

    ActivityInfo info = saveInstance.info;

    //token为空,则需要拉起组件对应的进程,可参考启动篇p进程的拉起

    if (VClientImpl.get().getToken() == null) {

        InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);

        if (installedAppInfo == null) {

            return true;

        }

        VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId);

        getH().sendMessageAtFrontOfQueue(Message.obtain(msg));

        return false;

    }

    if (!VClientImpl.get().isBound()) {

        VClientImpl.get().bindApplication(info.packageName, info.processName);

        getH().sendMessageAtFrontOfQueue(Message.obtain(msg));

        return false;

    }

    int taskId = IActivityManager.getTaskForActivity.call(

            ActivityManagerNative.getDefault.call(),

            token,

            false

    );

    //通知VAMS创建完成

    VActivityManager.get().onActivityCreate(ComponentUtils.toComponentName(info), caller, token, info, intent, ComponentUtils.getTaskAffinity(info), taskId, info.launchMode, info.flags);

    ClassLoader appClassLoader = VClientImpl.get().getClassLoader(info.applicationInfo);

    intent.setExtrasClassLoader(appClassLoader);

    //真正替换intent信息的地方,此时intent信息为原版目标信息,又交还给了系统处理

    ActivityThread.ActivityClientRecord.intent.set(r, intent);

    ActivityThread.ActivityClientRecord.activityInfo.set(r, info);

    return true;

}

系统拿到intent之后,所实例化的也就变成了ClientApp中的目标Activity。

最后一个hook点是在Instrumentation.callActivityOnCreate中,此时Activity已经被new出来,VA需要在这个时机恢复真正的主题和屏幕方向,以及修复一些问题。

不过,在android9.0以后,Activity的启动流程发生了一些变化,而VA的开源代码中还没有相关的适配,我们可以简单描述一下处理办法:还是可以通过mH的消息机制来进行拦截,只不过拦截的消息从LAUNCH_ACTIVITY变成了EXECUTE_TRANSACTION,替换intent的地方由ActivityClientRecord变成了LaunchActivityItem,尽管流程上的变化略大,但是应对方式却没有那么复杂,只需要找准hook点即可。

其他三大组件

除去Activity,其他三大组件也各自利用一些技巧规避了问题,因篇幅有限,我们不再做具体的代码分析,只简述一下实现原理,有兴趣的同学可以继续深入了解。

ContentProvider

相比较于StubActivity的占位作用,StubContentProvider却并不是为了占位,其作用是为了调用时可以带起p进程,而在进程拉起后,会进行application的初始化,bindApplicationNoCheck方法中会真正对Client App中的ContentProvider进行安装注册。

当进程A调用进程B(或应用B)中的ContentProvider时,会hook进程A中的getContentProvider方法,判断目标Provider所在进程B是否存在,如果不存在则拉起,这样进程B的Provider安装完毕,返回进程A所需要的目标Provider句柄,完成调用。

Service

由于Service并不像Activity那样有交互有页面,它的生命周期非常简单,并且ClientApp中的Service并不需要暴露给外部(指沙盒外)App使用,因此在VA中,Service不需要让AMS等系统服务知晓。

通过hook startService方法将逻辑直接引导到VAMS中,利用ApplicationThread.scheduleCreateService对目标Service直接进行创建(如果目标进程不存在则拉起进程),如果是bindService则使用ApplicationThread.scheduleBindService等待bind完成。

简而言之就是Client App中Service的创建运行过程,对系统服务屏蔽,利用mirror手段直接调用其生命周期。

BroadcastReceiver

由于广播分静态注册和动态注册,而ClientApp中的静态广播无法被系统AMS所知晓,因此VA使用动态注册来代替静态注册。

当VA Service进程拉起时,VA对所有已安装的APP进行扫描,遍历其所有的Reveiver信息,通过新建StaticBroadcastReceiver代理来接收每个IntentFilter。

然后hook了ClientApp中的broadcastIntent方法,这里其实也做了一个intent包裹,和Activity逻辑类似,当代理广播StaticBroadcastReceiver接收到包装后的intent信息时,解包出真正的intent,回调到ClientApp空间,实例化目标 Receiver(如果目标进程不存在则拉起进程),最终手动进行对其OnCreate和finish的调用。

实际上,各种插件化框架对于四大组件的处理是大同小异的,仅仅是hook点,hook时机略有不同而已。

结语

VA的基本原理,代码层面到此基本结束,剩下的是不断的堆砌完善工作,而基于VA后续也有非常多的拓展,作者有句话说的很好:VA怎么用,一切取决于你的想象力。

如果你对它的其他能力感兴趣,可以看看这篇文章:VirtualApp技术黑产利用研究报告

如果你对免root实现xposed,或者一些基于VA的二次开发项目感兴趣,可以继续了解以下项目:

github.com/android-hac…

github.com/asLody/Sand…

github.com/WindySha/Xp…

github.com/android-hac…

www.taichi-app.com/#/index

笔者在写下此篇时也是依据于当前的理解来分析,如果有不对的地方欢迎指正,或者有兴趣的同学也欢迎一起来交流,感谢大家!

参考文档

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13499.html

(0)

相关推荐

  • 用len函数评估Python代码的效率

    用len函数评估Python代码的效率Python作为一门高级编程语言,其代码简洁、易读、易懂的特点成为了众多开发者的首选。但是,在实际的应用过程中,我们也需要考虑代码的效率。本文将从多个方面探讨如何使用len函数评估Python代码的效率,为Python开发者提供一些有效的优化方案。

    2024-04-15
    79
  • 2020重新出发,MySql基础,MySql的约束[通俗易懂]

    2020重新出发,MySql基础,MySql的约束[通俗易懂]
    @ MySQL约束概述 MySQL约束是一种限制,它通过限制表中的数据,来确保数据的完整性和唯一性。使用约束来限定表中的数据是很有必要的。 在 MySQL …

    2023-04-05
    150
  • mysql -h -u_MySQL date

    mysql -h -u_MySQL date在mysql中,hint指的是“查询优化提示”,会提示优化器按照一定的方式来生成执行计划进行优化,让用户的sql语句更具灵活性;Hint可基于表的连接顺序、方法、访问路径、并行度等规则对DML(数据操

    2023-06-16
    155
  • dnf2015劳动节礼包(dnf2019国庆礼包)

    dnf2015劳动节礼包(dnf2019国庆礼包)

    2023-09-14
    183
  • 学习Python: 快速掌握编程基础,提高数据分析能力

    学习Python: 快速掌握编程基础,提高数据分析能力Python 这门语言被广泛应用在科学计算、数据分析、人工智能等领域,得益于其易于学习、易于阅读的语法和开源社区的支持。学习 Python 不仅能让你掌握编程基础,更可以提高数据分析能力,这也是本文的重点和主题。

    2024-04-02
    80
  • Python计算时间差

    Python计算时间差在Python编程中,我们通常需要计算时间差。时间差就是在两个时间点之间相隔的时间。比如我们想要知道两个事件发生的时间差,或者我们需要计算程序运行的时间等等,这时就需要用到Python的时间模块。

    2024-04-24
    49
  • 从架构入手轻松读懂框架源码:以jQuery,Zepto,Vue和lodash-es为例[亲测有效]

    从架构入手轻松读懂框架源码:以jQuery,Zepto,Vue和lodash-es为例[亲测有效]不知道有没有朋友有这种经历。雄心勃勃的打开一个开源框架的源码,开始看,一行一行的看下去,看了几行就感觉,“我艹,这什么玩意儿”,然后就看不下去了。如果你有类似的经历,可以看看本文,本文会以几个常见开源库为例讲解几种常见的开源框架的代码架构,从架构出发,帮你轻松读懂框架源码。记住…

    2023-08-02
    128
  • Oracle入门学习四

    Oracle入门学习四上一篇:Oracle入门学习三 学习视频:https://www.bilibili.com/video/BV1tJ411r7EC?p=35 Oracle表连接:内连接、外连接。外连接分左连接、右连接。

    2023-02-24
    174

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注