Android 虚拟环境之虚拟环境检测<完结版>

虚拟环境简介<一>

虚拟环境检测<二>

虚拟环境检测<二>

上边两篇,介绍了虚拟环境分类,以及检测的方法.

背景介绍:

公司项目是一个直播项目,频繁的接收到运营反馈,项目的直播被录屏转到其他平台,以及被虚拟环境串改配置,导致公司的一些限制无法生效.资本家提意见了要处理一下.

这部牛马又开始掉头发了.

预期效果:

  • 检测虚拟机环境(网上找个三方库检测就行)
  • 检测虚拟空间(专注处理)

1:虚拟空间检测

1.1 检测配置

kotlin 复制代码
 class VirtualConfig {
    private var virtualPackages: Set<String> = setOf(
        "com.vmos",
        "org.app.virtual",
        "de.robv.android.xposed.installer",
        "com.bly.dkplat",
        "com.qihoo.magic",
        "com.island",
        "com.shelter"
    )

    private var enableLog: Boolean = true
    private var enableEmulatorCheck: Boolean = true
    private var  enableSignatureCheck: Boolean = true
    private var enableUidCheck: Boolean = true
    private var enableHardwareCheck: Boolean = true
    private var enableMountCheck: Boolean = true
    private var enablePackageNameCheck: Boolean = true
    private var enableParentProcessCheck: Boolean = false
    private var enableMapsCheck: Boolean = true

    private var enableKnownVirtualAppsCheck: Boolean = true
    // 模拟器检查设置方法
    fun setEnableEmulatorCheck(enable: Boolean): VirtualConfig {
        this.enableEmulatorCheck = enable
        return this
    }

    // UID检查设置方法
    fun setEnableUidCheck(enable: Boolean): VirtualConfig {
        this.enableUidCheck = enable
        return this
    }

    // 硬件检查设置方法
    fun setEnableHardwareCheck(enable: Boolean): VirtualConfig {
        this.enableHardwareCheck = enable
        return this
    }

    // 挂载检查设置方法
    fun setEnableMountCheck(enable: Boolean): VirtualConfig {
        this.enableMountCheck = enable
        return this
    }

    // 包名检查设置方法
    fun setEnablePackageNameCheck(enable: Boolean): VirtualConfig {
        this.enablePackageNameCheck = enable
        return this
    }

    // 父进程检查设置方法
    fun setEnableParentProcessCheck(enable: Boolean): VirtualConfig {
        this.enableParentProcessCheck = enable
        return this
    }

    // Maps检查设置方法
    fun setEnableMapsCheck(enable: Boolean): VirtualConfig {
        this.enableMapsCheck = enable
        return this
    }



    // 已知虚拟应用检查设置方法
    fun setEnableKnownVirtualAppsCheck(enable: Boolean): VirtualConfig {
        this.enableKnownVirtualAppsCheck = enable
        return this
    }

    // 设置虚拟应用包名集合
    fun setVirtualPackages(packages: Set<String>): VirtualConfig {
        this.virtualPackages = packages
        return this
    }

    // 添加虚拟应用包名
    fun addVirtualPackage(packageName: String): VirtualConfig {
        this.virtualPackages = this.virtualPackages + packageName
        return this
    }

    // 移除虚拟应用包名
    fun removeVirtualPackage(packageName: String): VirtualConfig {
        this.virtualPackages = this.virtualPackages - packageName
        return this
    }

    // 日志启用设置方法
    fun setEnableLog(enable: Boolean): VirtualConfig {
        this.enableLog = enable
        return this
    }

    // 获取属性的getter方法
    fun isEnableEmulatorCheck() = enableEmulatorCheck
    fun isEnableUidCheck() = enableUidCheck
    fun isEnableSignatureCheck() = enableSignatureCheck
    fun isEnableHardwareCheck() = enableHardwareCheck
    fun isEnableMountCheck() = enableMountCheck
    fun isEnablePackageNameCheck() = enablePackageNameCheck
    fun isEnableParentProcessCheck() = enableParentProcessCheck
    fun isEnableMapsCheck() = enableMapsCheck

    fun isEnableKnownVirtualAppsCheck() = enableKnownVirtualAppsCheck
    fun getVirtualPackages() = virtualPackages
    fun isEnableLog() = enableLog
}

1.2 检测工具类

kotlin 复制代码
package com.wkq.util

import android.content.Context
import android.util.Log
import android.webkit.WebView
import com.blankj.utilcode.util.DeviceUtils
import com.snail.antifake.jni.EmulatorDetectUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference

/**
 *
 *@Author: wkq
 *
 *@Time: 2025/9/3 14:38
 *
 *@Desc: 虚拟空间检测
 */
object VirtualSpaceDetectionManager {


    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private var config: VirtualConfig = VirtualConfig()

    /**
     * 初始化方法
     * @param cfg 配置对象,可自定义开启哪些检测、日志、虚拟包名等
     */
    @JvmStatic
    fun init(context:Context,cfg: VirtualConfig? = null) {
        cfg?.let {
            config = it
            VirtualUtil.init(cfg.getVirtualPackages(), cfg.isEnableLog())
        }

        if (config.isEnableLog()) Log.i("VirtualChecker", "VirtualChecker 初始化完成,配置: $config")
        // 重要  加入WebView 可以增强 readMapsByFile 和so 的检测
        val webView = WebView(context)
        webView.loadUrl("about:blank")
    }

    fun cancelAll() {
        scope.coroutineContext.cancelChildren()
    }

    interface Callback {
        fun onResult(result: VirtualCheckResult)
    }

    /**
     * 是否是虚拟机(快速检测)
     */
    fun isRunningInEmulator(context: Context?): Boolean {
        return try {
            val contextWR = WeakReference(context)
            DeviceUtils.isEmulator() || EmulatorDetectUtil.isEmulator(contextWR.get())
        } catch (e: Exception) {
            false
        }
    }

    fun checkVirtualSpace(context: Context, callback: (VirtualCheckResult) -> Unit) {
        val contextWR = WeakReference(context)

        scope.launch {
            val results = mutableListOf<VirtualCheckResult>()
            val checks = mutableListOf<Pair<String, suspend () -> VirtualCheckResult>>()
            val ctx = contextWR.get() ?: return@launch

            if (config.isEnableEmulatorCheck()) {
                checks.add("Emulator" to { safeCheck {
                    isRunningInEmulator(ctx)
                        .let { VirtualCheckResult(it, "Emulator=$it") } } })
            }
            if (config.isEnableUidCheck()) checks.add("UID" to { safeCheck { VirtualUtil.isUidSuspicious(ctx) } })
            if (config.isEnableSignatureCheck()) checks.add("SignatureCheck" to { safeCheck {  SignatureChecker.isSignatureTampered(
                contextWR.get() ?: return@safeCheck VirtualCheckResult(false),
                SoUtil.getCDecryptedSha256(),
                SoUtil.getCDecryptedPublicKey(),
                SoUtil.getCDecryptedCN()
            )} })
            if (config.isEnableHardwareCheck()) checks.add("Hardware" to { safeCheck { VirtualUtil.checkHardwareInfo(ctx) } })
            if (config.isEnableMountCheck()) checks.add("Mounts" to { safeCheck { VirtualUtil.checkSuspiciousMounts() } })

            if (config.isEnablePackageNameCheck()) checks.add("PackageName" to { safeCheck { VirtualUtil.checkPackageName(ctx) } })
            if (config.isEnableParentProcessCheck()) checks.add("ParentProcess" to { safeCheck { VirtualUtil.isParentVirtualSpace() } })
            if (config.isEnableMapsCheck()) checks.add("MapsFile" to { safeCheck { VirtualUtil.readMapsByFile(ctx) } })
            if (config.isEnableMapsCheck()) checks.add("MapsSoFile" to { safeCheck { VirtualUtil.readMapsBySo(ctx) } })

            if (config.isEnableKnownVirtualAppsCheck()) checks.add("KnownVirtualApps" to { safeCheck { VirtualUtil.detectKnownVirtualApps(ctx) } })

            var finalResult = VirtualCheckResult(false)

            for ((name, check) in checks) {
                val r = check()
                results.add(r)
                if (config.isEnableLog()) Log.e("VirtualChecker", "$name -> ${r.isVirtual}: ${r.resultContent}")
                if (r.isVirtual) {
                    finalResult = r
                    break
                }
            }

            withContext(Dispatchers.Main) {
                callback(finalResult)
            }
        }
    }

    private suspend fun safeCheck(check: suspend () -> VirtualCheckResult): VirtualCheckResult {
        return try {
            check()
        } catch (e: Exception) {
            if (config.isEnableLog()) Log.e("VirtualChecker", "检测异常: ${e.stackTraceToString()}")
            VirtualCheckResult(false, "Exception: ${e.message}")
        }
    }
}

1.3 检测方法

kotlin 复制代码
package com.wkq.util

import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build
import android.os.Process
import android.util.Log
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.net.NetworkInterface
import java.util.Enumeration

/**
 * 虚拟环境检测工具类
 * 提供多种虚拟环境特征检测方法,包括已知虚拟应用检测、挂载点检测、硬件信息检测等
 * 所有检测结果均封装为VirtualCheckResult对象返回
 */
object VirtualUtil {

    // -----------------------------
    // 动态配置参数
    // -----------------------------
    /** 已知的虚拟环境应用包名集合 */
    private var virtualPackages: Set<String> = setOf(
        "com.vmos",                 // VMOS虚拟机
        "org.app.virtual",          // 虚拟应用
        "de.robv.android.xposed.installer", // Xposed框架
        "com.bly.dkplat",           // 多开大师
        "com.qihoo.magic",          // 360分身大师
        "com.island",               // Island沙箱
        "com.shelter"               // Shelter沙箱
    )

    /** 日志开关标志 */
    private var enableLog: Boolean = true

    /**
     * 初始化方法,动态配置虚拟包名和日志开关
     * @param virtualPkg 自定义的虚拟应用包名集合,为空则使用默认集合
     * @param logEnable 是否启用日志输出,默认启用
     */
    @JvmStatic
    fun init(virtualPkg: Set<String>? = null, logEnable: Boolean = true) {
        virtualPkg?.let { virtualPackages = it }
        enableLog = logEnable
        if (enableLog) Log.i("VirtualUtil", "初始化完成,虚拟空间包名: $virtualPackages")
    }

    // -----------------------------
    // 虚拟环境检测方法
    // -----------------------------

    /**
     * 检测设备上是否安装了已知的虚拟环境应用
     * @param context 上下文对象,用于获取已安装应用列表
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun detectKnownVirtualApps(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        try {
            // 获取包管理器
            val pm = context.packageManager
            // 获取所有已安装应用的包信息
            val packages = pm.getInstalledPackages(0)

            for (pkgInfo in packages) {
                // 检查当前应用包名是否在已知虚拟应用包名集合中
                if (pkgInfo.packageName in virtualPackages) {
                    result.isVirtual = true
                    result.resultContent = pkgInfo.packageName
                    if (enableLog) Log.e("VirtualUtil", "已知虚拟应用检测: 发现${pkgInfo.packageName}")
                    return result
                }
            }

            if (enableLog) Log.i("VirtualUtil", "已知虚拟应用检测: 未发现已知虚拟应用")
        } catch (e: Exception) {
            if (enableLog) Log.e("VirtualUtil", "获取已安装包失败: ${e.message}", e)
        }
        return result
    }

    /**
     * 检测系统挂载点是否存在虚拟环境特征
     * 通过读取/proc/self/cgroup文件,检查是否包含虚拟环境相关关键词
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun checkSuspiciousMounts(): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        try {
            // 读取/proc/self/cgroup文件,该文件包含进程的控制组信息
            BufferedReader(FileReader("/proc/self/cgroup")).use { br ->
                var line: String?
                while (br.readLine().also { line = it } != null) {
                    line?.let {
                        if (enableLog) Log.e("VirtualUtil", "挂载点检测: 检查行 $it")
                        // 检查是否包含虚拟环境相关关键词
                        if (it.contains("vmos", true) ||
                            it.contains("virtual", true) ||
                            it.contains("parallel", true)) {
                            result.isVirtual = true
                            result.resultContent = it
                            return result
                        }
                    }
                }
            }
            if (enableLog) Log.i("VirtualUtil", "挂载点检测: 未发现异常挂载点")
        } catch (e: Exception) {
            if (enableLog) Log.e("VirtualUtil", "读取 /proc/self/cgroup 失败", e)
        }
        return result
    }

    /**
     * 检测硬件信息是否存在虚拟环境特征
     * 主要通过检查MAC地址是否为虚拟环境常用的默认地址
     * @param context 上下文对象
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun checkHardwareInfo(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        // 获取设备MAC地址并转为小写
        val macAddress = getMacAddress()?.lowercase()
        if (enableLog) Log.e("VirtualUtil", "硬件信息检测: MAC地址为 $macAddress")
        if (macAddress.isNullOrEmpty()) return result

        // 虚拟环境常用的默认MAC地址集合
        val virtualMacs = setOf(
            "00:00:00:00:00:00",  // 全零MAC地址
            "02:00:00:00:00:00",  // 虚拟设备常用MAC
            "aa:bb:cc:dd:ee:ff"   // 测试用默认MAC
        )

        // 检查当前MAC地址是否在虚拟MAC集合中
        if (macAddress in virtualMacs) {
            result.isVirtual = true
            result.resultContent = macAddress
            if (enableLog) Log.e("VirtualUtil", "硬件信息检测: 发现可疑MAC地址 $macAddress")
        }
        return result
    }

    /**
     * 获取设备的MAC地址
     * 遍历所有网络接口,筛选出有效的非虚拟接口的MAC地址
     * @return 有效的MAC地址,若获取失败则返回null
     */
    private fun getMacAddress(): String? {
        try {
            // 获取所有网络接口
            val interfaces: Enumeration<NetworkInterface> = NetworkInterface.getNetworkInterfaces()
            while (interfaces.hasMoreElements()) {
                val ni = interfaces.nextElement()
                // 跳过虚拟接口
                if (ni.isVirtual || ni.displayName.contains("virtual", true)) continue

                val macBytes = ni.hardwareAddress
                if (macBytes != null && macBytes.size > 0) {
                    // 将MAC字节数组转换为标准格式字符串
                    val mac = macBytes.joinToString(":") { String.format("%02X", it) }
                    if (enableLog) Log.e("VirtualUtil", "获取到MAC地址: $mac")

                    // 检查是否为无效MAC地址(全零)
                    val isInvalidMac = mac.all { it == '0' || it == ':' }
                    if (!isInvalidMac && mac.isNotBlank()) {
                        if (enableLog) Log.i("VirtualUtil", "有效MAC地址: $mac")
                        return mac
                    } else {
                        if (enableLog) Log.w("VirtualUtil", "无效MAC地址: $mac")
                    }
                }
            }
        } catch (_: Exception) {}
        return null
    }

    /**
     * 检测父进程是否为虚拟环境应用
     * 通过读取/proc文件系统获取进程信息,检查父进程是否为已知虚拟应用
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun isParentVirtualSpace(): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        try {
            // 获取当前进程ID
            val currentPid = Process.myPid()
            // 进程状态文件,包含进程基本信息
            val statFile = File("/proc/$currentPid/stat")
            if (!statFile.exists()) return result

            // 解析stat文件获取父进程ID(PPID)
            val statContent = statFile.readText().split(" ")
            val ppid = statContent.getOrNull(3)?.toIntOrNull() ?: return result

            // 父进程命令行文件,包含进程启动命令
            val parentCmdlineFile = File("/proc/$ppid/cmdline")
            if (!parentCmdlineFile.exists()) return result

            // 读取并解析父进程命令行信息
            val parentCmdline = parentCmdlineFile.readText().trim()
            val parentPackage = parentCmdline.substringBefore(":")

            // 检查父进程是否包含已知虚拟应用包名
            val matchedVirtual = virtualPackages.firstOrNull { parentPackage.contains(it) }
            result.isVirtual = matchedVirtual != null
            result.resultContent = "PID=$ppid, Cmdline=$parentCmdline, Virtual=${result.isVirtual}, Match=$matchedVirtual"

            if (enableLog) Log.e("VirtualUtil", "父进程检测: ${result.resultContent}")
        } catch (e: Exception) {
            if (enableLog) Log.e("VirtualUtil", "父进程检测异常: ${e.message}", e)
        }
        return result
    }

    /**
     * 检测应用包名是否被虚拟环境篡改
     * 通过反射获取包名并与实际包名对比,判断是否被Hook或篡改
     * @param context 上下文对象
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun checkPackageName(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        try {
            // 通过反射调用getPackageName()方法
            val clazz = Class.forName("android.content.Context")
            val method = clazz.getMethod("getPackageName")
            val reflectedPackageName = method.invoke(context) as String

            // 对比反射获取的包名与实际包名
            if (reflectedPackageName != context.packageName) {
                result.isVirtual = true
                result.resultContent = "Reflected=$reflectedPackageName, Actual=${context.packageName}"
            }
            if (enableLog) Log.e("VirtualUtil", "包名检测: ${result.resultContent}")
        } catch (e: Exception) {
            // 反射异常通常意味着被Hook,判定为虚拟环境
            result.isVirtual = true
            result.resultContent = "Reflection exception: ${e.message}"
            if (enableLog) Log.e("VirtualUtil", "包名检测异常", e)
        }
        return result
    }

    /**
     * 通过读取/proc/self/maps文件检测虚拟环境
     * 该文件包含进程的内存映射信息,虚拟环境通常会有异常的路径特征
     * @param context 上下文对象,用于获取应用包名
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun readMapsByFile(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        // /proc/self/maps文件包含当前进程的内存映射信息
        val mapsFile = File("/proc/self/maps")
        if (!mapsFile.exists() || !mapsFile.canRead()) {
            if (enableLog) Log.e("VirtualUtil", "/proc/self/maps 不存在或不可读")
            return result
        }
        try {
            mapsFile.bufferedReader().use { reader ->
                var line: String?
                while (reader.readLine().also { line = it } != null) {
                    val currentLine = line!!.lowercase()
                    if (enableLog) Log.e("VirtualUtil", "/proc/self/maps: 检查行 $currentLine")
                    // 检查是否包含当前应用包名且路径异常
                    if (currentLine.contains(context.packageName) && isAbnormal(currentLine, context.packageName)) {
                        result.isVirtual = true
                        result.resultContent = currentLine
                        return result
                    }
                }
            }
        } catch (e: Exception) {
            if (enableLog) Log.e("VirtualUtil", "读取 /proc/self/maps 异常", e)
        }
        return result
    }

    /**
     * 通过SO库读取内存映射信息检测虚拟环境
     * 与readMapsByFile方法原理相同,只是读取方式不同,提高检测成功率
     * @param context 上下文对象,用于获取应用包名
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun readMapsBySo(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        // 通过SO库获取内存映射信息
        val lines = SoUtil.getReadProcSelfMaps()?.lines() ?: return result

        for (line in lines) {
            // 如果开启日志,打印包含当前应用包名的行
            if (enableLog) {
                if (line.contains(context.packageName)) {
                    Log.e("VirtualUtil", "readMapsBySo 命中自身包名行: $line")
                }
            }

            // 检查路径是否异常
            if (isAbnormal(line, context.packageName)) {
                result.isVirtual = true
                result.resultContent = line
                if (enableLog) Log.e("VirtualUtil", "readMapsBySo 检测到异常: $line")
                return result
            }
        }

        return result
    }

    /**
     * 判断内存映射路径是否存在异常(虚拟环境特征)
     * @param path 内存映射路径
     * @param packageName 应用包名
     * @return 路径是否异常
     */
    private fun isAbnormal(path: String, packageName: String): Boolean {
        // 路径不包含应用包名则不视为异常
        if (!path.contains(packageName)) return false

        val tag = "/data/data/"
        val parts = path.split(packageName)

        for (part in parts) {
            // 检查路径中是否包含异常的/data/data/路径结构
            if (part.startsWith(tag) && part.length > tag.length) return true
            if (part.endsWith(tag)) return false
            if (part.split(tag).size > 1) return true
        }
        return false
    }

    /**
     * 检测应用UID是否存在虚拟环境特征
     * 正常应用的UID有固定范围,虚拟环境可能使用异常的UID
     * @param context 上下文对象
     * @return 检测结果,包含是否为虚拟环境及检测详情
     */
    fun isUidSuspicious(context: Context): VirtualCheckResult {
        val result = VirtualCheckResult(false, "")
        // 获取应用的UID
        val uid = context.applicationInfo.uid
        // 获取设备品牌和制造商信息
        val brand = Build.BRAND.lowercase()
        val manufacturer = Build.MANUFACTURER.lowercase()

        // 判断是否为ColorOS系统(OPPO/Realme/OnePlus)
        val isColorOS = brand.contains("oppo") || brand.contains("realme") || brand.contains("oneplus") ||
                manufacturer.contains("oppo") || manufacturer.contains("realme") || manufacturer.contains("oneplus")

        // ColorOS系统特殊处理,直接返回正常结果
        if (isColorOS) return result

        // 系统应用直接返回正常结果
        if (isSystemApp(context)) return result

        // 正常第三方应用的UID通常大于1000,小于1000的UID可能为虚拟环境
        if (uid < 1000) {
            result.isVirtual = true
            result.resultContent = "Suspicious UID: $uid"
            if (enableLog) Log.e("VirtualUtil", "UID检测: 发现可疑UID $uid")
        }
        return result
    }

    /**
     * 判断应用是否为系统应用
     * 系统应用的UID检测逻辑不同,需要特殊处理
     * @param context 上下文对象
     * @return 是否为系统应用
     */
    private fun isSystemApp(context: Context): Boolean {
        val flags = context.applicationInfo.flags
        // 系统应用标志或系统更新应用标志
        return (flags and ApplicationInfo.FLAG_SYSTEM) != 0 ||
                (flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
    }
}

1.4 其他类

结果类

javascript 复制代码
class VirtualCheckResult( var isVirtual: Boolean, var resultContent: String? = null)

签名对比类

kotlin 复制代码
package com.wkq.util

/**
 *
 *@Author: wkq
 *
 *@Time: 2025/8/1 16:29
 *
 *@Desc:
 */
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.os.Build
import android.util.Log
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.Date

import android.util.Base64

object SignatureChecker {
    /**
    * 签名文件校验
     */
    /**
     * 签名文件校验
     */
    fun isSignatureTampered(
        context: Context,
        sha256: String?,
        publicKey: String?,
        cn: String?
    ): VirtualCheckResult {
        return try {
            val packageInfo =SignatureChecker.getPackageInfo(context)
            val signatures = packageInfo.signatures
            if (signatures.isNullOrEmpty()) {
                return VirtualCheckResult(true, "无签名,视为篡改")
            }

            for (signature in signatures) {
                // 1. 校验签名哈希
                val signSha256 = SignatureChecker.getSignatureSha256(signature)
                if (signSha256 != sha256) {
                    return VirtualCheckResult(true, "SHA256不匹配: $signSha256")
                }

                // 2. 校验证书公钥
                val key = SignatureChecker.getPublicKeyFromSignature(signature)
                if (key != publicKey) {
                    return VirtualCheckResult(true, "公钥不匹配: $key")
                }

                // 3. 校验证书其他信息(颁发者/有效期)
                val cert = SignatureChecker.getX509Certificate(signature)
                if (cert.issuerDN.name != cn) {
                    return VirtualCheckResult(true, "颁发者CN不匹配: ${cert.issuerDN.name}")
                }
                if (cert.notAfter.before(Date())) {
                    return VirtualCheckResult(true, "证书已过期: ${cert.notAfter}")
                }
            }

            // 所有校验通过,未篡改
            VirtualCheckResult(false, null)
        } catch (e: Exception) {
            VirtualCheckResult(true, "检测异常或可能被Hook: ${e.message}")
        }
    }


    private const val TAG = "PublicKeyExtractor"

    /**
     * 提取当前应用签名的公钥(Base64编码字符串)
     * @param context 上下文
     * @return 公钥字符串(可作为LEGAL_PUBLIC_KEY的值),失败返回null
     */
    fun extractPublicKey(context: Context): String? {
        return try {
            val packageManager = context.packageManager
            // 获取当前应用的签名信息
            val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                // Android 9+ 使用新 API
                packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
                )
            } else {
                // 低版本使用旧 API
                @Suppress("DEPRECATION")
                packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNATURES
                )
            }
            // 获取签名(优先使用新API,兼容旧版本)
            val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                packageInfo.signingInfo?.signingCertificateHistory
            } else {
                @Suppress("DEPRECATION")
                packageInfo.signatures
            }

            if (signatures.isNullOrEmpty()) {
                Log.e(TAG, "未找到应用签名")
                return null
            }

            // 解析第一个签名为X509证书(多签名场景需遍历)
            val signature: Signature = signatures[0]
            val certFactory = CertificateFactory.getInstance("X.509")
            val x509Certificate = certFactory.generateCertificate(
                signature.toByteArray().inputStream()
            ) as X509Certificate

            // 提取公钥并转为Base64字符串(去除换行和空格)

            val publicKeyBytes = x509Certificate.publicKey.encoded
            val publicKeyBase64 = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)

            Log.d(TAG, "提取到公钥:\n$publicKeyBase64")
            Log.d(TAG, "可直接用作LEGAL_PUBLIC_KEY的值")

            publicKeyBase64
        } catch (e: Exception) {
            Log.e(TAG, "提取公钥失败", e)
            null
        }
    }

    fun extractCN(context: Context): String? {
        return try {
            // 获取当前应用的签名信息
            val packageManager = context.packageManager

            val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                // Android 9+ 使用新 API
                packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
                )
            } else {
                // 低版本使用旧 API
                @Suppress("DEPRECATION")
                packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNATURES
                )
            }

            // 获取签名(优先使用新API,兼容旧版本)
            val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                packageInfo.signingInfo?.signingCertificateHistory
            } else {
                @Suppress("DEPRECATION")
                packageInfo.signatures
            }

            if (signatures.isNullOrEmpty()) {
                Log.e(TAG, "未找到应用签名")
                return null
            }

            // 解析第一个签名为X509证书(多签名场景需遍历)
            val signature: Signature = signatures[0]
            val certFactory = CertificateFactory.getInstance("X.509")
            val x509Certificate = certFactory.generateCertificate(
                signature.toByteArray().inputStream()
            ) as X509Certificate

            // 从X509证书中提取CN(Common Name)
            val subjectDN = x509Certificate.subjectX500Principal.name
            val cnValue = extractCNFromDN(subjectDN)

            Log.d(TAG, "提取到CN:$cnValue")
            cnValue
        } catch (e: Exception) {
            Log.e(TAG, "提取CN失败", e)
            null
        }
    }

    /**
     * 从DN(Distinguished Name)字符串中解析出CN(Common Name)
     * DN格式示例:"CN=Example, O=Company, L=City, ST=State, C=Country"
     */
    private fun extractCNFromDN(dn: String): String? {
        // 分割DN的各个部分(处理可能的空格)
        val dnParts = dn.split(",").map { it.trim() }
        // 查找以"CN="开头的部分并截取值
        return dnParts.firstOrNull { it.startsWith("CN=") }?.substringAfter("CN=")
    }


    // 获取PackageInfo(避免直接用PackageManager,减少Hook风险)
    private fun getPackageInfo(context: Context): PackageInfo {

        val packageManager = context.packageManager

      return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // Android 9+ 使用新 API
            packageManager.getPackageInfo(
                context.packageName,
                PackageManager.GET_SIGNING_CERTIFICATES
            )
        } else {
            // 低版本使用旧 API
            @Suppress("DEPRECATION")
            packageManager.getPackageInfo(
                context.packageName,
                PackageManager.GET_SIGNATURES
            )
        }
    }

    // 计算签名的SHA256哈希
    private fun getSignatureSha256(signature: Signature): String {
        val md = MessageDigest.getInstance("SHA-256")
        md.update(signature.toByteArray())
        return bytesToHex(md.digest())
    }
    private fun getCertSha256(signature: Signature): String {
        val certFactory = CertificateFactory.getInstance("X.509")
        val cert = certFactory.generateCertificate(signature.toByteArray().inputStream()) as X509Certificate
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(cert.encoded)
        return digest.joinToString(":") { "%02X".format(it) } // 带冒号的格式
    }

    /**
     * 从签名中提取公钥(正确方式)
     * @return 公钥的Base64编码字符串(与LEGAL_PUBLIC_KEY格式一致)
     */
    fun getPublicKeyFromSignature(signature: Signature): String {
        val cert = getX509Certificate(signature)
        // 获取公钥的原始字节数组,再转为Base64字符串(无换行)
        val publicKeyBytes = cert.publicKey.encoded
        return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
    }

    /**
     * 解析签名为X509证书
     */
    private fun getX509Certificate(signature: Signature): X509Certificate {
        val certFactory = CertificateFactory.getInstance("X.509")
        return certFactory.generateCertificate(signature.toByteArray().inputStream()) as X509Certificate
    }


    // 字节数组转十六进制字符串
    private fun bytesToHex(bytes: ByteArray): String {
        val hexChars = CharArray(bytes.size * 2)
        for (i in bytes.indices) {
            val v = bytes[i].toInt() and 0xFF
            hexChars[i * 2] = "0123456789ABCDEF"[v ushr 4]
            hexChars[i * 2 + 1] = "0123456789ABCDEF"[v and 0x0F]
        }
        return String(hexChars)
    }
}

2. 示例

arduino 复制代码
    val config = VirtualConfig()
    config.setEnableLog(true)// 日志开关
    config.setEnableEmulatorCheck(true) //虚拟机开关
    config.setEnableUidCheck(true) // uid检测
    config.setEnableHardwareCheck(true) //硬件检测
    config.setEnableMountCheck(true) //挂载点检测
    config.setEnablePackageNameCheck(true) //包名检测
    config.setEnableParentProcessCheck(true) //父进程检测
    config.setEnableMapsCheck(true) //maps检测
    config.setEnableKnownVirtualAppsCheck(true) //已知虚拟app检测
    config.addVirtualPackage("xxxx") //添加虚拟app包名

    VirtualSpaceDetectionManager.init(this, config)
    binding.tvPhone.text = DeviceInfoUtils.getPhoneInfo( this)
    binding.btCheck.setOnClickListener {
        VirtualSpaceDetectionManager.checkVirtualSpace(this) {
            if (it.isVirtual) {
                binding.tvContent.text = "虚拟空间中运行"
            } else {
                binding.tvContent.text = "未检测到虚拟空间"
            }

        }
    }

    binding.btCheckXnj.setOnClickListener {
        val isXnj = VirtualSpaceDetectionManager.isRunningInEmulator(this)
        if (isXnj) {
            binding.tvContent.text = "虚拟机"
        } else {
            binding.tvContent.text = "非虚拟机"
        }
    }
}

总结

  • 注意误杀(系统双开误杀)
  • 注意收集已知虚拟空间报名

虚拟空间检测是一个复杂的攻防过程,重在手机和适配.机型误杀,检测不到是常态,要有耐心有毅力. 欢迎大家提供哪些检测不到虚拟空间环境.

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui