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 = "非虚拟机"
        }
    }
}

总结

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

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

相关推荐
liang_jy6 小时前
Android 单元测试(二)—— 高级 Mock 技术
android·面试·单元测试
liang_jy6 小时前
Android 单元测试(一)—— 基础
android·面试·单元测试
Digitally8 小时前
如何将照片从电脑传输到安卓设备
android·电脑
教程分享大师8 小时前
创维LB2002_S905L3A处理器当贝纯净版固件下载_带root权限 白色云电脑机顶盒
android
whatever who cares8 小时前
Android Activity 任务栈详解
android
idward3078 小时前
Android的USB通信 (AOA Android开放配件协议)
android·linux
且随疾风前行.8 小时前
Android Binder 驱动 - Media 服务启动流程
android·microsoft·binder
恋猫de小郭8 小时前
Flutter 真 3D 游戏引擎来了,flame_3d 了解一下
android·前端·flutter
一笑的小酒馆9 小时前
Android使用Flow+协程封装一个FlowBus
android