0. 背景
目前市面上越来越多通过虚拟化多开的应用或者开源项目,包括
- 平行空间
- VirtualApp
- 双开助手
- DualSpace
- Go双开
- 双开精灵
其中VirtualApp是一个个人开源的,直接点击可以跳转到GitHub页面。这些虚拟化方案都已经非常成熟并且许多经过了市场的检验。
但是很多敏感的应用,比如支付,社交,金融并不想让自己运行在虚拟环境中,因为在虚拟环境中很容易进行伪造设备,进行注入,进行调试等操作。因此如何检测出应用自身是否存在于多开环境下就有应用的环境了,在检测出处于自己虚拟环境下时可以提醒用户风险,直接关闭应用或者上报服务器进行风险控制。
1. 多开基本原理
其实除了VirtualApp我粗略了解过他一部分实现以及看过一些分析文章以外,其他虚拟化应用或者框架我都没有仔细了解过,但是根据我尝试测试以及查看他们的表现形式之后,发现他们的实现原理都是类似的,通过在宿主容器上面新建一个进程供插件APK寄宿,然后通过hook一些系统接口欺骗应用---让虚拟化后应用以为自己是正常运行的独立APP,欺骗系统---让系统认为此虚拟化应用是一个已正常安装在系统的应用。
我们要做的是,如何在虚拟化的应用内部判断自己是处于虚拟化环境的。
-
尝试直接获取已安装应用,判断是否有某些多开应用的包名,发现有几个虚拟化应用居然机智的把自己屏蔽掉,导致在虚拟化环境下无法获取到宿主应用的信息。该方法不可行。
-
尝试在宿主应用私有目录“/data/data/pkg“下创建和删除文件,一般来说如果应用是没有被虚拟化运行的话,是没有办法访问宿主下的私有目录的,只有在uid一致的情况下才有权限访问,如果能创建和删除成功证明应用被虚拟化并且拥有和宿主一样的uid,而uid和应用是一一对应的,因此可以反推出此应用是被虚拟化出来的。本来以为这个方法是比较靠谱的,虽然要hardcode并查找出市面上大部分的虚拟化应用的包名,但至少可行。但是发现部分虚拟化框架居然连文件访问都做了保护,导致虚拟化出来的应用无法访问宿主的目录。该方法也不可行。
进行当然不止上面的两个尝试,有些已经不太记得了,但是第二个尝试的描述还是有价值的,下面将描述一个可行的方案。
2. 检测的方式
通过上面的描述,可以发现uid是一个虚拟化无法绕过的东西,不同于传统linux上的含义,因为Android是单用户系统,因此传统的uid失去了原来的意义,但是Android巧妙的修改了uid的含义:uid是系统分配的一个应用的标志,每一个APP对应一个uid。因为虚拟化并没有真正的安装应用,因此uid必定是和宿主一致的。
接下来就是在uid上面做文章了,根据上面的第二个尝试的描述,虚拟化将文件访问也做了虚拟化,因此虚拟化后的应用必定是有自己的"/data/data/pkg"私有目录的。那么这样就有一个冲突的地方了,"/data/data"目录下会为已安装的应用创建一个私有目录,并且只创建一个。那么如果一个uid下面有两个进程,进程名对应包名,而包名对应的"/data/data"私有目录有两个可以访问,那是不是就和只为一个应用创建唯一一个私有目录相冲突了。
如下图所示,ps,过滤uid之后得到的结果:
![](https://img.haomeiwen.com/i7164595/fd7430368dedc921.png)
可以看到com.lbe.parallel和cn.zarathustra.checkvirtualapk这两个应用的包名,但是他们的uid都为u0_a541。
总结一下,我们的检测方法就是----如果满足同一uid下的两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了。
3. 实现
在我的项目里面是用C实现的,主要是为了执行效率和代码安全考虑,但是在写总结的时候发现其实在java层也完全可以实现,为了方便各位使用和接入,不用特地拷贝一个so或者弄一个Native层,因此用Java重写了一遍,下面是一些关键代码,完整项目可查看Github。
public static boolean isRunInVirtual() {
String filter = getUidStrFormat();
String result = exec("ps");
if (result == null || result.isEmpty()) {
return false;
}
String[] lines = result.split("\n");
if (lines == null || lines.length <= 0) {
return false;
}
int exitDirCount = 0;
for (int i = 0; i < lines.length; i++) {
if (lines[i].contains(filter)) {
int pkgStartIndex = lines[i].lastIndexOf(" ");
String processName = lines[i].substring(pkgStartIndex <= 0
? 0 : pkgStartIndex + 1, lines[i].length());
File dataFile = new File(String.format("/data/data/%s",
processName, Locale.CHINA));
if (dataFile.exists()) {
exitDirCount++;
}
}
}
return exitDirCount > 1;
}
4. 成功率测试以及总结
以下为在Android 6.0 三星Note 5,Android 7.0 一加5,Android 4.4 Sony Lt55上面测试的结果,测试包可在github上面的testcase目录下看到。
多开APP应用 | 测试结果 |
---|---|
Go双开 | 测试通过 |
双开精灵 | 测试通过 |
VirtualApp | 测试通过 |
平行空间 | 测试通过 |
双开助手 | 测试通过 |
双开精灵 | 测试通过 |
兼容性目前看来还是很有保障的
虽然这里公开的方法如果虚拟化框架作者看到还能能防御,比如hook住获取uid的接口,欺骗/data/data的访问,限制ps命令,但是我觉得虚拟化框架主要是为了某些对安全或者不介意的应用使用,如果在虚拟化框架下会产生损害应用利益的行为下,应用禁止了多开,这也是合理的。虚拟化框架不应该成为作恶的工具,方便用户固然好,但是为黑产提供方便之门就是作恶了。
网友评论