近两年,Android端的虚拟化技术和群控技术发展急速,带来不少好玩产品和便利工具。可是做为App开发者就头疼了,恶意用户(好比不文明用户、好比刷单)利用这些技术,做恶门槛低得不知道哪里去。咱们须要思考怎么识别和防护了。 下文介绍一些简单可是有效的恶意用户识别(方便后续封号)方案。java
这个很容易理解,模拟出来的机器,每次模拟的时候生成的设备ID,只存在模拟器使用的生命周期里。可能下一次模拟时又不同了。 应对方法:主要是检测运行模拟器的一些特征,好比驱动文件,Build类内的硬件讯息等。 好比Build类内有模拟器的字串,明显就是模拟器:android
public static boolean isEmulatorAbsoluly() {
if (Build.PRODUCT.contains("sdk") ||
Build.PRODUCT.contains("sdk_x86") ||
Build.PRODUCT.contains("sdk_google") ||
Build.PRODUCT.contains("Andy") ||
Build.PRODUCT.contains("Droid4X") ||
Build.PRODUCT.contains("nox") ||
Build.PRODUCT.contains("vbox86p")) {
return true;
}
if (Build.MANUFACTURER.equals("Genymotion") ||
Build.MANUFACTURER.contains("Andy") ||
Build.MANUFACTURER.contains("nox") ||
Build.MANUFACTURER.contains("TiantianVM")) {
return true;
}
if (Build.BRAND.contains("Andy")) {
return true;
}
if (Build.DEVICE.contains("Andy") ||
Build.DEVICE.contains("Droid4X") ||
Build.DEVICE.contains("nox") ||
Build.DEVICE.contains("vbox86p")) {
return true;
}
if (Build.MODEL.contains("Emulator") ||
Build.MODEL.equals("google_sdk") ||
Build.MODEL.contains("Droid4X") ||
Build.MODEL.contains("TiantianVM") ||
Build.MODEL.contains("Andy") ||
Build.MODEL.equals("Android SDK built for x86_64") ||
Build.MODEL.equals("Android SDK built for x86")) {
return true;
}
if (Build.HARDWARE.equals("vbox86") ||
Build.HARDWARE.contains("nox") ||
Build.HARDWARE.contains("ttVM_x86")) {
return true;
}
if (Build.FINGERPRINT.contains("generic/sdk/generic") ||
Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86") ||
Build.FINGERPRINT.contains("Andy") ||
Build.FINGERPRINT.contains("ttVM_Hdragon") ||
Build.FINGERPRINT.contains("generic/google_sdk/generic") ||
Build.FINGERPRINT.contains("vbox86p") ||
Build.FINGERPRINT.contains("generic/vbox86p/vbox86p")) {
return true;
}
return false;
}
复制代码
还有的特征只是疑似,但不肯定,对于这些特征,能够集合起来作一个疑似度评分,评分达到必定程度就标记为模拟器:git
int newRating = 0;
if (rating < 0) {
if (Build.PRODUCT.contains("sdk") ||
Build.PRODUCT.contains("Andy") ||
Build.PRODUCT.contains("ttVM_Hdragon") ||
Build.PRODUCT.contains("google_sdk") ||
Build.PRODUCT.contains("Droid4X") ||
Build.PRODUCT.contains("nox") ||
Build.PRODUCT.contains("sdk_x86") ||
Build.PRODUCT.contains("sdk_google") ||
Build.PRODUCT.contains("vbox86p")) {
newRating++;
}
if (Build.MANUFACTURER.equals("unknown") ||
Build.MANUFACTURER.equals("Genymotion") ||
Build.MANUFACTURER.contains("Andy") ||
Build.MANUFACTURER.contains("MIT") ||
Build.MANUFACTURER.contains("nox") ||
Build.MANUFACTURER.contains("TiantianVM")) {
newRating++;
}
if (Build.BRAND.equals("generic") ||
Build.BRAND.equals("generic_x86") ||
Build.BRAND.equals("TTVM") ||
Build.BRAND.contains("Andy")) {
newRating++;
}
if (Build.DEVICE.contains("generic") ||
Build.DEVICE.contains("generic_x86") ||
Build.DEVICE.contains("Andy") ||
Build.DEVICE.contains("ttVM_Hdragon") ||
Build.DEVICE.contains("Droid4X") ||
Build.DEVICE.contains("nox") ||
Build.DEVICE.contains("generic_x86_64") ||
Build.DEVICE.contains("vbox86p")) {
newRating++;
}
if (Build.MODEL.equals("sdk") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.equals("google_sdk") ||
Build.MODEL.contains("Droid4X") ||
Build.MODEL.contains("TiantianVM") ||
Build.MODEL.contains("Andy") ||
Build.MODEL.equals("Android SDK built for x86_64") ||
Build.MODEL.equals("Android SDK built for x86")) {
newRating++;
}
if (Build.HARDWARE.equals("goldfish") ||
Build.HARDWARE.equals("vbox86") ||
Build.HARDWARE.contains("nox") ||
Build.HARDWARE.contains("ttVM_x86")) {
newRating++;
}
if (Build.FINGERPRINT.contains("generic/sdk/generic") ||
Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86") ||
Build.FINGERPRINT.contains("Andy") ||
Build.FINGERPRINT.contains("ttVM_Hdragon") ||
Build.FINGERPRINT.contains("generic_x86_64") ||
Build.FINGERPRINT.contains("generic/google_sdk/generic") ||
Build.FINGERPRINT.contains("vbox86p") ||
Build.FINGERPRINT.contains("generic/vbox86p/vbox86p")) {
newRating++;
}
try {
String opengl = android.opengl.GLES20.glGetString(android.opengl.GLES20.GL_RENDERER);
if (opengl != null) {
if (opengl.contains("Bluestacks") ||
opengl.contains("Translator")
)
newRating += 10;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
File sharedFolder = new File(Environment
.getExternalStorageDirectory().toString()
+ File.separatorChar
+ "windows"
+ File.separatorChar
+ "BstSharedFolder");
if (sharedFolder.exists()) {
newRating += 10;
}
} catch (Exception e) {
e.printStackTrace();
}
rating = newRating;
}
return rating > 3;//不能再少了,不然有可能误判,若增减了新的嫌疑度断定属性,要从新评估该值
复制代码
多开麻烦在于真机多开,具有真机特征,模拟器的检测就失效了,由于它就是真机。 应对方法:普通的软多开,通常绕不过uid,仍是用宿主的。所以,若是知足同一uid下的两个进程对应的包名,在"/data/data"下有两个私有目录,则违背了系统 "只为一个应用建立惟一一个私有目录"的设定,则该应用被多开了。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;
}
复制代码
这个方法是在简书 JZaratustra 大佬的文章里学到的:Android虚拟机多开检测。 可是有一些多开,好比小米自带的多开这种,进程好像都是隔离的独立uid的,暂时没有好办法识别。windows
很少说了,方法都被你Hook了,你就是大爷,你说啥就是啥。 应对方法:检测是否安装了xposed相关应用,检测调用栈道的可疑方法,检测并不该该native的native方法,经过/proc/[pid]/maps检测可疑的共享对象或者JAR。bash
PackageManager packageManager = context.getPackageManager();
List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
for(ApplicationInfo applicationInfo : applicationInfoList) {
if(applicationInfo.packageName.equals("de.robv.android.xposed.installer")) {
Log.wtf("HookDetection", "Xposed found on the system.");
}
if(applicationInfo.packageName.equals("com.saurik.substrate")) {
Log.wtf("HookDetection", "Substrate found on the system.");
}
}
复制代码
try {
throw new Exception("blah");
} catch (Exception e) {
int zygoteInitCallCount = 0;
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
if (stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) {
zygoteInitCallCount++;
if (zygoteInitCallCount == 2) {
Log.wtf("HookDetection", "Substrate is active on the device.");
isHook = true;
}
}
if (stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") &&
stackTraceElement.getMethodName().equals("invoked")) {
Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate.");
isHook = true;
}
if (stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") &&
stackTraceElement.getMethodName().equals("main")) {
Log.wtf("HookDetection", "Xposed is active on the device.");
isHook = true;
}
if (stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") &&
stackTraceElement.getMethodName().equals("handleHookedMethod")) {
Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed.");
isHook = true;
}
}
}
复制代码
try {
Set<String> libraries = new HashSet();
String mapsFilename = "/proc/" + android.os.Process.myPid() + "/maps";
BufferedReader reader = new BufferedReader(new FileReader(mapsFilename));
String line;
while ((line = reader.readLine()) != null) {
if (line.endsWith(".so") || line.endsWith(".jar")) {
int n = line.lastIndexOf(" ");
libraries.add(line.substring(n + 1));
}
}
for (String library : libraries) {
if (library.contains("com.saurik.substrate")) {
Log.wtf("HookDetection", "Substrate shared object found: " + library);
isHook = true;
}
if (library.contains("XposedBridge.jar")) {
Log.wtf("HookDetection", "Xposed JAR found: " + library);
isHook = true;
}
}
reader.close();
} catch (Exception e) {
Log.wtf("HookDetection", e.toString());
}
复制代码
注意,只要针对这几个检测相关函数Hook,就反反Hook了。很容易绕过。服务器
可用于识别设备的标识有不少,除了Android ID,还有imei、mac、pseduo_id,aaid,gsf_id等。因为谷歌是反对惟一绝对追踪用户的,因此这些id或难或简单都是可能被修改的。好比,经过adb命令就能够无root直接修改Android ID。可是,这些标识所有都修改的话仍是优势麻烦的。客户端能够把这些id都上报给服务器,服务器再结合地理位置、ip等其余信息作一个类似度断定,能够找出一些疑似同一恶意用户的帐号。app
若是你有SD卡写权限的话,按本身的规则生成id并加密,在本身应用私有目录以外的隐蔽地方偷偷写成一个隐藏文件(只要在文件名或者文件夹名字前加一个点号便可)。只要生成过一次,就以这个为准,不管用户修改设备信息注册多少个马甲,都能识别为同一设备用户。函数
全部登陆用户都必须绑定手机号。从产品流程上提升了马甲成本,可是也提升了用户注册门槛。工具
固然了,以上方法只能防小白不防大师,这些方法很容易就能够被有经验的逆向人员绕过。 写出来,是但愿能集思广益,得到更多的反制思路,提升恶意分子伪造设备的成本。(实际上是但愿碰到大佬指点,提升下本不成器菜鸟的知识水平😄)有更深刻实践的同窗,求评论,求私信。
参考Demo:
参考文章: