参考:https://blog.csdn.net/mawei7510/article/details/80250416
在应用开发中,如果有签到打卡之类的功能,我们肯定需要在项目中禁止用户开启虚拟定位,导致在***米之外的距离模拟定位然后进行了打卡操作!
首先:获取用户手机是否打开了 “允许模拟位置” 选项?
其实很简单,这些设置项,基本都是写在数据库里,所以只要看看setting的源码(或者查看logcat可能也可以得到些有用的信息),就能知道该配置是写了数据库的哪个字段。
boolean isOpen = Settings.Secure.getInt(context.getContentResolver(),Settings.Secure.ALLOW_MOCK_LOCATION, 0) != 0;
//很明显,Settings.Secure.ALLOW_MOCK_LOCATION 就是存放允许模拟位置的数据库字段了
//当isOpen为ture时,则是用户打开了允许模拟位置的选项,否则则没有开启!
其次:虚拟定位可以通过一些第三方应用,然后把我们自己的应用克隆一个。在启动的时候需要在第三方的应用里面来启动,这样的话在私有文件里面生成的包名势必会和直接启动自己的应用有区别,知道了这些,我们将通过以下三个方法来一一检测;
首先介绍一些那些使用应用分身双开和虚拟定位的应用和自己的应用在私有目录下生成的包名有什么区别:
我们知道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家的分身大师可以绕过。
下面开始正式检测:
我们先通过执行对uid进行过滤,得到类似下面的结果
// 正常情况下
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的目录。看下具体代码实现;
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;
}
这里的应用列表检测不是指简单的遍历应用列表判断是不是安装了多开App,我们并不阻止用户安装多开App并多开其他App,我们只是不希望用户多开我们自己的App,因此不能检测到用户安装了多开App就把他干掉。
多开App都会对context.getPackageName()
进行处理,让这个方法返回原始App的包名,因此在被多开的App看来,多开App的包名和原始的那个App的包名一样,因此在多开环境下遍历应用列表时会发现包名等于原始App的包名的应用会有两个。
private boolean checkPkg(Context context) {
try {
if (context == null) {
return false;
}
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs) {
if (packageName.equals(info.packageName)) {
count++;
}
}
return count > 1;
} catch (Exception ignore) {}
return false;
}
读取/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改个包名就失效了。
Set 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;
}
以上三种检测方法有的会被第三方虚拟定位软件或者多开分身软件躲避掉,有的则不会,所以使用的时候建议三种方法全部用上。