Android 双开沙箱 VirtualApp 源码分析(二)

VirtualApp源码分析——(一)VA启动 VirtualApp(以下简称VA)是一个开源的Android app虚拟化引擎,它提供了一个虚拟空间,允许其他应用运行在这个虚拟空间中,实现应用多开(可重复安装)。 在VA中,host应用(VA自身)和client应用(通过VA安装的应用)具有相同的uid。 在运行时,一般会包含三个进程: io.virtualapp,主进程,负责VA的用户界面及应用管理; io.virtualapp:... 阅读详情

Android 双开沙箱 VirtualApp 源码分析(二)

VA 初始化

先看一下代码:
VirtualCore.startup

public void startup(Context context) throws Throwable {
        if (!isStartUp) {
            // 确保 MainThread
            if (Looper.myLooper() != Looper.getMainLooper()) {
                throw new IllegalStateException("VirtualCore.startup() must called in main thread.");
            }
            VASettings.STUB_CP_AUTHORITY = context.getPackageName() + "." + VASettings.STUB_DEF_AUTHORITY;
            ServiceManagerNative.SERVICE_CP_AUTH = context.getPackageName() + "." + ServiceManagerNative.SERVICE_DEF_AUTH;
            this.context = context;
            // 获取 ActivityThread 实例
            mainThread = ActivityThread.currentActivityThread.call();
            unHookPackageManager = context.getPackageManager();
            hostPkgInfo = unHookPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
            detectProcessType();
            // hook 系统类
            InvocationStubManager invocationStubManager = InvocationStubManager.getInstance();
            invocationStubManager.init();
            invocationStubManager.injectAll();
            // 修复权限管理
            ContextFixer.fixContext(context);
            isStartUp = true;
            if (initLock != null) {
                initLock.open();
                initLock = null;
            }
        }
    }

整个 VA 会运行在四种进程

private enum ProcessType {
        /**
         * Server process
         */
        Server,
        /**
         * Virtual app process
         */
        VAppClient,
        /**
         * Main process
         */
        Main,
        /**
         * Child process
         */
        CHILD
    }

分别是前面提到的 VAService 进程,Client App 进程,VA 自身的 App 主进程,子进程。
这样的话,Application 就会被初始化多次,所以要在初始化的时候根据进程类型有选择的做对应的初始化工作。

InvocationStubManager.injectInternal():
主要完成对 Java 层 framework 的 Hook,将其定位到 VA 伪造 VA framework 上去。

private void injectInternal() throws Throwable {
        // VA 自身的 App 进程不需要 Hook
        if (VirtualCore.get().isMainProcess()) {
            return;
        }
        // VAService 需要 Hook AMS 和 PMS
        if (VirtualCore.get().isServerProcess()) {
            addInjector(new ActivityManagerStub());
            addInjector(new PackageManagerStub());
            return;
        }
        // Client APP 需要 Hook 整个 framework,来使其调用到 VA framework
        if (VirtualCore.get().isVAppProcess()) {
            addInjector(new LibCoreStub());
            addInjector(new ActivityManagerStub());
            addInjector(new PackageManagerStub());
            addInjector(HCallbackStub.getDefault());
            addInjector(new ISmsStub());
            addInjector(new ISubStub());
            addInjector(new DropBoxManagerStub());
            .....................

VA 初始化主要就是这些

Client App 的安装

VirtualCore.installPackage

 public InstallResult installPackage(String apkPath, int flags) {
        try {
            // 调用远程 VAService
            return getService().installPackage(apkPath, flags);
        } catch (RemoteException e) {
            return VirtualRuntime.crash(e);
        }
    }

最终调用 VAServcie 中的 VAppManagerService.installPackage():

// 安装 apk 先于 installPackageAsUser,主要目的是生成 VPackage 结构
    public synchronized InstallResult installPackage(String path, int flags, boolean notify) {
        long installTime = System.currentTimeMillis();
        if (path == null) {
            return InstallResult.makeFailure("path = NULL");
        }
        // 是否 OPT 优化(dex -> binary)
        boolean skipDexOpt = (flags & InstallStrategy.SKIP_DEX_OPT) != 0;
        // apk path
        File packageFile = new File(path);
        if (!packageFile.exists() || !packageFile.isFile()) {
            return InstallResult.makeFailure("Package File is not exist.");
        }
        VPackage pkg = null;
        try {
            // 进入解析包结构,该结构是可序列化的,为了持久化在磁盘上
            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;
        // PackageCache holds all packages, try to check if we need to update.
        VPackage existOne = PackageCacheManager.get(pkg.packageName);
        PackageSetting existSetting = existOne != null ? (PackageSetting) existOne.mExtras : null;
        if (existOne != null) {
            if ((flags & InstallStrategy.IGNORE_NEW_VERSION) != 0) {
                res.isUpdate = true;
                return res;
            }
            if (!canUpdate(existOne, pkg, flags)) {
                return InstallResult.makeFailure("Not allowed to update the package.");
            }
            res.isUpdate = true;
        }
        // 获得 app 安装文件夹
        File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName);
        // so 文件夹
        File libDir = new File(appDir, "lib");
        if (res.isUpdate) {
            FileUtils.deleteDir(libDir);
            VEnvironment.getOdexFile(pkg.packageName).delete();
            VActivityManagerService.get().killAppByPkg(pkg.packageName, VUserHandle.USER_ALL);
        }
        if (!libDir.exists() && !libDir.mkdirs()) {
            return InstallResult.makeFailure("Unable to create lib dir.");
        }

        // 是否基于系统的 apk 加载,前提是安装过的 apk 并且 dependSystem 开关打开
        boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0
                && VirtualCore.get().isOutsideInstalled(pkg.packageName);

        if (existSetting != null && existSetting.dependSystem) {
            dependSystem = false;
        }
        // 复制 so 到 sandbox lib
        NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);

        // 如果不基于系统,一些必要的拷贝工作
        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);
        }

        // 给上可执行权限,5.0 之后在 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);
            }
        }
        //保存 VPackage Cache 到 Disk
        PackageParserEx.savePackageCache(pkg);
        //保存到 RamCache
        PackageCacheManager.put(pkg, ps);
        mPersistenceLayer.save();
        BroadcastSystem.get().startApp(pkg);
        //发送通知 安装完成
        if (notify) {
            notifyAppInstalled(ps, -1);
        }
        res.isSuccess = true;
        return res;
    }

APk 的安装主要完成以下几件事情:

  1. 解析 menifest 拿到 apk 内部信息,包括组件信息,权限信息等。并将这些信息序列化到磁盘和内存中,以备打开时调用。
  2. 准备 App 在 VA 沙箱环境中的私有空间,并且复制一些必要的 apk 和 so libs。
  3. 最后通知前台安装完成。

解析 apk menifest:PackageParserEx.parsePackage():

 // 解析包结构
    public static VPackage parsePackage(File packageFile) throws Throwable {
        PackageParser parser = PackageParserCompat.createParser(packageFile);
        // 调用对应系统版本的 parsePackage 方法
        PackageParser.Package p = PackageParserCompat.parsePackage(parser, packageFile, 0);
        // 包含此信息代表其是 share lib
        if (p.requestedPermissions.contains("android.permission.FAKE_PACKAGE_SIGNATURE")
                && p.mAppMetaData != null
                && p.mAppMetaData.containsKey("fake-signature")) {
            String sig = p.mAppMetaData.getString("fake-signature");
            p.mSignatures = new Signature[]{new Signature(sig)};
            VLog.d(TAG, "Using fake-signature feature on : " + p.packageName);
        } else {
            // 验证签名
            PackageParserCompat.collectCertificates(parser, p, PackageParser.PARSE_IS_SYSTEM);
        }
        // 转换成可以序列化在磁盘上的 Cache
        return buildPackageCache(p);
    }

这里解析 Menifest 的方法其实是调用了 framework 隐藏方法 android.content.pm.PackageParser.parsePackage 来实现的,这个方法返回 android.content.pm.Package 结构,这个类型也是隐藏的,怎么办?可以从 sdk 中复制这个类到自己的项目中欺骗编译器。这就是上一章一开始提到的。

这里还有一个问题,就是 Package 类是不可序列化的,换句话说就是不能直接保存在磁盘上,我们需要将其转换成可以序列化的 VPackage 类型,这就是 buildPackageCache() 的作用。

VPackage:

public class VPackage implements Parcelable {

    public static final Creator<VPackage> CREATOR = new Creator<VPackage>() {
        @Override
        public VPackage createFromParcel(Parcel source) {
            return new VPackage(source);
        }

        @Override
        public VPackage[] newArray(int size) {
            return new VPackage[size];
        }
    };
    public ArrayList<ActivityComponent> activities;
    public ArrayList<ActivityComponent> receivers;
    public ArrayList<ProviderComponent> providers;
    public ArrayList<ServiceComponent> services;
    public ArrayList<InstrumentationComponent> instrumentation;
    public ArrayList<PermissionComponent> permissions;
    public ArrayList<PermissionGroupComponent> permissionGroups;
    public ArrayList<String> requestedPermissions;
    public ArrayList<String> protectedBroadcasts;
    public ApplicationInfo applicationInfo;
    public Signature[] mSignatures;
    public Bundle mAppMetaData;
    public String packageName;
    public int mPreferredOrder;
    public String mVersionName;
    public String mSharedUserId;
    public ArrayList<String> usesLibraries;
    public int mVersionCode;
    public int mSharedUserLabel;
    // Applications hardware preferences
    public ArrayList<ConfigurationInfo> configPreferences = null;
    // Applications requested features
    public ArrayList<FeatureInfo> reqFeatures = null;
    public Object mExtras;

可以看到 VPackage 几乎保存了 apk 中所有的关键信息,尤其是组件的数据结构会在 app 在 VA 中运行的时候给 VAMS,VPMS 这些 VAService 提供 apk 的组件信息。

关于是否 dependSystem 和 isInstallOutside,这个有关 apk 的动态加载,如果 dependSysytem 并且 apk 已经在外部环境安装了,那么 VA 会调用系统提供的 API 就可以动态加载 APK。反之 VA 需要做一些必要的复制工作然后再费劲的去加载 APK。

作者:OSTCB
来源:优快云
原文:https://blog.youkuaiyun.com/ganyao939543405/article/details/76150725

Android虚拟化技术方案技术评测 完成虚拟化一个应用,要做的核心事情只有一件:在运行子应用时,欺骗系统让系统误以为正在运行的是宿主,否则系统会毫不犹豫的将你杀死!! 阅读详情

相关推荐

VirtualApp源码(完整工程)

VirtualApp源码(完整工程),基于android studio (ndk,sdk)开发,实现安卓双开。

Android逆向学习(二)vscode进行双开与图标修改

双开就是在一个android机器中同时安装和运行两个相同的软件,实现双开的方法有很多,这里我们就讲最简单的一个就是修改包名,让系统认为这是两个不同的软件,这个的实现方法也很简单,我们只需要修改相应文件下的包名就可以了。

qq_52380836的博客 3059

Excel炒股党必备:手把手教你用Power Query免费获取并刷新股票历史数据

本文详细介绍了如何利用Excel中的Power Query工具免费获取并自动刷新股票历史数据,特别适合Excel炒股党。从环境配置、数据源选择到实战操作,手把手教你建立智能数据管道,实现高效投资分析。通过参数化股票代码和定时刷新功能,大幅提升数据更新效率。

weixin_26767391的博客 654

【使用VirtualApp无root对手游进行修改】

使用VirtualApp对手游进行修改

qq_45071266的博客 2929

Python批量注册脚本开发详细

开发一个Python批量注册脚本需要考虑多个方面,包括目标网站的注册流程、验证码处理、请求发送、数据存储等。以下是一个详细的开发流程和代码示例,用于实现基本的批量注册功能。

nsp202505的博客 3345

RTX 50系显卡实战3DGS:从源码编译到避坑指南

本文详细介绍了在RTX 50系显卡上成功运行3D Gaussian Splatting(3DGS)的完整实战指南。针对新显卡架构与旧版预编译包不兼容的核心问题,文章提供了从源码编译CUDA扩展的清晰步骤,包括环境配置、依赖安装、核心模块diff-gaussian-rasterization和simple-knn的编译避坑方法,并涵盖了数据准备、模型训练与实时预览的全流程,帮助开发者高效利用新硬件进行3D重建。

weixin_28314575的博客 263

Android 双开沙箱 VirtualApp 源码分析(三)App 启动

上一章:Android 双开沙箱 VirtualApp 源码分析(二) 在这之前,我们还是要先了解一下 VA Client Framework 和 VAService 之间的通讯方式VAService 与通讯VAService首先,VAService 是指 VA 仿造 Android 原生 framework 层 Service 实现的一套副本,举例有 VActivityManagerServic

ganyao939543405的博客 1万+

AgentScope Java多智能体框架

AgentScope Java 的核心设计理念是“Agent-Oriented Programming” (面向智能体编程)。

zzz19920821的博客 1734

Android 双开沙箱 VirtualApp 源码分析(一)

欢迎使用优快云-markdown编辑器 最近发现了一个非常好的开源项目,基本实现了一个 Android 上的沙箱环境,不过应用场景最多的还是应用双开。 VA github: https://github.com/asLody/VirtualApp VA 的源码注释: https://github.com/ganyao114/VA_Doc 第一章主要是分析一下项目的整体结构。 包结构 andro...

CoderAldrich的专栏 1961

C++ Qt6 QtQuick/QML入门进阶与项目实战视频教程

C++ Qt这些年在PC客户端、嵌入式、汽车座舱仪表等领域应用很广泛,例如剪映专业版、微信4.0、亿图脑图、Steam、美图秀秀、腾讯会议、钉钉(部分模块)等都是使用C++ QWidget/QML开发。特别是QML这种声明式UI开发更加快捷,QML的界面开发效率相对于QWidget更快速,界面效果更好,QtQuick/QML在Qt6版本也逐渐完善了,使用QML进行客户端开发是个不错的选择。

琅嬛福地 4487

Python爬虫实战:爬取国家企业信用信息公示系统(最新反反爬技术解析)

在当今大数据时代,企业信用数据对于市场分析、风险评估和商业决策具有重要价值。国家企业信用信息公示系统作为官方权威的企业信息平台,包含了全国各类企业的注册信息、行政处罚、经营异常等关键数据。复杂的反爬机制(动态Token、验证码、请求频率限制)数据渲染方式多样(部分内容由JavaScript动态加载)法律与合规风险(需遵守《网络安全法》和网站Robots协议)本文将详细介绍如何使用Python最新技术栈构建一个高效、稳定的企业信用信息爬虫,并重点解析应对各种反爬策略的解决方案。

2201_76125261的博客 2695

Android 双开沙箱 VirtualApp 源码分析(六)ContentProvider

Android 双开沙箱 VirtualApp 源码分析(六)ContentProvider Provider 注册 回顾前面,Activity 启动的时候会检查 Application 是否初始化,会调用 bindApplication,里面执行了安装 Provider 的方法: private void installContentProviders(Context app, List<...

CoderAldrich的专栏 616

【WRF理论第十四期】 VEGPARM.TBL 文件详解

WRF 读取 VEGPARM.TBL 文件

WW、forever的博客 575

Android 双开沙箱 VirtualApp 源码分析(五)BroadcastReceiver

Android 双开沙箱 VirtualApp 源码分析(五)BroadcastReceiver 方案猜测 同 Activity 一样,Client App 在 Menifest 中注册的静态广播外部 AMS 是无法知晓的,经过前几章的分析,相信大家已经是老司机了,我们可以先尝试提出自己的观点。 和 Activity 一样使用 Stub 组件占坑?仔细想一想是无法实现的,因为你无法预先确定 Cl...

CoderAldrich的专栏 657

【银河麒麟服务器操作系统】安装英伟达4090D显卡驱动操作指南

本文详细介绍了在银河麒麟高级服务器操作系统V10SP3-2403-X86版本上安装NVIDIA RTX4090D显卡驱动的完整流程。主要内容包括:准备工作检查、开源驱动禁用、运行级别切换、依赖包安装以及交互式安装过程中的关键选项选择(32位库选Yes、X配置选No)。通过禁用nouveau驱动、重建内核引导等步骤确保驱动安装成功,最后通过nvidia-smi命令验证驱动安装情况。文档提供了操作检查表和安装流程图,帮助用户在银河麒麟系统上顺利完成显卡驱动部署。

信创运维工程师 · 银河麒麟 & 容器实战 | 金融生产环境迁移·等保加固 | 142篇原创·17万+阅读 3098

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Android 双开沙箱 VirtualApp 源码分析(四)启动插件 Service

Android 双开沙箱 VirtualApp 源码分析(四)启动插件 Service 原生 Service 创建过程 首先有必要了解一下原生 framework 对 Service 的创建,因为在 VA 中启动 Service 和 Activity 有很大的区别。 首先入口 ContextWrapper.startService(): @Override public Component...

CoderAldrich的专栏 445

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Android adb指令实现应用双开功能

android adb 应用双开

wangjicong_215的博客 4517

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

【Android 插件化】VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )

一、目前的 API 现状、 二、安装应用源码分析、 1、安装按钮执行的操作、 2、返回到 HomeActivity 执行的操作、

让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 ) 1946

【Android 插件化】VirtualApp 源码分析 ( 安装应用源码分析 | HomePresenterImpl 添加应用 | AppRepository.addVirtualApp )

一、安装应用源码分析、 1、HomePresenterImpl 添加应用、 2、AppRepository.addVirtualApp 安装 SD 卡 APK 应用、

让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 ) 1409
上一篇: Android 双开沙箱 VirtualApp 源码分析(一)
下一篇: Android 双开沙箱 VirtualApp 源码分析(三)App 启动
CoderAldrich
博客等级 码龄12年 431粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值