EasyProtector 环境检测机制深度分析报告
目录
- 项目概述
- 整体架构
- 模拟器检测机制 (EmulatorCheckUtil)
- 虚拟环境/多开检测机制 (VirtualApkCheckUtil)
- 安全检测机制 (SecurityCheckUtil)
- Native层反调试检测 (antitrace.cpp)
- 无障碍服务检测 (AccessibilityServicesCheckUtil)
- 辅助工具类
- 应用层实现与配置
- 项目构建与编译配置
- 检测流程图
- 检测结果判定逻辑
- 总结
1. 项目概述
1.1 项目简介
EasyProtector 是一个 Android 安全检测库,用于检测应用运行环境的安全性。主要功能包括:
- 模拟器检测:识别应用是否运行在 Android 模拟器中
- 多开/虚拟环境检测:检测应用是否被多开软件或虚拟机运行
- Root 检测:检测设备是否已 Root
- Xposed 框架检测:检测是否存在 Xposed Hook 框架
- 调试检测:检测应用是否正在被调试
- 进程追踪检测:检测是否有进程在追踪当前应用
1.2 核心类结构
com.lahm.library/
├── EasyProtectorLib.java # 对外暴露的统一入口类
├── EmulatorCheckUtil.java # 模拟器检测工具类
├── VirtualApkCheckUtil.java # 虚拟环境/多开检测工具类
├── SecurityCheckUtil.java # 安全检测工具类(Root/Debug/Xposed等)
├── AccessibilityServicesCheckUtil.java # 无障碍服务检测
├── NDKUtil.java # Native库加载工具类
├── CommandUtil.java # Shell命令执行工具类
├── CheckResult.java # 检测结果封装类
├── EmulatorCheckCallback.java # 模拟器检测回调接口
├── VirtualCheckCallback.java # 虚拟环境检测回调接口
└── LibLoader.java # 库加载器接口
1.3 依赖关系
EasyProtectorLib (入口)
├── SecurityCheckUtil
│ └── CommandUtil
├── EmulatorCheckUtil
│ └── CommandUtil
├── VirtualApkCheckUtil
│ └── CommandUtil
├── AccessibilityServicesCheckUtil
└── NDKUtil
└── antitrace.so (Native层)
2. 整体架构
2.1 入口类 EasyProtectorLib
文件位置: library/src/main/java/com/lahm/library/EasyProtectorLib.java
该类是整个库的统一入口,提供了以下静态方法:
| 方法名 | 功能 | 调用的工具类 |
|---|---|---|
checkSignature(Context) |
获取应用签名 | SecurityCheckUtil |
checkIsDebug(Context) |
检测Debug模式 | SecurityCheckUtil |
checkIsPortUsing(host, port) |
检测端口占用 | SecurityCheckUtil |
checkIsRoot() |
检测Root权限 | SecurityCheckUtil |
checkIsXposedExist() |
检测Xposed框架 | SecurityCheckUtil |
checkXposedExistAndDisableIt() |
尝试关闭Xposed | SecurityCheckUtil |
checkHasLoadSO(soName) |
检测SO库加载 | SecurityCheckUtil |
checkIsBeingTracedByJava() |
Java层追踪检测 | SecurityCheckUtil |
checkIsBeingTracedByC() |
Native层追踪检测 | NDKUtil |
checkIsRunningInEmulator(Context, Callback) |
模拟器检测 | EmulatorCheckUtil |
checkIsRunningInVirtualApk(msg, Callback) |
多开环境检测 | VirtualApkCheckUtil |
2.2 检测结果类 CheckResult
文件位置: library/src/main/java/com/lahm/library/CheckResult.java
public class CheckResult {
public static final int RESULT_MAYBE_EMULATOR = 0; // 可能是模拟器
public static final int RESULT_EMULATOR = 1; // 确定是模拟器
public static final int RESULT_UNKNOWN = 2; // 可能是真机
public int result; // 检测结果状态码
public String value; // 检测到的具体值
}
3. 模拟器检测机制 (EmulatorCheckUtil)
文件位置: library/src/main/java/com/lahm/library/EmulatorCheckUtil.java
模拟器检测是该库最核心和复杂的功能,采用多维度特征检测 + 嫌疑值累计的策略。
3.1 检测总体流程
主入口方法 readSysProperty(Context, EmulatorCheckCallback) 执行以下检测步骤:
public boolean readSysProperty(Context context, EmulatorCheckCallback callback) {
int suspectCount = 0; // 嫌疑值计数器
// 1. 检测硬件名称
// 2. 检测渠道(flavor)
// 3. 检测设备型号
// 4. 检测硬件制造商
// 5. 检测主板名称
// 6. 检测主板平台
// 7. 检测基带信息
// 8. 检测传感器数量
// 9. 检测已安装应用数量
// 10. 检测相机闪光灯支持
// 11. 检测相机支持
// 12. 检测蓝牙支持
// 13. 检测光线传感器
// 14. 检测进程组信息(cgroup)
// 嫌疑值 > 3 则判定为模拟器
return suspectCount > 3;
}
3.2 详细检测维度分析
3.2.1 硬件名称检测 (checkFeaturesByHardware)
检测原理: 通过读取系统属性 ro.hardware 获取硬件名称,匹配已知模拟器特征值。
实现代码分析:
private CheckResult checkFeaturesByHardware() {
String hardware = getProperty("ro.hardware");
if (null == hardware) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = hardware.toLowerCase();
switch (tempValue) {
case "ttvm": // 天天模拟器
case "nox": // 夜神模拟器
case "cancro": // 网易MUMU模拟器
case "intel": // 逍遥模拟器
case "vbox": // VirtualBox虚拟机
case "vbox86": // 腾讯手游助手
case "android_x86": // 雷电模拟器
result = RESULT_EMULATOR;
break;
default:
result = RESULT_UNKNOWN;
break;
}
return new CheckResult(result, hardware);
}
检测到的模拟器列表:
| 特征值 | 对应模拟器 |
|---|---|
ttvm |
天天模拟器 |
nox |
夜神模拟器 |
cancro |
网易MUMU模拟器 |
intel |
逍遥模拟器 |
vbox / vbox86 |
VirtualBox/腾讯手游助手 |
android_x86 |
雷电模拟器 |
判定逻辑:
- 若属性为
null→ 可能是模拟器 (suspectCount++) - 若匹配已知特征 → 确定是模拟器 (直接返回
true) - 其他 → 未知,继续检测
3.2.2 渠道信息检测 (checkFeaturesByFlavor)
检测原理: 通过读取 ro.build.flavor 系统属性,识别编译渠道。
private CheckResult checkFeaturesByFlavor() {
String flavor = getProperty("ro.build.flavor");
if (null == flavor) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = flavor.toLowerCase();
if (tempValue.contains("vbox")) // VirtualBox特征
result = RESULT_EMULATOR;
else if (tempValue.contains("sdk_gphone")) // Google官方模拟器
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, flavor);
}
检测特征:
vbox→ VirtualBox 虚拟机sdk_gphone→ Google 官方 Android 模拟器 (AVD)
3.2.3 设备型号检测 (checkFeaturesByModel)
检测原理: 通过 ro.product.model 获取设备型号名称。
private CheckResult checkFeaturesByModel() {
String model = getProperty("ro.product.model");
if (null == model) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = model.toLowerCase();
if (tempValue.contains("google_sdk")) // Google SDK
result = RESULT_EMULATOR;
else if (tempValue.contains("emulator")) // 包含emulator字样
result = RESULT_EMULATOR;
else if (tempValue.contains("android sdk built for x86")) // x86架构SDK
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, model);
}
检测特征:
| 特征关键词 | 含义 |
|---|---|
google_sdk |
Google SDK 模拟器 |
emulator |
通用模拟器标识 |
android sdk built for x86 |
x86架构的SDK模拟器 |
3.2.4 硬件制造商检测 (checkFeaturesByManufacturer)
检测原理: 通过 ro.product.manufacturer 获取制造商信息。
private CheckResult checkFeaturesByManufacturer() {
String manufacturer = getProperty("ro.product.manufacturer");
if (null == manufacturer) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = manufacturer.toLowerCase();
if (tempValue.contains("genymotion")) // Genymotion模拟器
result = RESULT_EMULATOR;
else if (tempValue.contains("netease")) // 网易MUMU模拟器
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, manufacturer);
}
检测特征:
genymotion→ Genymotion 模拟器netease→ 网易 MUMU 模拟器
3.2.5 主板名称检测 (checkFeaturesByBoard)
检测原理: 通过 ro.product.board 获取主板名称。
private CheckResult checkFeaturesByBoard() {
String board = getProperty("ro.product.board");
if (null == board) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = board.toLowerCase();
if (tempValue.contains("android")) // 通用android主板
result = RESULT_EMULATOR;
else if (tempValue.contains("goldfish")) // goldfish是QEMU模拟器的代号
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, board);
}
检测特征:
android→ 通用 Android 模拟器主板goldfish→ QEMU 模拟器核心(Android 模拟器底层)
3.2.6 主板平台检测 (checkFeaturesByPlatform)
检测原理: 通过 ro.board.platform 获取平台信息。
private CheckResult checkFeaturesByPlatform() {
String platform = getProperty("ro.board.platform");
if (null == platform) return new CheckResult(RESULT_MAYBE_EMULATOR, null);
String tempValue = platform.toLowerCase();
if (tempValue.contains("android"))
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, platform);
}
3.2.7 基带信息检测 (checkFeaturesByBaseBand)
检测原理: 通过 gsm.version.baseband 获取基带版本。模拟器通常没有真实的基带信息。
private CheckResult checkFeaturesByBaseBand() {
String baseBandVersion = getProperty("gsm.version.baseband");
if (null == baseBandVersion)
return new CheckResult(RESULT_MAYBE_EMULATOR, null);
if (baseBandVersion.contains("1.0.0.0")) // 模拟器常用的假基带版本
result = RESULT_EMULATOR;
else
result = RESULT_UNKNOWN;
return new CheckResult(result, baseBandVersion);
}
特殊处理: 基带信息为 null 时,嫌疑值 +2(而非 +1),因为模拟器基带为空的概率极高。
case RESULT_MAYBE_EMULATOR:
suspectCount += 2; // 权重更高
break;
3.2.8 传感器数量检测 (getSensorNumber)
检测原理: 真机通常有大量传感器(陀螺仪、加速度计、光线传感器等),而模拟器传感器数量很少。
private int getSensorNumber(Context context) {
SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
return sm.getSensorList(Sensor.TYPE_ALL).size();
}
// 判断逻辑
if (sensorNumber <= 7) ++suspectCount;
阈值: 传感器数量 ≤ 7 → 嫌疑值 +1
3.2.9 已安装应用数量检测 (getUserAppNumber)
检测原理: 真机用户通常会安装大量第三方应用,模拟器通常是干净环境。
private int getUserAppNumber() {
String userApps = CommandUtil.getSingleInstance().exec("pm list package -3");
return getUserAppNum(userApps);
}
// 判断逻辑
if (userAppNumber <= 5) ++suspectCount;
实现细节:
- 执行 Shell 命令
pm list package -3列出所有第三方应用 - 统计返回结果中的包数量
- 阈值:应用数量 ≤ 5 → 嫌疑值 +1
3.2.10 相机功能检测 (supportCamera / supportCameraFlash)
检测原理: 模拟器通常不支持真实的相机和闪光灯。
private boolean supportCamera(Context context) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_CAMERA);
}
private boolean supportCameraFlash(Context context) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
}
// 判断逻辑
if (!supportCameraFlash) ++suspectCount;
if (!supportCamera) ++suspectCount;
3.2.11 蓝牙功能检测 (supportBluetooth)
检测原理: 模拟器通常不支持蓝牙。
private boolean supportBluetooth(Context context) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
}
// 判断逻辑
if (!supportBluetooth) ++suspectCount;
3.2.12 光线传感器检测 (hasLightSensor)
检测原理: 光线传感器是手机必备传感器,模拟器通常没有。
private boolean hasLightSensor(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
return sensor != null;
}
// 判断逻辑
if (!hasLightSensor) ++suspectCount;
3.2.13 进程组信息检测 (checkFeaturesByCgroup)
检测原理: 通过读取 /proc/self/cgroup 分析进程组信息。
private CheckResult checkFeaturesByCgroup() {
String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
if (null == filter)
return new CheckResult(RESULT_MAYBE_EMULATOR, null);
return new CheckResult(RESULT_UNKNOWN, filter);
}
3.3 综合判定逻辑
最终判定规则:suspectCount > 3 → 判定为模拟器
嫌疑值累加规则:
┌─────────────────────────────────────┬─────────────┐
│ 检测项 │ 嫌疑值增量 │
├─────────────────────────────────────┼─────────────┤
│ 硬件名称为null │ +1 │
│ 渠道信息为null │ +1 │
│ 设备型号为null │ +1 │
│ 制造商为null │ +1 │
│ 主板名称为null │ +1 │
│ 平台信息为null │ +1 │
│ 基带信息为null │ +2 (权重高) │
│ 传感器数量 ≤ 7 │ +1 │
│ 第三方应用 ≤ 5 │ +1 │
│ 不支持闪光灯 │ +1 │
│ 不支持相机 │ +1 │
│ 不支持蓝牙 │ +1 │
│ 无光线传感器 │ +1 │
│ cgroup信息为null │ +1 │
└─────────────────────────────────────┴─────────────┘
若任一检测项返回 RESULT_EMULATOR → 直接判定为模拟器
4. 虚拟环境/多开检测机制 (VirtualApkCheckUtil)
文件位置: library/src/main/java/com/lahm/library/VirtualApkCheckUtil.java
4.1 检测目标
检测应用是否运行在以下多开/虚拟环境中:
| 包名 | 对应软件 |
|---|---|
com.bly.dkplat |
多开分身 |
com.by.chaos |
Chaos引擎 |
com.lbe.parallel |
平行空间 |
com.excelliance.dualaid |
双开助手 |
com.lody.virtual |
VirtualXposed / VirtualApp |
com.qihoo.magic |
360分身大师 |
4.2 检测方法详解
4.2.1 私有文件路径检测 (checkByPrivateFilePath)
检测原理: 多开软件运行应用时,应用的私有目录路径会包含多开软件的包名。
public boolean checkByPrivateFilePath(Context context, VirtualCheckCallback callback) {
String path = context.getFilesDir().getPath();
// 正常路径: /data/data/com.your.app/files
// 多开路径: /data/data/com.lody.virtual/virtual/data/data/com.your.app/files
for (String virtualPkg : virtualPkgs) {
if (path.contains(virtualPkg)) {
if (callback != null) callback.findSuspect();
return true;
}
}
return false;
}
检测原理图:
正常环境:
/data/data/com.your.app/files
└── 应用包名
多开环境:
/data/data/com.lody.virtual/virtual/data/data/com.your.app/files
└── 多开软件包名 └── 应用包名
4.2.2 包名重复检测 (checkByOriginApkPackageName)
检测原理: 多开软件会 hook getPackageName() 方法。检测系统中是否存在多个相同包名的应用。
public boolean checkByOriginApkPackageName(Context context, VirtualCheckCallback callback) {
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs) {
if (packageName.equals(info.packageName)) {
count++;
}
}
// 正常情况下 count = 1,多开情况下 count > 1
return count > 1;
}
4.2.3 进程Maps检测 (checkByMultiApkPackageName)
检测原理: 被多开的应用运行时会加载多开软件的 SO 库,通过读取 /proc/self/maps 检测。
public boolean checkByMultiApkPackageName(VirtualCheckCallback callback) {
BufferedReader bufr = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = bufr.readLine()) != null) {
for (String pkg : virtualPkgs) {
if (line.contains(pkg)) {
// maps中出现多开软件包名 → 被多开
return true;
}
}
}
return false;
}
/proc/self/maps 文件内容示例:
地址范围 权限 偏移 设备 inode 路径
7f6d5e8000-7f6d5eb000 r-xp 00000000 fd:00 123456 /data/app/com.lody.virtual/lib/arm64/libvirtual.so
4.2.4 UID检测 (checkByHasSameUid)
检测原理: Android 系统中一个 APP 对应一个 UID。如果同一 UID 下有多个进程对应不同的包名,则可能被多开。
public boolean checkByHasSameUid(VirtualCheckCallback callback) {
String filter = getUidStrFormat(); // 获取当前进程的UID格式字符串,如 "u0_a123"
String result = CommandUtil.getSingleInstance().exec("ps"); // 执行ps命令
String[] lines = result.split("\n");
int exitDirCount = 0;
for (String line : lines) {
if (line.contains(filter)) {
// 提取进程名
String processName = extractProcessName(line);
File dataFile = new File("/data/data/" + processName);
if (dataFile.exists()) {
exitDirCount++;
}
}
}
// 同一UID下存在多个私有目录 → 被多开
return exitDirCount > 1;
}
UID解析过程:
private String getUidStrFormat() {
// 1. 读取 /proc/self/cgroup 获取 uid 信息
String filter = exec("cat /proc/self/cgroup");
// 内容示例: "2:cpu:/uid_10123/pid_1234"
// 2. 提取uid数字
int uid = extractUid(filter); // 得到 10123
// 3. 转换为ps命令中的格式
// Android用户空间UID从10000开始,所以 u0_a123 = uid 10123
return String.format("u0_a%d", uid - 10000); // "u0_a123"
}
4.2.5 LocalServerSocket检测 (checkByCreateLocalServerSocket) [推荐]
检测原理: 利用 LocalServerSocket 的唯一性特性。同一个 uniqueMsg 只能创建一个 socket,如果创建失败说明已有另一个实例在运行。
private volatile LocalServerSocket localServerSocket;
public boolean checkByCreateLocalServerSocket(String uniqueMsg, VirtualCheckCallback callback) {
if (localServerSocket != null) return false;
try {
localServerSocket = new LocalServerSocket(uniqueMsg);
return false; // 创建成功 → 没有被多开
} catch (IOException e) {
// 创建失败 → 已有同名socket存在 → 被多开
if (callback != null) callback.findSuspect();
return true;
}
}
使用建议:
- 单进程应用:使用
context.getPackageName()作为uniqueMsg - 多进程应用:使用
SecurityCheckUtil.getCurrentProcessName()作为uniqueMsg
4.2.6 端口监听检测 (checkByPortListening) [已废弃]
检测原理: 通过在 localhost 上建立 TCP 连接,发送暗号识别是否有另一个实例存在。
@Deprecated
public void checkByPortListening(String secret, VirtualCheckCallback callback) {
startClient(secret); // 先尝试连接已存在的服务端
new ServerThread(secret, callback).start(); // 启动服务端监听
}
工作流程:
1. 扫描 /proc/net/tcp6 获取已开启的本地端口
2. 尝试连接每个端口并发送 secret
3. 如果收到相同 secret 的响应 → 被多开
4. 同时启动 ServerSocket 监听,等待其他实例连接
废弃原因: 实现复杂,不如 LocalServerSocket 方案简洁可靠。
4.2.7 TopTask/TopActivity 检测
检测原理: 通过检查顶层 Task 获取真实的包名/Activity名。
public String checkByTopTask(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> rtis = am.getRunningTasks(1);
return rtis.get(0).topActivity.getPackageName();
}
public String checkByTopActivity(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> rtis = am.getRunningTasks(1);
return rtis.get(0).topActivity.getClassName();
}
注意: 此方法需要 GET_TASKS 权限,且在高版本 Android 中受限。
5. 安全检测机制 (SecurityCheckUtil)
文件位置: library/src/main/java/com/lahm/library/SecurityCheckUtil.java
5.1 应用签名获取 (getSignature)
用途: 获取应用签名用于校验应用完整性,防止被篡改。
public String getSignature(Context context) {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
StringBuilder builder = new StringBuilder();
for (Signature signature : signatures) {
builder.append(signature.toCharsString());
}
return builder.toString();
}
5.2 Debug检测
5.2.1 检测Debug版本 (checkIsDebugVersion)
public boolean checkIsDebugVersion(Context context) {
return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
检测原理: 检查 ApplicationInfo.flags 中的 FLAG_DEBUGGABLE 标志位。
5.2.2 检测调试器连接 (checkIsDebuggerConnected)
public boolean checkIsDebuggerConnected() {
return android.os.Debug.isDebuggerConnected();
}
检测原理: 调用 Android SDK 提供的 API 直接检测。
5.2.3 USB充电检测 (checkIsUsbCharging)
public boolean checkIsUsbCharging(Context context) {
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, filter);
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
return chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
}
用途: 辅助判断调试状态(调试时通常会连接USB)。
5.3 Root检测 (isRoot)
public boolean isRoot() {
int secureProp = getroSecureProp();
if (secureProp == 0) // eng/userdebug版本,自带root权限
return true;
else
return isSUExist(); // user版本,检查su文件
}
5.3.1 系统属性检测 (getroSecureProp)
private int getroSecureProp() {
String roSecureObj = CommandUtil.getSingleInstance().getProperty("ro.secure");
if (roSecureObj == null) return 1;
return "0".equals(roSecureObj) ? 0 : 1;
}
检测原理: ro.secure=0 表示系统是 eng 或 userdebug 版本,自带 root 权限。
5.3.2 SU文件检测 (isSUExist)
private boolean isSUExist() {
String[] paths = {
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
};
for (String path : paths) {
File file = new File(path);
if (file.exists()) return true;
}
return false;
}
检测路径说明:
| 路径 | 说明 |
|---|---|
/sbin/su |
系统sbin目录 |
/system/bin/su |
系统bin目录 |
/system/xbin/su |
系统扩展bin目录 |
/data/local/xbin/su |
本地扩展目录 |
/data/local/bin/su |
本地bin目录 |
/system/sd/xbin/su |
SD卡系统目录 |
/system/bin/failsafe/su |
安全模式目录 |
/data/local/su |
本地数据目录 |
5.4 Xposed框架检测
5.4.1 类加载检测 (isXposedExists) [已废弃]
private static final String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers";
private static final String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge";
@Deprecated
public boolean isXposedExists() {
try {
ClassLoader.getSystemClassLoader()
.loadClass(XPOSED_HELPERS)
.newInstance();
} catch (ClassNotFoundException e) {
return false; // 类不存在 → 无Xposed
} catch (InstantiationException | IllegalAccessException e) {
return true; // 类存在但无法实例化 → 有Xposed
}
// 同样检查 XPOSED_BRIDGE 类
return true;
}
5.4.2 异常堆栈检测 (isXposedExistByThrow) [推荐]
public boolean isXposedExistByThrow() {
try {
throw new Exception("gg");
} catch (Exception e) {
for (StackTraceElement element : e.getStackTrace()) {
if (element.getClassName().contains(XPOSED_BRIDGE)) {
return true; // 堆栈中出现Xposed类 → 有Xposed
}
}
return false;
}
}
检测原理: Xposed 通过 AOP 方式 hook 方法,在调用栈中会留下痕迹。主动抛出异常并检查堆栈信息。
5.4.3 尝试禁用Xposed (tryShutdownXposed)
public boolean tryShutdownXposed() {
try {
Field xpdisabledHooks = ClassLoader.getSystemClassLoader()
.loadClass(XPOSED_BRIDGE)
.getDeclaredField("disableHooks");
xpdisabledHooks.setAccessible(true);
xpdisabledHooks.set(null, Boolean.TRUE); // 设置 disableHooks = true
return true;
} catch (Exception e) {
return false;
}
}
检测原理: Xposed 框架有一个 disableHooks 静态变量,设置为 true 可以禁用 hook。
局限性: 如果 Xposed 先 hook 了 isXposedExistByThrow 的返回值,则无法执行后续禁用操作。
5.5 SO库加载检测 (hasReadProcMaps)
public boolean hasReadProcMaps(String paramString) {
Set<String> loadedLibs = new HashSet<>();
BufferedReader reader = new BufferedReader(
new FileReader("/proc/" + Process.myPid() + "/maps"));
String line;
while ((line = reader.readLine()) != null) {
if (line.endsWith(".so") || line.endsWith(".jar")) {
loadedLibs.add(line.substring(line.lastIndexOf(" ") + 1));
}
}
for (String lib : loadedLibs) {
if (lib.contains(paramString)) {
return true; // 找到目标SO库
}
}
return false;
}
用途: 检测是否加载了特定的 SO 库(如恶意注入的库)。
5.6 进程追踪检测 (readProcStatus)
public boolean readProcStatus() {
BufferedReader reader = new BufferedReader(
new FileReader("/proc/" + Process.myPid() + "/status"));
String tracerPid = "";
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("TracerPid")) {
tracerPid = line.substring(line.indexOf(":") + 1).trim();
break;
}
}
return !"0".equals(tracerPid); // TracerPid != 0 表示正在被追踪
}
/proc/[pid]/status 文件示例:
Name: com.lahm.app
State: S (sleeping)
Tgid: 12345
Pid: 12345
PPid: 1
TracerPid: 0 ← 0表示未被追踪,非0表示追踪进程的PID
...
5.7 端口检测
public boolean isLocalPortUsing(int port) {
return isPortUsing("127.0.0.1", port);
}
public boolean isPortUsing(String host, int port) throws UnknownHostException {
InetAddress address = InetAddress.getByName(host);
try {
Socket socket = new Socket(address, port);
return true; // 能连接 → 端口被占用
} catch (IOException e) {
return false; // 连接失败 → 端口未被占用
}
}
用途: 检测调试端口(如 adb 默认端口 5555)是否被占用。
5.8 获取当前进程名
public String getCurrentProcessName() {
FileInputStream fis = new FileInputStream("/proc/self/cmdline");
byte[] buffer = new byte[256];
int len = 0;
int b;
while ((b = fis.read()) > 0 && len < buffer.length) {
buffer[len++] = (byte) b;
}
return new String(buffer, 0, len, "UTF-8");
}
用途: 在多进程应用中获取当前进程名,用于区分不同进程。
6. Native层反调试检测 (antitrace.cpp)
文件位置: library/src/main/jni/antitrace.cpp
6.1 整体架构
// JNI入口点
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// 1. 可选:自我附加防止被其他进程附加
// ptrace(PTRACE_TRACEME, 0, 0, 0); // 已注释
// 2. 启动循环检测线程
create_thread_check_traceid();
return JNI_VERSION_1_6;
}
6.2 TracerPid检测线程
void *thread_function(void *argv) {
int pid = getpid();
char file_name[20] = {'\0'};
sprintf(file_name, "/proc/%d/status", pid);
while (1) {
FILE *fp = fopen(file_name, "r");
// 读取第6行(TracerPid所在行)
char linestr[256];
for (int i = 0; i <= 5; i++) {
fgets(linestr, 256, fp);
}
// 提取TracerPid数值
int traceid = get_number_for_str(linestr);
if (traceid > 1000) {
// 被追踪且追踪者PID > 1000
// 华为P9会主动附加系统进程,PID < 1000,所以设置阈值
exit(0); // 直接退出进程
}
fclose(fp);
sleep(5); // 每5秒检测一次
}
return ((void *) 0);
}
6.3 数字提取函数
int get_number_for_str(char *str) {
if (str == NULL) return -1;
char result[20];
int count = 0;
while (*str != '\0') {
if (*str >= 48 && *str <= 57) { // ASCII码 '0'-'9'
result[count++] = *str;
}
str++;
}
return atoi(result);
}
6.4 检测流程图
┌─────────────────────────────────────────────────────────────────┐
│ JNI_OnLoad 被调用 │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ create_thread_check_traceid() │
│ 创建检测线程 │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ thread_function │
│ (循环检测线程) │
└────────────────────┬───────────────────┘
│
▼
┌────────────────────────────────────────┐
│ 读取 /proc/[pid]/status 文件 │
└────────────────────┬───────────────────┘
│
▼
┌────────────────────────────────────────┐
│ 解析 TracerPid 字段值 │
└────────────────────┬───────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ TracerPid > 1000│ │ TracerPid ≤ 1000│
│ 被恶意追踪 │ │ 正常/系统 │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ exit(0) │ │ sleep(5) │
│ 退出进程 │ │ 继续循环 │
└─────────────────┘ └─────────────────┘
7. 无障碍服务检测 (AccessibilityServicesCheckUtil)
文件位置: library/src/main/java/com/lahm/library/AccessibilityServicesCheckUtil.java
7.1 检测指定包名的无障碍服务
public boolean checkAccessibilityEnabled(Context context, String packageName) {
// 1. 检查无障碍服务是否开启
int hasSetSetting = Settings.Secure.getInt(
context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_ENABLED);
if (hasSetSetting == 1) {
// 2. 获取已启用的无障碍服务列表
String settingValue = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
// 3. 解析服务列表(以:分隔)
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(':');
splitter.setString(settingValue);
// 4. 检查是否包含目标包名
while (splitter.hasNext()) {
String accessibilityService = splitter.next();
if (accessibilityService.contains(packageName)) {
return true;
}
}
}
return false;
}
7.2 跳转到无障碍设置
public Intent go2SetAccessibilityService() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_ACCESSIBILITY_SETTINGS);
return intent;
}
8. 辅助工具类
8.1 CommandUtil - 命令执行工具
文件位置: library/src/main/java/com/lahm/library/CommandUtil.java
8.1.1 获取系统属性
public String getProperty(String propName) {
try {
Object roSecureObj = Class.forName("android.os.SystemProperties")
.getMethod("get", String.class)
.invoke(null, propName);
return (String) roSecureObj;
} catch (Exception e) {
return null;
}
}
实现原理: 通过反射调用隐藏 API android.os.SystemProperties.get()。
8.1.2 执行Shell命令
public String exec(String command) {
Process process = Runtime.getRuntime().exec("sh");
BufferedOutputStream bos = new BufferedOutputStream(process.getOutputStream());
bos.write(command.getBytes());
bos.write('\n');
bos.flush();
bos.close();
process.waitFor();
BufferedInputStream bis = new BufferedInputStream(process.getInputStream());
return readFromStream(bis);
}
8.2 NDKUtil - Native库加载工具
文件位置: library/src/main/java/com/lahm/library/NDKUtil.java
public static void loadLibrariesOnce(LibLoader libLoader) {
synchronized (NDKUtil.class) {
if (!mIsLibLoaded) {
if (libLoader == null) {
libLoader = new LibLoader() {
public void loadLibrary(String libName) {
System.loadLibrary(libName);
}
};
}
libLoader.loadLibrary("antitrace");
mIsLibLoaded = true;
}
}
}
9. 应用层实现与配置
9.1 TestApplication - 应用启动时的安全防护
文件位置: app/src/main/java/com/lahm/easyprotector/TestApplication.java
应用在启动时即进行安全检测和防护,这是推荐的最佳实践。
public class TestApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 应用启动时立即尝试禁用 Xposed 框架
EasyProtectorLib.checkXposedExistAndDisableIt();
// 可选:在启动时检测多开环境并退出
// EasyProtectorLib.checkIsRunningInVirtualApk(getPackageName(), new VirtualCheckCallback() {
// @Override
// public void findSuspect() {
// System.exit(0); // 检测到多开直接退出
// }
// });
}
}
关键设计点:
| 时机 | 操作 | 目的 |
|---|---|---|
| Application.onCreate | 禁用Xposed | 在任何Activity启动前禁用hook |
| Application.onCreate | 检测多开 | 创建LocalServerSocket占位 |
| 检测到威胁 | System.exit(0) | 立即终止应用 |
启动时检测的优势:
- 最早时机执行,减少被绕过的可能
- 在任何业务代码执行前完成检测
- LocalServerSocket需要尽早创建以占位
9.2 TestAccessibilityService - 无障碍服务演示
文件位置: app/src/main/java/com/lahm/easyprotector/TestAccessibilityService.java
这是一个无障碍服务的示例实现,展示了自动安装APK的功能(与环境检测无直接关系,但展示了无障碍服务的能力)。
public class TestAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: // 通知栏事件
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: // 页面切换事件
break;
}
// 检测是否为系统安装器
if (event.getPackageName().toString().equals("com.android.packageinstaller")) {
installAPK(event);
}
}
private void installAPK(AccessibilityEvent event) {
AccessibilityNodeInfo source = getRootInActiveWindow();
// 自动点击"下一步"按钮
List<AccessibilityNodeInfo> nextInfos = source.findAccessibilityNodeInfosByText("下一步");
nextClick(nextInfos);
// 自动点击"安装"按钮
List<AccessibilityNodeInfo> installInfos = source.findAccessibilityNodeInfosByText("安装");
nextClick(installInfos);
// 自动点击"打开"按钮
List<AccessibilityNodeInfo> openInfos = source.findAccessibilityNodeInfosByText("打开");
nextClick(openInfos);
}
private void nextClick(List<AccessibilityNodeInfo> infos) {
if (infos != null) {
for (AccessibilityNodeInfo info : infos) {
if (info.isEnabled() && info.isClickable()) {
info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
}
}
功能说明:
- 监听系统安装器(
com.android.packageinstaller)的界面事件 - 自动查找并点击"下一步"、"安装"、"打开"按钮
- 实现APK静默安装的自动化
9.3 AndroidManifest.xml 配置分析
文件位置: app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lahm.easyprotector">
<!-- 网络权限:用于端口检测 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 无障碍服务绑定权限 -->
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<application
android:name=".TestApplication"
android:allowBackup="true"
...>
<!-- 主Activity -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 无障碍服务声明 -->
<service
android:name=".TestAccessibilityService"
android:enabled="true"
android:exported="true"
android:label="@string/access_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/test_access_service_config" />
</service>
</application>
</manifest>
权限分析:
| 权限 | 用途 | 必要性 |
|---|---|---|
INTERNET |
端口检测、Socket通信 | 可选 |
BIND_ACCESSIBILITY_SERVICE |
无障碍服务 | 仅演示用 |
注意: 核心检测功能不需要任何特殊权限,这是该库的设计亮点之一。
9.4 无障碍服务配置文件
文件位置: app/src/main/res/xml/test_access_service_config.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:canRetrieveWindowContent="true"
android:description="@string/access_description"
android:notificationTimeout="100"
android:packageNames="com.lahm.easyprotector,com.android.packageinstaller" />
配置参数详解:
| 参数 | 值 | 含义 |
|---|---|---|
accessibilityEventTypes |
typeAllMask |
监听所有类型的无障碍事件 |
accessibilityFeedbackType |
feedbackGeneric |
通用反馈类型 |
canRetrieveWindowContent |
true |
允许访问活动窗口内容 |
notificationTimeout |
100 |
事件响应超时时间(毫秒) |
packageNames |
两个包名 | 只监听指定应用的事件 |
9.5 MainActivity - 测试界面实现
文件位置: app/src/main/java/com/lahm/easyprotector/MainActivity.java
主界面提供了所有检测功能的测试按钮:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 多开检测按钮
findViewById(R.id.checkByPrivateFilePath).setOnClickListener(this);
findViewById(R.id.checkByOriginApkPackageName).setOnClickListener(this);
findViewById(R.id.checkByMultiApkPackageName).setOnClickListener(this);
findViewById(R.id.checkByHasSameUid).setOnClickListener(this);
findViewById(R.id.checkByPortListening).setOnClickListener(this);
findViewById(R.id.checkByCreateLocalServerSocket).setOnClickListener(this);
// 安全检测按钮
findViewById(R.id.checkRoot).setOnClickListener(this);
findViewById(R.id.checkDebuggable).setOnClickListener(this);
findViewById(R.id.checkDebuggerAttach).setOnClickListener(this);
findViewById(R.id.checkTracer).setOnClickListener(this);
findViewById(R.id.checkXP).setOnClickListener(this);
// 模拟器检测按钮
findViewById(R.id.readSysProperty).setOnClickListener(this);
}
}
测试功能矩阵:
| 按钮ID | 功能 | 调用方法 |
|---|---|---|
checkByPrivateFilePath |
私有路径检测 | VirtualApkCheckUtil.checkByPrivateFilePath() |
checkByOriginApkPackageName |
包名检测 | VirtualApkCheckUtil.checkByOriginApkPackageName() |
checkByMultiApkPackageName |
Maps文件检测 | VirtualApkCheckUtil.checkByMultiApkPackageName() |
checkByHasSameUid |
UID检测 | VirtualApkCheckUtil.checkByHasSameUid() |
checkByPortListening |
端口监听检测 | VirtualApkCheckUtil.checkByPortListening() |
checkByCreateLocalServerSocket |
Socket检测 | EasyProtectorLib.checkIsRunningInVirtualApk() |
checkRoot |
Root检测 | EasyProtectorLib.checkIsRoot() |
checkDebuggable |
Debug版本检测 | EasyProtectorLib.checkIsDebug() |
checkDebuggerAttach |
调试器连接检测 | SecurityCheckUtil.checkIsDebuggerConnected() |
checkTracer |
C层追踪检测 | EasyProtectorLib.checkIsBeingTracedByC() |
checkXP |
Xposed检测 | EasyProtectorLib.checkIsXposedExist() |
readSysProperty |
模拟器检测 | EasyProtectorLib.checkIsRunningInEmulator() |
9.6 测试界面布局
文件位置: app/src/main/res/layout/activity_main.xml
界面采用垂直滚动布局,分为多个检测区域:
┌─────────────────────────────────────────────┐
│ VirtualApp 检测区域 │
├─────────────────────────────────────────────┤
│ [checkByPrivateFilePath] 结果显示 │
│ [checkByOriginApkPackageName] 结果显示 │
│ [checkByMultiApkPackageName] 结果显示 │
│ [checkByHasSameUid] 结果显示 │
│ [checkByPortListening] 结果显示 │
│ [checkByCreateLocalServerSocket] 结果显示 │
├─────────────────────────────────────────────┤
│ Root 检测区域 │
├─────────────────────────────────────────────┤
│ [checkRoot] 结果显示 │
├─────────────────────────────────────────────┤
│ Debug 检测区域 │
├─────────────────────────────────────────────┤
│ [checkDebuggable] 结果显示 │
│ [checkDebuggerAttach] 结果显示 │
│ [checkTracer] 结果显示 │
├─────────────────────────────────────────────┤
│ XPosed Framework 检测区域 │
├─────────────────────────────────────────────┤
│ [checkXP] 结果显示 │
├─────────────────────────────────────────────┤
│ Emulator info 检测区域 │
├─────────────────────────────────────────────┤
│ [click me emulator info] │
│ 详细模拟器信息输出区域 │
├─────────────────────────────────────────────┤
│ [for test] │
└─────────────────────────────────────────────┘
10. 项目构建与编译配置
10.1 项目结构
EasyProtector/
├── app/ # 演示应用模块
│ ├── build.gradle # 应用构建配置
│ ├── proguard-rules.pro # 应用混淆规则
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/ # Java源码
│ └── res/ # 资源文件
├── library/ # 核心库模块
│ ├── build.gradle # 库构建配置
│ ├── CMakeLists.txt # Native编译配置
│ ├── proguard-rules.pro # 库混淆规则
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/ # Java源码
│ └── jni/ # Native源码
├── build.gradle # 根项目配置
├── settings.gradle # 模块包含配置
└── gradle.properties # Gradle属性配置
10.2 根项目 build.gradle
文件位置: build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.novoda:bintray-release:0.9.2' // 用于发布到bintray
}
}
allprojects {
repositories {
google()
jcenter()
}
}
10.3 Library模块 build.gradle
文件位置: library/build.gradle
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 16 // 最低支持 Android 4.1
targetSdkVersion 29
versionCode gitVersionCode()
versionName gitVersionName()
ndk {
abiFilters 'armeabi-v7a','arm64-v8a' // 支持的CPU架构
}
}
// Native编译配置
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
}
// 通过git commit数获取版本号
def static gitVersionCode() {
def cmd = 'git rev-list HEAD --count'
10000 + cmd.execute().text.trim().toInteger()
}
// 通过git tag获取版本名
def static gitVersionName() {
def cmd = 'git describe --tags'
def tag = cmd.execute().text.trim()
def arr = tag.split("-")
return arr[0]
}
版本管理策略:
| 方法 | 命令 | 计算公式 | 示例 |
|---|---|---|---|
gitVersionCode() |
git rev-list HEAD --count |
10000 + commit数 | 10150 |
gitVersionName() |
git describe --tags |
最近的tag名 | v1.1.1 |
10.4 CMakeLists.txt - Native编译配置
文件位置: library/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 编译 antitrace.so 共享库
add_library(
antitrace # 库名称(不需要lib前缀)
SHARED # 共享库类型
src/main/jni/antitrace.cpp # 源文件
)
# 查找log库(用于在C/C++中打印日志)
find_library(
log-lib
log
)
# 将log库链接到antitrace
target_link_libraries(
antitrace
${log-lib}
)
编译输出:
lib/armeabi-v7a/libantitrace.so- 32位ARM架构lib/arm64-v8a/libantitrace.so- 64位ARM架构
10.5 App模块 build.gradle
文件位置: app/build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion = '29.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "com.lahm.easyprotector"
minSdkVersion 19 // 演示App最低支持 Android 4.4
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation project(':library') // 依赖library模块
}
10.6 Library混淆规则
文件位置: library/proguard-rules.pro
# 基础配置
-dontskipnonpubliclibraryclasses # 不忽略非公共的库类
-optimizationpasses 5 # 代码压缩级别
-dontusemixedcaseclassnames # 不使用大小写混合
-dontpreverify # 不做预校验
-verbose # 记录日志
-keepattributes *Annotation* # 保持注解
-ignorewarning # 忽略警告
-dontoptimize # 不优化
# 混淆算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
# 生成调试文件
-dump class_files.txt # APK内所有class的内部结构
-printseeds seeds.txt # 未混淆的类和成员
-printusage unused.txt # 未被使用的代码
-printmapping mapping.txt # 混淆前后的映射
# 保持规则
-keep class android.support.**
-keep class * extends android.support.**
-dontwarn android.support.**
# 保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 不混淆 Serializable
-keepnames class * implements java.io.Serializable
# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
关键混淆保护:
| 规则 | 目的 |
|---|---|
-keepclasseswithmembernames ... native |
保护JNI方法名不被混淆 |
-keepattributes *Annotation* |
保持注解(反射需要) |
-printmapping mapping.txt |
生成混淆映射(用于堆栈还原) |
10.7 SDK版本兼容性
| 配置项 | Library | App | 说明 |
|---|---|---|---|
| minSdkVersion | 16 | 19 | 库支持Android 4.1+ |
| targetSdkVersion | 29 | 29 | 目标Android 10 |
| compileSdkVersion | 29 | 29 | 编译SDK版本 |
10.8 CPU架构支持
ndk {
abiFilters 'armeabi-v7a','arm64-v8a'
}
| 架构 | 说明 | 覆盖设备 |
|---|---|---|
armeabi-v7a |
32位ARM | 绝大多数Android手机 |
arm64-v8a |
64位ARM | 新款Android手机 |
未支持的架构:
x86/x86_64- Intel模拟器(检测库本身要对抗模拟器,不需要支持)armeabi- 旧版ARM(Android 4.0以下,已弃用)
11. 检测流程图
11.1 完整检测流程
┌─────────────────────────────────────┐
│ EasyProtectorLib │
│ (统一入口) │
└─────────────────┬───────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ EmulatorCheck │ │ VirtualApkCheck │ │ SecurityCheck │
│ 模拟器检测 │ │ 多开环境检测 │ │ 安全检测 │
└────────┬────────┘ └──────────┬──────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│• 系统属性检测 │ │• 私有路径检测 │ │• Root检测 │
│• 硬件特征检测 │ │• 包名重复检测 │ │• Xposed检测 │
│• 传感器检测 │ │• Maps文件检测 │ │• Debug检测 │
│• 功能支持检测 │ │• UID检测 │ │• 追踪检测 │
│• 进程组检测 │ │• Socket检测 │ │• 签名检测 │
└─────────────────┘ └─────────────────────┘ └──────────────────┘
│ │ │
└───────────────────────────┼──────────────────────────┘
│
▼
┌─────────────────────────┐
│ NDKUtil (Native层) │
│ C层追踪检测 │
└─────────────────────────┘
11.2 模拟器检测决策树
开始检测
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
系统属性检测 硬件特征检测 软件特征检测
│ │ │
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
hardware flavor 传感器数量 光线传感器 已安装APP数 相机/蓝牙
model board (≤7) (无) (≤5) (不支持)
manufacturer platform
baseband
│ │ │ │ │ │
└───────────────┴────┴───────────────┴────┴───────────────┘
│
▼
┌─────────────────────────┐
│ 计算嫌疑值 suspectCount │
└────────────┬────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
suspectCount > 3 suspectCount ≤ 3
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 是模拟器 │ │ 是真机 │
└─────────────┘ └─────────────┘
12. 检测结果判定逻辑
12.1 模拟器检测判定表
| 检测维度 | 检测方法 | 确定是模拟器的特征值 | 嫌疑值 |
|---|---|---|---|
| 硬件名称 | ro.hardware |
ttvm/nox/cancro/intel/vbox/vbox86/android_x86 | null→+1 |
| 渠道信息 | ro.build.flavor |
包含vbox或sdk_gphone | null→+1 |
| 设备型号 | ro.product.model |
包含google_sdk/emulator/android sdk built for x86 | null→+1 |
| 制造商 | ro.product.manufacturer |
包含genymotion/netease | null→+1 |
| 主板名称 | ro.product.board |
包含android/goldfish | null→+1 |
| 平台信息 | ro.board.platform |
包含android | null→+1 |
| 基带信息 | gsm.version.baseband |
包含1.0.0.0 | null→+2 |
| 传感器数量 | SensorManager | - | ≤7→+1 |
| 第三方APP数 | pm list package -3 | - | ≤5→+1 |
| 闪光灯 | FEATURE_CAMERA_FLASH | - | 不支持→+1 |
| 相机 | FEATURE_CAMERA | - | 不支持→+1 |
| 蓝牙 | FEATURE_BLUETOOTH | - | 不支持→+1 |
| 光线传感器 | TYPE_LIGHT | - | 无→+1 |
| 进程组 | /proc/self/cgroup | - | null→+1 |
最终判定: suspectCount > 3 → 模拟器
12.2 多开环境检测判定表
| 检测方法 | 判定为多开的条件 | 推荐程度 |
|---|---|---|
| checkByPrivateFilePath | 私有目录路径包含多开软件包名 | ★★★ |
| checkByOriginApkPackageName | 系统中存在多个相同包名的应用 | ★★★ |
| checkByMultiApkPackageName | /proc/self/maps包含多开软件包名 | ★★★ |
| checkByHasSameUid | 同一UID下存在多个私有目录 | ★★☆ |
| checkByCreateLocalServerSocket | 创建LocalServerSocket失败 | ★★★★★ |
| checkByPortListening | 端口通信检测到相同secret | ★☆☆ (已废弃) |
12.3 Root检测判定表
| 检测项 | 判定为Root的条件 |
|---|---|
| ro.secure | = 0 (eng/userdebug版本) |
| su文件 | 以下路径任一存在su文件 |
su文件检测路径:
- /sbin/su
- /system/bin/su
- /system/xbin/su
- /data/local/xbin/su
- /data/local/bin/su
- /system/sd/xbin/su
- /system/bin/failsafe/su
- /data/local/su
12.4 Xposed检测判定表
| 检测方法 | 判定为存在Xposed的条件 |
|---|---|
| isXposedExists | XposedHelpers或XposedBridge类存在 |
| isXposedExistByThrow | 异常堆栈中包含XposedBridge |
| tryShutdownXposed | 能成功设置disableHooks字段 |
12.5 追踪检测判定表
| 检测层 | 检测方法 | 判定为被追踪的条件 |
|---|---|---|
| Java层 | readProcStatus | TracerPid ≠ 0 |
| Native层 | thread_function | TracerPid > 1000 |
13. 总结
13.1 检测能力矩阵
| 检测类型 | Java层 | Native层 | 检测手段数量 |
|---|---|---|---|
| 模拟器 | ✓ | ✗ | 14种 |
| 多开/虚拟环境 | ✓ | ✗ | 6种 |
| Root | ✓ | ✗ | 2种 |
| Xposed | ✓ | ✗ | 3种 |
| 调试 | ✓ | ✓ | 3种 |
| 进程追踪 | ✓ | ✓ | 2种 |
13.2 核心技术点总结
- 系统属性读取: 通过反射调用
SystemProperties.get()读取系统属性 - 文件系统分析: 读取
/proc/self/maps、/proc/[pid]/status等文件 - Shell命令执行: 通过
Runtime.exec()执行 ps、pm 等命令 - 硬件特征检测: 通过
PackageManager.hasSystemFeature()和SensorManager - LocalServerSocket: 利用其唯一性特性检测多开
- 异常堆栈分析: 通过主动抛异常检测 Xposed hook
- Native层循环检测: 后台线程持续监控 TracerPid
13.3 检测策略特点
- 多维度检测: 从多个角度检测,提高准确率
- 嫌疑值累计: 不依赖单一特征,综合判断
- Java+Native双层: 提高反逆向难度
- 回调机制: 提供检测结果详情,方便调试
13.4 已知局限性
- 模拟器检测: 新版模拟器可能修改系统属性绕过检测
- Root检测: 可通过隐藏su文件或hook文件存在检测绕过
- Xposed检测: EdXposed/LSPosed 等新框架可能有不同特征
- 多开检测: 多开软件持续更新,包名列表需要维护
- Native检测: TracerPid > 1000 的阈值在某些设备上可能误判
13.5 项目测试数据
根据项目 image/v1.1.1/ 目录中的截图,该库已在以下环境中测试:
| 测试环境 | 版本 | 测试结果 |
|---|---|---|
| AS模拟器 | Android 9.0 | 可检测 |
| MUMU模拟器 | 1.1.0.2 | 可检测 |
| 夜神模拟器 | 6.5.0.0 | 可检测 |
| 腾讯手游助手 | 1.0.10158.123 | 可检测 |
| 逍遥模拟器 | 7.0.2 | 可检测 |
| 雷电模拟器 | 3.75 | 可检测 |
| OPPO R9tm | Android 5.1 ColorOS 3.0 | 真机识别 |
| VIVO Y76A | Android 6.0 FuntouchOS 2.6 | 真机识别 |
| 华为P9 | Android 8.0 EMUI 8.0 | 真机识别 |
附录A: 完整项目文件清单
EasyProtector-master/
│
├── .gitignore # Git忽略配置
├── LICENSE # Apache 2.0 开源协议
├── README.md # 项目说明文档
├── build.gradle # 根项目Gradle配置
├── gradle.properties # Gradle属性配置
├── settings.gradle # 模块包含配置 (include ':app', ':library')
│
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar # Gradle Wrapper JAR
│ └── gradle-wrapper.properties # Gradle Wrapper配置
│
├── image/
│ └── v1.1.1/ # 测试截图
│ ├── AS模拟器9.0.png
│ ├── MUMU模拟器1.1.0.2.png
│ ├── OPPO R9tm 5.1 colorOS3.0.png
│ ├── VIVO Y76A 6.0 FuntouchOS2.6.png
│ ├── 华为P9 8.0.0 EMUI8.0.0.jpg
│ ├── 夜神模拟器6.5.0.0.png
│ ├── 腾讯手游助手 1.0.10158.123.png
│ ├── 逍遥模拟器7.0.2.png
│ └── 雷电模拟器3.75.png
│
├── app/ # 演示应用模块
│ ├── .gitignore
│ ├── build.gradle # App构建配置
│ ├── proguard-rules.pro # App混淆规则
│ └── src/main/
│ ├── AndroidManifest.xml # App清单文件
│ ├── java/com/lahm/easyprotector/
│ │ ├── MainActivity.java # 测试主界面
│ │ ├── TestAccessibilityService.java # 无障碍服务示例
│ │ └── TestApplication.java # Application类
│ └── res/
│ ├── drawable/
│ │ └── ic_launcher_background.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── layout/
│ │ └── activity_main.xml # 主界面布局
│ ├── mipmap-*/ # 应用图标
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings.xml # 字符串资源
│ │ └── styles.xml
│ └── xml/
│ └── test_access_service_config.xml # 无障碍服务配置
│
└── library/ # 核心检测库模块
├── .gitignore
├── build.gradle # Library构建配置
├── CMakeLists.txt # Native编译配置
├── proguard-rules.pro # Library混淆规则
└── src/main/
├── AndroidManifest.xml # Library清单文件
├── java/com/lahm/library/
│ ├── EasyProtectorLib.java # [核心] 统一入口类
│ ├── EmulatorCheckUtil.java # [核心] 模拟器检测 (14种方法)
│ ├── VirtualApkCheckUtil.java # [核心] 多开检测 (6种方法)
│ ├── SecurityCheckUtil.java # [核心] 安全检测 (Root/Xposed/Debug)
│ ├── AccessibilityServicesCheckUtil.java # 无障碍服务检测
│ ├── NDKUtil.java # Native库加载工具
│ ├── CommandUtil.java # Shell命令执行工具
│ ├── CheckResult.java # 检测结果封装类
│ ├── EmulatorCheckCallback.java # 模拟器检测回调接口
│ ├── VirtualCheckCallback.java # 多开检测回调接口
│ └── LibLoader.java # 库加载器接口
├── jni/
│ └── antitrace.cpp # [核心] Native层反调试检测
└── res/values/
└── strings.xml
附录B: 所有检测方法索引
B.1 按检测类型分类
模拟器检测 (EmulatorCheckUtil - 14种):
checkFeaturesByHardware()- ro.hardware属性检测checkFeaturesByFlavor()- ro.build.flavor属性检测checkFeaturesByModel()- ro.product.model属性检测checkFeaturesByManufacturer()- ro.product.manufacturer属性检测checkFeaturesByBoard()- ro.product.board属性检测checkFeaturesByPlatform()- ro.board.platform属性检测checkFeaturesByBaseBand()- gsm.version.baseband属性检测getSensorNumber()- 传感器数量检测getUserAppNumber()- 第三方应用数量检测supportCamera()- 相机支持检测supportCameraFlash()- 闪光灯支持检测supportBluetooth()- 蓝牙支持检测hasLightSensor()- 光线传感器检测checkFeaturesByCgroup()- 进程组信息检测
多开/虚拟环境检测 (VirtualApkCheckUtil - 7种):
checkByPrivateFilePath()- 私有文件路径检测checkByOriginApkPackageName()- 包名重复检测checkByMultiApkPackageName()- Maps文件检测checkByHasSameUid()- UID检测checkByCreateLocalServerSocket()- LocalServerSocket检测 [推荐]checkByPortListening()- 端口监听检测 [已废弃]checkByTopTask()/checkByTopActivity()- TopTask检测
安全检测 (SecurityCheckUtil):
getSignature()- 应用签名获取checkIsDebugVersion()- Debug版本检测checkIsDebuggerConnected()- 调试器连接检测checkIsUsbCharging()- USB充电检测isRoot()- Root权限检测isXposedExists()- Xposed类加载检测 [已废弃]isXposedExistByThrow()- Xposed堆栈检测 [推荐]tryShutdownXposed()- 尝试禁用XposedhasReadProcMaps()- SO库加载检测readProcStatus()- TracerPid检测isPortUsing()- 端口占用检测getCurrentProcessName()- 当前进程名获取
Native层检测 (antitrace.cpp):
thread_function()- 循环检测TracerPid
B.2 按检测技术分类
| 技术类型 | 使用的方法 |
|---|---|
| 系统属性读取 | 模拟器检测的7个属性检测方法 |
| 文件系统分析 | hasReadProcMaps, readProcStatus, checkByMultiApkPackageName |
| 硬件特征检测 | getSensorNumber, hasLightSensor, supportCamera等 |
| 包管理器API | checkByOriginApkPackageName, supportBluetooth等 |
| 反射调用 | CommandUtil.getProperty, isXposedExists, tryShutdownXposed |
| Socket通信 | checkByCreateLocalServerSocket, checkByPortListening |
| Shell命令 | getUserAppNumber, checkByHasSameUid |
| 异常分析 | isXposedExistByThrow |
| Native检测 | antitrace.cpp中的TracerPid循环检测 |
附录C: 系统属性检测完整列表
| 属性名 | 用途 | 模拟器典型值 |
|---|---|---|
ro.hardware |
硬件名称 | ttvm/nox/cancro/intel/vbox/vbox86/android_x86 |
ro.build.flavor |
编译渠道 | 含vbox/sdk_gphone |
ro.product.model |
设备型号 | 含google_sdk/emulator/android sdk built for x86 |
ro.product.manufacturer |
制造商 | 含genymotion/netease |
ro.product.board |
主板名称 | 含android/goldfish |
ro.board.platform |
平台名称 | 含android |
gsm.version.baseband |
基带版本 | null/1.0.0.0 |
ro.secure |
安全模式 | 0 (eng/userdebug版本) |
ro.debuggable |
可调试标志 | 1 |
附录D: 已知多开软件包名列表
| 包名 | 软件名称 |
|---|---|
com.bly.dkplat |
多开分身 |
com.by.chaos |
Chaos引擎 |
com.lbe.parallel |
平行空间 |
com.excelliance.dualaid |
双开助手 |
com.lody.virtual |
VirtualXposed / VirtualApp |
com.qihoo.magic |
360分身大师 |