惡意用戶識別?——Java 層反模擬器、反Hook、反多開技巧

近兩年,Android端的虛擬化技術和羣控技術發展急速,帶來不少好玩產品和便利工具。可是做爲App開發者就頭疼了,惡意用戶(好比不文明用戶、好比刷單)利用這些技術,做惡門檻低得不知道哪裏去。咱們須要思考怎麼識別和防護了。 下文介紹一些簡單可是有效的惡意用戶識別(方便後續封號)方案。java

Anti 模擬器

這個很容易理解,模擬出來的機器,每次模擬的時候生成的設備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;//不能再少了,不然有可能誤判,若增減了新的嫌疑度斷定屬性,要從新評估該值
複製代碼

Anti 多開

多開麻煩在於真機多開,具有真機特徵,模擬器的檢測就失效了,由於它就是真機。 應對方法:普通的軟多開,通常繞不過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

Anti Hook

很少說了,方法都被你Hook了,你就是大爺,你說啥就是啥。 應對方法:檢測是否安裝了xposed相關應用,檢測調用棧道的可疑方法,檢測並不該該native的native方法,經過/proc/[pid]/maps檢測可疑的共享對象或者JAR。bash

檢測是否安裝了xposed相關應用

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;
                }

            }
        }
複製代碼

經過/proc/[pid]/maps檢測可疑的共享對象或者JAR:

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

若是你有SD卡寫權限的話,按本身的規則生成id並加密,在本身應用私有目錄以外的隱蔽地方偷偷寫成一個隱藏文件(只要在文件名或者文件夾名字前加一個點號便可)。只要生成過一次,就以這個爲準,不管用戶修改設備信息註冊多少個馬甲,都能識別爲同一設備用戶。函數

手機號短信認證

全部登陸用戶都必須綁定手機號。從產品流程上提升了馬甲成本,可是也提升了用戶註冊門檻。工具

固然了,以上方法只能防小白不防大師,這些方法很容易就能夠被有經驗的逆向人員繞過。 寫出來,是但願能集思廣益,得到更多的反制思路,提升惡意分子僞造設備的成本。(實際上是但願碰到大佬指點,提升下本不成器菜鳥的知識水平😄)有更深刻實踐的同窗,求評論,求私信。

參考Demo:

anti-counterfeit-android

參考文章:

Android反調試之 AntiEmulator 檢測安卓模擬器

基於文件特徵的Android模擬器檢測

Android Java層的anti-hooking技巧

Android虛擬機多開檢測

相關文章
相關標籤/搜索