""" 设备环境守卫 — Agent 启动时执行全面自检 检测项: 1. Root 隐藏状态(Shamiko / MagiskHide) 2. Frida 反检测状态(进程名/端口/内存特征) 3. 设备指纹唯一性(上报并校验碰撞) 4. 模拟器检测(Build属性/传感器/电池/特征文件) 5. 无障碍服务状态 6. SELinux 状态 7. 关键 App 安装隐藏(HMA-OSS 检测) """ import hashlib import logging import os import re import time from typing import Dict, Any, List, Tuple logger = logging.getLogger(__name__) class DeviceGuard: """设备环境守卫 — 启动自检 + 持续巡检""" # 不应被目标 App 看到的包名 HIDDEN_PACKAGES = [ "com.topjohnwu.magisk", "io.github.vvb2060.magisk", "me.weishu.kernelsu", "org.lsposed.manager", "moe.shizuku.privileged.api", "eu.chainfire.supersu", "com.noshufou.android.su", "com.koushikdutta.superuser", "com.termux", ] # 模拟器 Build 特征 EMU_FINGERPRINTS = [ "goldfish", "generic", "vbox", "sdk_gphone", "Andy", "Droid4X", "nox", "bluestacks", "genymotion", "ttVM_Hdragon", ] # Frida 默认特征 FRIDA_INDICATORS = [ "/data/local/tmp/frida-server", "/data/local/tmp/re.frida.server", ] def __init__(self, device): """ Args: device: uiautomator2 设备对象 """ self.d = device self.report: Dict[str, Any] = {} # ------------------------------------------------------------------ # 主入口 # ------------------------------------------------------------------ def run_full_check(self) -> Dict[str, Any]: """执行全面自检,返回结果报告""" logger.info("🛡️ 设备环境自检开始...") start = time.time() checks: List[Tuple[str, callable]] = [ ("root_hidden", self._check_root_hidden), ("frida_stealth", self._check_frida_stealth), ("fingerprint", self._collect_fingerprint), ("emulator", self._check_emulator), ("accessibility", self._check_accessibility), ("selinux", self._check_selinux), ("app_hidden", self._check_app_hidden), ("dangerous_props", self._check_dangerous_props), ] results = {} warnings = [] for name, fn in checks: try: result = fn() results[name] = result if result.get("warning"): warnings.append(f"{name}: {result['warning']}") logger.warning(f" ⚠️ {name}: {result['warning']}") else: logger.info(f" ✅ {name}: OK") except Exception as e: results[name] = {"ok": False, "error": str(e)} warnings.append(f"{name}: {e}") logger.error(f" ❌ {name}: {e}") elapsed = time.time() - start self.report = { "ok": len(warnings) == 0, "warnings": warnings, "warning_count": len(warnings), "checks": results, "elapsed_ms": int(elapsed * 1000), "timestamp": int(time.time()), } # AB-04:量产环境红灯分级。联调态(dev)暴露记为黄,量产态(prod)关键暴露记为红。 self.report["redlight"] = self._classify_redlight(results) if warnings: logger.warning(f"🛡️ 自检完成 — {len(warnings)} 项警告 ({elapsed:.1f}s)") else: logger.info(f"🛡️ 自检全部通过 ({elapsed:.1f}s)") return self.report def _classify_redlight(self, results: Dict[str, Any]) -> Dict[str, Any]: """量产环境红灯分级(AB-04)。 env:`WP_ENV=prod` 视为量产态,关键暴露 → 红灯(应阻断上线); 否则联调态(dev),同样的暴露只记黄灯(允许联调,但仍提示)。 关键暴露 = Frida 默认特征 / su 可见 / ro.debuggable=1 / adbd running。 """ env = (os.environ.get("WP_ENV") or os.environ.get("WORKPHONE_ENV") or "dev").lower() is_prod = env in ("prod", "production", "release") critical: List[str] = [] frida = results.get("frida_stealth") or {} if not frida.get("ok", True): critical.append(f"frida_stealth: {frida.get('warning', '默认特征暴露')}") root = results.get("root_hidden") or {} if root.get("warning", "").startswith("su 二进制可见"): critical.append(f"root_hidden: {root['warning']}") props = (results.get("dangerous_props") or {}).get("dangerous_props") or {} if props.get("ro.debuggable"): critical.append("dangerous_props: ro.debuggable=1") if props.get("adb_running"): critical.append("dangerous_props: adbd running") level = "green" if critical: level = "red" if is_prod else "yellow" return { "env": env, "is_prod": is_prod, "level": level, "critical_exposures": critical, "advice": ( "量产红灯:关键暴露须先隐藏(Shamiko/改名改端口/关 adbd/ro.debuggable=0)再上线" if level == "red" else ("联调态黄灯:暴露项不阻断联调,量产前须清零" if level == "yellow" else "环境干净") ), } # ------------------------------------------------------------------ # 1. Root 隐藏检测 # ------------------------------------------------------------------ def _check_root_hidden(self) -> dict: result = {"ok": True} # su 二进制是否可被常规方式发现 su_paths = ["/system/bin/su", "/system/xbin/su", "/sbin/su"] for p in su_paths: out = self._shell(f"ls {p} 2>/dev/null").strip() if out and "No such file" not in out: result["ok"] = False result["warning"] = f"su 二进制可见: {p}" return result # Magisk 目录 magisk_dirs = ["/data/adb/magisk", "/data/adb/modules"] for d in magisk_dirs: out = self._shell(f"ls {d} 2>/dev/null").strip() if out and "No such file" not in out: # 目录存在但应被 Shamiko 隐藏不让目标App看到 result["magisk_dir_visible"] = True # 检查 Shamiko 是否安装 shamiko = self._shell("ls /data/adb/modules/shamiko 2>/dev/null").strip() result["shamiko_installed"] = bool(shamiko and "No such file" not in shamiko) if not result["shamiko_installed"]: result["warning"] = "Shamiko 未安装,Root 可能暴露" result["ok"] = False return result # ------------------------------------------------------------------ # 2. Frida 反检测 # ------------------------------------------------------------------ def _check_frida_stealth(self) -> dict: result = {"ok": True} # 检查默认 frida-server 进程名 ps_out = self._shell("ps -A 2>/dev/null || ps").strip() if "frida-server" in ps_out or "frida-agent" in ps_out: result["ok"] = False result["warning"] = "检测到默认 frida-server 进程名,必须使用 Phantom-Frida" return result # 检查默认端口 27042 netstat = self._shell("netstat -tlnp 2>/dev/null || ss -tlnp 2>/dev/null").strip() if ":27042" in netstat: result["ok"] = False result["warning"] = "检测到 Frida 默认端口 27042" return result # 检查默认路径 for path in self.FRIDA_INDICATORS: out = self._shell(f"ls {path} 2>/dev/null").strip() if out and "No such file" not in out: result["ok"] = False result["warning"] = f"Frida 默认路径可见: {path}" return result # 检查 /proc/self/maps 中的 frida 字符串(从 shell 模拟目标App视角) maps = self._shell("cat /proc/self/maps 2>/dev/null | head -200").strip() if "frida" in maps.lower(): result["ok"] = False result["warning"] = "/proc/self/maps 中发现 frida 特征" return result # ------------------------------------------------------------------ # 3. 设备指纹采集 # ------------------------------------------------------------------ def _collect_fingerprint(self) -> dict: fp_data = {} props = { "ro.serialno": "serial", "ro.build.fingerprint": "build_fp", "ro.build.display.id": "display_id", "ro.product.model": "model", "ro.product.brand": "brand", "ro.product.device": "device", "ro.product.board": "board", "ro.hardware": "hardware", "ro.boot.serialno": "boot_serial", "persist.sys.timezone": "timezone", "ro.build.version.sdk": "sdk_version", } for prop, key in props.items(): val = self._shell(f"getprop {prop} 2>/dev/null").strip() if val: fp_data[key] = val # Android ID android_id = self._shell( "settings get secure android_id 2>/dev/null" ).strip() if android_id: fp_data["android_id"] = android_id # MAC 地址 mac = self._shell( "cat /sys/class/net/wlan0/address 2>/dev/null" ).strip() if mac and mac != "00:00:00:00:00:00": fp_data["mac"] = mac # 蓝牙地址 bt = self._shell( "settings get secure bluetooth_address 2>/dev/null" ).strip() if bt: fp_data["bluetooth"] = bt # 屏幕参数 try: info = self.d.info fp_data["screen_w"] = info.get("displayWidth", 0) fp_data["screen_h"] = info.get("displayHeight", 0) except Exception: pass # IMEI(需 root 或特殊权限) imei = self._shell( "service call iphonesubinfo 1 2>/dev/null | grep -oP \"'[^']+'\"" ).strip() if imei: fp_data["imei_raw"] = imei # 计算指纹哈希 fp_str = "|".join(f"{k}={v}" for k, v in sorted(fp_data.items())) fp_hash = hashlib.md5(fp_str.encode()).hexdigest() return { "ok": True, "fingerprint_hash": fp_hash, "dimensions": len(fp_data), "data": fp_data, } # ------------------------------------------------------------------ # 4. 模拟器检测 # ------------------------------------------------------------------ def _check_emulator(self) -> dict: result = {"ok": True, "is_emulator": False, "signals": []} # Build 属性 build_fp = self._shell("getprop ro.build.fingerprint 2>/dev/null").strip().lower() hardware = self._shell("getprop ro.hardware 2>/dev/null").strip().lower() product = self._shell("getprop ro.product.name 2>/dev/null").strip().lower() for emu in self.EMU_FINGERPRINTS: if emu.lower() in build_fp or emu.lower() in hardware or emu.lower() in product: result["signals"].append(f"Build特征: {emu}") # 特征文件 emu_files = [ "/dev/qemu_pipe", "/dev/goldfish_pipe", "/system/lib/libc_malloc_debug_qemu.so", "/sys/qemu_trace", "/dev/socket/qemud", ] for f in emu_files: out = self._shell(f"ls {f} 2>/dev/null").strip() if out and "No such file" not in out: result["signals"].append(f"特征文件: {f}") # 传感器数量(真机通常 > 10,模拟器 ≤ 3) sensors = self._shell( "dumpsys sensorservice 2>/dev/null | grep -c 'Sensor' || echo 0" ).strip() try: sensor_count = int(sensors) result["sensor_count"] = sensor_count if sensor_count < 5: result["signals"].append(f"传感器过少: {sensor_count}") except ValueError: pass # 电池状态(模拟器通常电池 API 异常) battery_status = self._shell( "dumpsys battery 2>/dev/null | grep 'status' | head -1" ).strip() if "status: 1" in battery_status: result["signals"].append("电池状态异常(status=1=UNKNOWN)") if result["signals"]: result["is_emulator"] = True result["ok"] = False result["warning"] = f"疑似模拟器: {', '.join(result['signals'][:3])}" return result # ------------------------------------------------------------------ # 5. 无障碍服务检测 # ------------------------------------------------------------------ def _check_accessibility(self) -> dict: out = self._shell( "settings get secure enabled_accessibility_services 2>/dev/null" ).strip() services = [s for s in out.split(":") if s.strip()] if out and out != "null" else [] return { "ok": True, "active_services": services, "count": len(services), } # ------------------------------------------------------------------ # 6. SELinux 状态 # ------------------------------------------------------------------ def _check_selinux(self) -> dict: out = self._shell("getenforce 2>/dev/null").strip().lower() enforcing = out == "enforcing" result = {"ok": True, "mode": out} if not enforcing: result["warning"] = f"SELinux 非 Enforcing 模式: {out}" result["ok"] = False return result # ------------------------------------------------------------------ # 7. App 隐藏检测(HMA-OSS) # ------------------------------------------------------------------ def _check_app_hidden(self) -> dict: visible = [] for pkg in self.HIDDEN_PACKAGES: out = self._shell(f"pm list packages {pkg} 2>/dev/null").strip() if f"package:{pkg}" in out: visible.append(pkg) result = {"ok": len(visible) == 0, "visible_sensitive_apps": visible} if visible: result["warning"] = f"敏感 App 未隐藏: {', '.join(visible)}" return result # ------------------------------------------------------------------ # 8. 危险属性检测 # ------------------------------------------------------------------ def _check_dangerous_props(self) -> dict: """检测会暴露自动化/Root 的系统属性""" dangerous = {} # ro.debuggable debuggable = self._shell("getprop ro.debuggable 2>/dev/null").strip() if debuggable == "1": dangerous["ro.debuggable"] = "1 (应为 0)" # ro.secure secure = self._shell("getprop ro.secure 2>/dev/null").strip() if secure == "0": dangerous["ro.secure"] = "0 (应为 1)" # init.svc.adbd adbd = self._shell("getprop init.svc.adbd 2>/dev/null").strip() if adbd == "running": dangerous["adb_running"] = True result = {"ok": len(dangerous) == 0, "dangerous_props": dangerous} if dangerous: result["warning"] = f"危险属性: {list(dangerous.keys())}" return result # ------------------------------------------------------------------ # 工具 # ------------------------------------------------------------------ def _shell(self, cmd: str) -> str: try: timeout = float(os.environ.get("WP_GUARD_SHELL_TIMEOUT", "4") or "4") return self.d.shell(cmd, timeout=timeout).output or "" except Exception: return ""