多开/分身原本用于方便有多个微信/QQ解决同时登录的问题,但近来年被各种黑产所利用,多见于薅羊毛,部分多开App甚至提供了篡改功能。对于普通用户根本不会有多开的需求的App,一旦检测到当前运行在多开环境下,有理由限制该用户的后续行为。

在尝试了目前市面上多款多开App后,总结了几种检测方案。

多开原理

目前市面上的多开App的原理类似,都是以新进程运行被多开的App,并hook各类系统函数,使被多开的App认为自己是一个正常的App在运行。

从形式上来说多开App有2种形式,一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App,其原理和前一种是一样的,市面上多开分身这款App是用的这种形式,用户每分身一个App需新安装一个包名为dkmodel.xxx.xxx的App。

检测方案

检测files目录路径

我们知道App的私有目录是/data/data/包名//data/user/用户号/包名,通过Context.getFilesDir()方法可以拿到私有目录下的files目录。在多开环境下,获取到目录会变为/data/data/多开App的包名/xxxxxxxx/data/user/用户号/多开App的包名/xxxxxxxx

举个例子,在我手机上,正常使用App上面的代码获取到的路径为/data/user/0/top.darkness463.virtualcheck/files。在多开分身的多开环境下,路径为/data/user/0/dkmodel.zom.rxo/virtual/data/user/0/top.darkness463.virtualcheck/files

当然,多开软件是可以hook处理让你拿到正常的目录,但截至写这篇文章为止,市面上大部分多开App没有绕过这项检测,仅有360家的分身大师可以绕过。

ps检测

这种检测方法见这篇文章。这里就不重复了,我简单说一下原理。

我们先通过执行ps命令并以自己的uid进行过滤,得到类似下面的结果。

1
2
3
4
5
6
7
// 正常情况下
u0_a148 8162 423 1806036 56368 SyS_epoll+ 0 S top.darkness463.virtualcheck

// 多开环境下
u0_a155 19752 422 4437612 62752 SyS_epoll+ 0 S top.darkness463.virtualcheck
u0_a155 19758 422 564234 54356 SyS_epoll+ 0 S com.lbe.parallel
u0_a155 19747 422 734562 24542 SyS_epoll+ 0 S com.lbe.parallel:mdserver

可以看到在多开环境下,会获取到自己的包名和多开App的包名这2个包名,通过这些包名去/data/data/下找会找到2个目录,而正常情况下只能在/data/data/下找到自己的App的目录。

具体的实现原文已经贴了,这里也不重复了。目前未发现有多开App能绕过该项检测,但在Google Pixel Android 8.0下执行ps获取不到进程信息,在Android 8.1的类AOSP rom下都能正常获取,不知道那个手机什么情况。

应用列表检测

这里的应用列表检测不是指简单的遍历应用列表判断是不是安装了多开App,我们并不阻止用户安装多开App并多开其他App,我们只是不希望用户多开我们自己的App,因此不能检测到用户安装了多开App就把他干掉。

多开App都会对context.getPackageName()进行处理,让这个方法返回原始App的包名,因此在被多开的App看来,多开App的包名和原始的那个App的包名一样,因此在多开环境下遍历应用列表时会发现包名等于原始App的包名的应用会有两个。

代码如下。只对部分多开App有效,例如360的分身大师,不少多开App会绕过这项检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean checkPkg(Context context) {
try {
if (context == null) {
return false;
}
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs) {
if (packageName.equals(info.packageName)) {
count++;
}
}
return count > 1;
} catch (Exception ignore) {}
return false;
}

maps检测

读取/proc/self/maps,多开App会加载一些自己的so到内存空间,举个例子,360的分身大师加载了其目录下的某个so,/data/app/com.qihoo.magic-gdEsg8KRAuJy0MuY18BlqQ==/lib/arm/libbreakpad-jni-1.5.so,通过对各种多开App的包名的匹配,如果maps中有多开App的包名的东西,那么当前就是运行在多开环境下。

目前没有发现多开App绕过该项检测,但缺点是需要收集所有多开App的包名,一旦多开App改个包名就失效了。

伪代码如下。

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

Set<String> virtualPkgs; // 多开App包名列表
private boolean check() {
BufferedReader bufr = null;
try {
bufr = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = bufr.readLine()) != null) {
for (String pkg : virtualPkgs) {
if (line.contains(pkg)) {
return true;
}
}
}
} catch (Exception ignore) {

} finally {
if (bufr != null) {
try {
bufr.close();
} catch (IOException e) {

}
}
}
return false;
}

总结

这些方法有的已经被部分多开App绕过了,有的暂时还未绕过,建议所有的检测都加上,而且可以Java层和C层都进行检测。当然,与黑产斗争的道路我们永远处于劣势。