using Android.Content; using System.Collections.Generic; using System.Linq; using System.Text; namespace System.Application.Security { /// /// Android[多开/分身]检测 /// https://github.com/lamster2018/EasyProtector/blob/master/library/src/main/java/com/lahm/library/VirtualApkCheckUtil.java /// internal static class VirtualApkCheckUtil { /// /// 市面上多开应用的包名列表 /// static readonly string[] virtualPkgs = new[] { "com.bly.dkplat", //多开分身本身的包名 "com.by.chaos", //chaos引擎 "com.lbe.parallel", //平行空间 "com.excelliance.dualaid", //双开助手 "com.lody.virtual", //VirtualXposed,VirtualApp "com.qihoo.magic", //360分身大师 }; static bool? mCheckByPrivateFilePath; /// /// 检测APP私有路径,某些多开软件会包含多开软件的包名 /// /// /// public static bool CheckByPrivateFilePath(Context context) { if (mCheckByPrivateFilePath.HasValue) return mCheckByPrivateFilePath.Value; var filesDirPath = context.FilesDir?.Path; if (filesDirPath == null) throw new NullReferenceException("FilesDir.Path"); mCheckByPrivateFilePath = virtualPkgs .Any(x => filesDirPath.Contains(x, StringComparison.OrdinalIgnoreCase)); return mCheckByPrivateFilePath.Value; } static bool? mCheckByOriginApkPackageName; /// /// 检测原始的包名,多开应用会hook处理getPackageName方法,如果在应用列表里出现了同样的包,那么认为该应用被多开了 /// /// /// public static bool CheckByOriginApkPackageName(Context context) { if (mCheckByOriginApkPackageName.HasValue) return mCheckByOriginApkPackageName.Value; var count = 0; try { var packageName = context.PackageName; if (packageName == null) throw new NullReferenceException("context.PackageName"); var pm = context.PackageManager; if (pm == null) throw new NullReferenceException("context.PackageManager"); var pkgs = pm.GetInstalledPackages(0); foreach (var info in pkgs) { if (packageName.Equals(info.PackageName, StringComparison.OrdinalIgnoreCase)) { count++; } if (count > 1) { mCheckByOriginApkPackageName = true; return mCheckByOriginApkPackageName.Value; } } } catch (Java.Lang.Throwable t) { t.PrintStackTraceWhenDebug(); } mCheckByOriginApkPackageName = count > 1; return mCheckByOriginApkPackageName.Value; } static bool? mCheckByMultiApkPackageName; /// /// 运行被克隆的应用,该应用会加载多开应用的so库,检测已经加载的so里是否包含这些应用的包名 /// /// public static bool CheckByMultiApkPackageName() { if (mCheckByMultiApkPackageName.HasValue) return mCheckByMultiApkPackageName.Value; try { var reader = new Java.IO.FileReader("/proc/self/maps"); using var bufferedReader = new Java.IO.BufferedReader(reader); string? line; while ((line = bufferedReader.ReadLine()) != null) { var hasContains = virtualPkgs .Any(x => line.Contains(x, StringComparison.OrdinalIgnoreCase)); if (hasContains) { mCheckByMultiApkPackageName = true; return mCheckByMultiApkPackageName.Value; } } } catch (Java.Lang.Throwable t) { t.PrintStackTraceWhenDebug(); } mCheckByMultiApkPackageName = false; return mCheckByMultiApkPackageName.Value; } static class CommandUtil { static string? GetStrFromBufferInputSteam(Java.IO.BufferedInputStream bufferedInputStream) { if (bufferedInputStream == null) return string.Empty; var BUFFER_SIZE = 512; var buffer = new byte[BUFFER_SIZE]; try { var result = new StringBuilder(); while (true) { int read = bufferedInputStream.Read(buffer); if (read > 0) { result.Append(new Java.Lang.String(buffer, 0, read).ToString()); } if (read < BUFFER_SIZE) { break; } } return result.ToString(); } catch (Java.Lang.Throwable t) { t.PrintStackTraceWhenDebug(); return null; } } public static string? Exec(string command) { var runtime = Java.Lang.Runtime.GetRuntime(); if (runtime == null) throw new NullReferenceException("Java.Lang.Runtime.GetRuntime"); try { var command2 = new Java.Lang.String(command); using var process = runtime.Exec("sh"); if (process != null) { using var bufferedOutputStream = new Java.IO.BufferedOutputStream(process.OutputStream); using var bufferedInputStream = new Java.IO.BufferedInputStream(process.InputStream); bufferedOutputStream.Write(command2.GetBytes()); bufferedOutputStream.Write('\n'); bufferedOutputStream.Flush(); bufferedOutputStream.Close(); process.WaitFor(); var outputStr = GetStrFromBufferInputSteam(bufferedInputStream); return outputStr; } } catch (Java.Lang.Throwable t) { t.PrintStackTraceWhenDebug(); } return null; } } static string? GetUidStrFormat() { var filter = CommandUtil.Exec("cat /proc/self/cgroup"); if (string.IsNullOrWhiteSpace(filter)) return null; #pragma warning disable CS8604 // 可能的 null 引用参数。 var filter2 = filter.ToJavaString(); #pragma warning restore CS8604 // 可能的 null 引用参数。 int uidStartIndex = filter2.LastIndexOf("uid"); int uidEndIndex = filter2.LastIndexOf("/pid"); if (uidStartIndex < 0) return null; if (uidEndIndex <= 0) uidEndIndex = filter2.Length(); filter2 = filter2.Substring(uidStartIndex + 4, uidEndIndex).ToJavaString(); try { var strUid = filter2.ReplaceAll("\n", ""); var uid = strUid.TryParseInt32(); if (uid.HasValue) { return Java.Lang.String.Format("u0_a%d", uid - 10000); } } catch (Java.Lang.Throwable t) { t.PrintStackTraceWhenDebug(); } return null; } static bool? mCheckByHasSameUid; /// /// Android系统一个app一个uid,如果同一uid下有两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了 /// /// public static bool CheckByHasSameUid() { if (mCheckByHasSameUid.HasValue) return mCheckByHasSameUid.Value; var filter = GetUidStrFormat(); if (string.IsNullOrWhiteSpace(filter)) return false; var result = CommandUtil.Exec("ps"); if (string.IsNullOrWhiteSpace(result)) return false; #pragma warning disable CS8602 // 取消引用可能出现的空引用。 var lines = result.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); #pragma warning restore CS8602 // 取消引用可能出现的空引用。 if (lines == null || lines.Length <= 0) return false; var exitDirCount = 0; for (int i = 0; i < lines.Length; i++) { if (lines[i].Contains(filter, StringComparison.OrdinalIgnoreCase)) { var pkgStartIndex = lines[i].LastIndexOf(" "); var processName = lines[i].JavaSubstring( pkgStartIndex <= 0 ? 0 : pkgStartIndex + 1, lines[i].Length); using var dataFile = new Java.IO.File($"/data/data/{processName}"); if (dataFile.Exists()) { exitDirCount++; } } if (exitDirCount > 1) { mCheckByHasSameUid = true; return mCheckByHasSameUid.Value; } } mCheckByHasSameUid = exitDirCount > 1; return mCheckByHasSameUid.Value; } static volatile Android.Net.LocalServerSocket? localServerSocket; public static bool CheckByCreateLocalServerSocket(string uniqueMsg) { if (localServerSocket != null) return false; try { localServerSocket = new Android.Net.LocalServerSocket(uniqueMsg); return false; } catch (Java.IO.IOException ex) { ex.PrintStackTraceWhenDebug(); return true; } } static bool? mCheckByCreateLocalServerSocket; public static bool CheckByCreateLocalServerSocket(Context context) { if (mCheckByCreateLocalServerSocket.HasValue) return mCheckByCreateLocalServerSocket.Value; var packageName = context.PackageName; if (packageName == null) throw new NullReferenceException("context.PackageName"); mCheckByCreateLocalServerSocket = CheckByCreateLocalServerSocket(packageName); return mCheckByCreateLocalServerSocket.Value; } const char O = 'Y'; const char X = 'N'; static char ToChar(bool b) => b ? O : X; static bool Check(IEnumerable> checkfuns) { foreach (var item in checkfuns) { var result = item?.Invoke() ?? false; if (result) return true; } return false; } static bool Check(params Func[] checkfuns) => Check(checkfuns.AsEnumerable()); static bool Check(Context context, params Func[] checkfuns) => Check(checkfuns.Select, Func>( x => () => x?.Invoke(context) ?? false)); public static bool Check(Context context) { return Check(context, CheckByPrivateFilePath, CheckByOriginApkPackageName, CheckByCreateLocalServerSocket) || Check(CheckByHasSameUid, CheckByMultiApkPackageName); } public static string GetCheckResult(Context context) { var chars = new char[] { ToChar(CheckByPrivateFilePath(context)), ToChar(CheckByOriginApkPackageName(context)), ToChar(CheckByHasSameUid()), ToChar(CheckByMultiApkPackageName()), ToChar(CheckByCreateLocalServerSocket(context)), }; return new string(chars); } } }