近兩年,Android端的虛擬化技術和群控技術發展急速,帶來很多好玩產品和便利工具。但是作為App開發者就頭疼了,惡意使用者(比如不文明使用者、比如刷單)利用這些技術,作惡門檻低得不知道哪裡去。我們需要思考怎麼識別和防禦了。 下文介紹一些簡單但是有效的惡意使用者識別(方便後續封號)方案。
Anti 模擬器
這個很容易理解,模擬出來的機器,每次模擬的時候生成的裝置ID,只存在模擬器使用的生命週期裡。可能下一次模擬時又不一樣了。 應對方法:主要是檢測執行模擬器的一些特徵,比如驅動檔案,Build類內的硬體訊息等。 比如Build類內有模擬器的字串,明顯就是模擬器:
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;
}
複製程式碼
還有的特徵只是疑似,但不確定,對於這些特徵,可以集合起來做一個疑似度評分,評分達到一定程度就標記為模擬器:
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"下有兩個私有目錄,則違背了系統 "只為一個應用建立唯一一個私有目錄"的設定,則該應用被多開了。
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的,暫時沒有好辦法識別。
Anti Hook
不多說了,方法都被你Hook了,你就是大爺,你說啥就是啥。 應對方法:檢測是否安裝了xposed相關應用,檢測呼叫棧道的可疑方法,檢測並不應該native的native方法,通過/proc/[pid]/maps檢測可疑的共享物件或者JAR。
檢測是否安裝了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等其他資訊做一個相似度判定,可以找出一些疑似同一惡意使用者的賬號。
SD卡儲存自制ID
如果你有SD卡寫許可權的話,按自己的規則生成id並加密,在自己應用私有目錄之外的隱蔽地方偷偷寫成一個隱藏檔案(只要在檔名或者資料夾名字前加一個點號即可)。只要生成過一次,就以這個為準,無論使用者修改裝置資訊註冊多少個馬甲,都能識別為同一裝置使用者。
手機號簡訊認證
所有登入使用者都必須繫結手機號。從產品流程上提高了馬甲成本,但是也提高了使用者註冊門檻。
當然了,以上方法只能防小白不防大師,這些方法很容易就可以被有經驗的逆向人員繞過。 寫出來,是希望能集思廣益,獲得更多的反制思路,提高惡意分子偽造裝置的成本。(其實是希望碰到大佬指點,提高下本不成器菜鳥的知識水平?)有更深入實踐的同學,求評論,求私信。
參考Demo:
參考文章: