大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说virtualapp源码分析_virtualapp商业版,希望您对编程的造诣更进一步.
作者:字节游戏中台客户端团队 – 聂伟
背景
VirtualApp(以下简称VA)是一款运行在android系统中的沙盒(或者叫轻量andorid虚拟机)产品。
VA相关的功能可以通过github介绍来了解,我们本篇主要就VA的实现原理来分析,源码虽然枯燥,却有很多闪光点值得我们学习。
运行机制
首先来看看VA环境下的运作方式(引自网图):
VA一共会运行在三种进程之中:
- 宿主进程,即VA自身的主进程,子进程
- Client App进程,即在VA中运行的各种App进程
- 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不影响宿主进程的运行。
当然,大体原理是如此,实际情况会更复杂一些,接下来我们看看代码包结构:
代码包结构
- android:通过建立和android相同的目录来引用一些系统隐藏类,达到欺骗编译器的目的,如android.content.pm.PackageParser。
- client:Client App所运行的环境,包含大量的hook代码。
- server:VA Server进程相关,仿造了原生framework services部分功能。
- mirror:android系统类的镜像包,和系统类同名,封装了反射过程,可以很便捷地直接调用系统类的一些字段和方法。
- jni:native hook相关,主要是虚拟机的hook和io重定向(将Client App中的io路径重定向到VA内部)。
其他如os是处理一些环境问题以及多用户的管理,remote是用于aidl传输的各种序列化bean。
从运行机制和代码包结构中,可以看到VA基本的轮廓,下面我们逐一进行分析。
源码分析
mirror
假设我们想hook掉Instrumentation,首先来看看普通的hook实现:
然后是mirror实现:
可以看到普通的实现方式不仅繁琐,而且有大量的模板代码,重复工作,而mirror大大简化了这个过程,仿佛就像是在直接调用系统方法一样。
我们看看mirror下的ActivityThread定义:
所有使用的原ActivityThread的相关成员变量/方法,都以:
public static Ref/RefStatic+属性类型 同名成员变量名/方法名
的方式声明在了mirror.ActivityThread当中,并且在RefClass.load方法中传入当前Class对象和真正的className:
load方法封装了反射获取字段的逻辑,映射同名的属性到mirror类下(正如mirror的英译:镜像),其中Ref/RefStatic+属性类型内部也有同名filed的赋值逻辑,不再赘述。
mirror以简洁而又优雅的设计大大简化了反射注入的成本,使用过程中只需要声明属性即可,而不需要关心其反射细节,使用简单,原理简单。
Java层hook
运行机制有说到,VA会hook原生的AM,来截断部分方法调用到VAM,VAM再去调用自实现的VAMS。我们简单以startActivity方法示例:
原生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的全貌:
- VA中实现了大量了Stub来hook各种系统服务(proxies包),并最终添加到InvocationStubManager中统一进行inject调用。
- 很多场景下的方法只需要替换固定位置的参数(如包名,uid),VA为此提供了数个可以快捷hook方法的衍生类,如ReplaceCallingPkgXXX,只需要简单继承即可。
- 封装的思路并不难理解,难点在于如何找到hook点(并且还存在android多版本差异的兼容问题),VA几乎接管了整个framework层,任何没有hook到的地方都有可能引发crash,这也是其稳定性比较难做的原因之一。
我们通过下面的图,可以简单了解到大部分系统服务hook所需要做的工作(大多方法只需要替换包名即可):
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库函数,修改相关函数的输入参数,我们直接看具体的逻辑:
该方法会在Clint App初始化其application时调用,通过redirectDirectory来添加重定向前后的目录,这里可以看到一部分是data/data为首的原始目录,还有一部分是外置存储的各种目录(VA完全按照其规则在内部也创建了相同的目录结构)。
然后通过NativeEngine.enableIORedirect()来调用到IOUniformer.cpp中的startUniformer方法:
可以看下首个方法faccessat的替换方法声明:
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:
这里userId存在一个复用逻辑,比如已经安装了同一个app四次,userId分别为0,1,2,3,然后卸载了2,再重新安装会优先复用2(如果已使用就继续递增),最终通过VAppManagerService的installPackageAsUser方法,为该app添加一个userId记录并保存到磁盘即可。
我们安装oppo应用商店两次,来看看安装后的数据目录:
- 拷贝该app的所有so包,如果直接安装apk还会在目录下存放拷贝的base.apk。
- package和签名信息序列化后保存的文件,包含该app的所有组件信息。
- 用户体系,userlist中保存了所有的userId信息,默认user为0。
- dex转二进制存放的目录,以及外置存储目录(会通过io重定向将app对外置存储的操作重定向到此目录中)。
启动
运行机制里说过,VA一共运行在三种进程中(最新的介绍中又添加了64位支持的插件进程),宿主进程自不必说,我们主要看看VA Server进程和Client App进程是如何运行起来的:
清单文件中可以看到prcess定义了两种进程,x进程(即VA Server进程)和p进程(即Client App进程),p进程命名为p0,p1,p2等等,这是因为我们可能会运行多个app,或者不同进程的组件,在Client App启动以后,VA会将p进程修改为Client App真正进程的名字。
而每次新进程的创建,意味着Application的重新初始化:
VApp的多次初始化都会调用VirtualCore的startup方法,其中InvocationStubManager通过injectAll添加所有hook类,完成了java层hook的注入:
那么,x进程是如果拉起的呢?这里VA比较巧妙地利用了Provider的call方法自动拉起进程的机制,来看看BinderProvider:
我们发现BinderProvider中添加并初始化了所有的VA service(实际最终添加到ServiceFetcher中),并在call方法中put了ServiceFetcher这个IBinder句柄,ServiceFetcher正是Client端获取VA Service的钥匙。
最后在Client端中可以进行调用获取:
- 通过Provider的call方法,获取BinderProvider返回的Bundle,而BinderProvider在清单文件中prcess定义正是:x进程,如果进程未拉起,此时默认会拉起x进程,所有的VA Service运行在x进程之上。
- 拿到Bundler以后,就可以获取BinderProvider中put进去的Binder,正是ServiceFetcher。
- ServiceFetcher管理着所有的VA Service,通过ServiceFetcher,即是拿到了所有VA Services的调用权。
p进程也是同理,当我们想要启动Client App中的某个四大组件,发现组件所在的进程不存在,那么首先肯定是拉起进程:
这里也初始化了本地的一个VClientImpl,记录了token和vuid,并将Client打包到Bundle以便调用Client端的方法,最终同样也是通过Provider.call的方式进行唤起:
其中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;
}
- 当要启动一个所在进程并不存在的Activity时,首先得拉起其所在的进程,启动篇我们也分析过了p进程是如何拉起的。
- fetchStubActivity会根据当前的情况,根据vpid去清单文件寻找合适的StubActivity,同一进程下的StubActivity信息会被多次复用。
- 将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同理。
可以简单看一下数据结构,他们之间也存在相互持有关系,为了方便溯源和处理:
至此启动Intent的伪造包装完毕,系统AMS接收到的也是StubActivity的相关信息,经过AMS的各种处理,消息又回到了Client端主线程的mH Handler中,由Instrumentation实例化目标Activity,VA选择在在这个时机拦截消息,恢复Intent:
处理逻辑来到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的二次开发项目感兴趣,可以继续了解以下项目:
笔者在写下此篇时也是依据于当前的理解来分析,如果有不对的地方欢迎指正,或者有兴趣的同学也欢迎一起来交流,感谢大家!
参考文档
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13499.html