
上边两篇,介绍了虚拟环境分类,以及检测的方法.
背景介绍:
公司项目是一个直播项目,频繁的接收到运营反馈,项目的直播被录屏转到其他平台,以及被虚拟环境串改配置,导致公司的一些限制无法生效.资本家提意见了要处理一下.
这部牛马又开始掉头发了.
预期效果:
- 检测虚拟机环境(网上找个三方库检测就行)
- 检测虚拟空间(专注处理)
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 = "非虚拟机"
}
}
}
总结
- 注意误杀(系统双开误杀)
- 注意收集已知虚拟空间报名
虚拟空间检测是一个复杂的攻防过程,重在手机和适配.机型误杀,检测不到是常态,要有耐心有毅力. 欢迎大家提供哪些检测不到虚拟空间环境.