¿Por qué detectar root?
En un dispositivo rooteado el atacante tiene acceso root al filesystem — puede leer el almacenamiento interno de tu app, extraer las SharedPreferences, interceptar las llamadas de sistema, y desactivar las protecciones de seguridad. El Keystore también puede verse comprometido en algunos dispositivos rooteados.
Para apps financieras, de salud o enterprise, ejecutar en un dispositivo rooteado representa un riesgo que la política de seguridad puede no tolerar.
Técnicas de detección de root
object RootDetector {
fun estaRooteado(): Boolean {
return verificarBinarioSU() ||
verificarAppsRoot() ||
verificarPropiedadesBuild() ||
verificarMontajesRW()
}
// 1. Buscar el binario 'su' en rutas conocidas
private fun verificarBinarioSU(): Boolean {
val rutasSU = listOf(
"/system/bin/su", "/system/xbin/su",
"/sbin/su", "/su/bin/su",
"/data/local/xbin/su", "/data/local/bin/su",
"/system/sd/xbin/su", "/system/bin/failsafe/su"
)
return rutasSU.any { File(it).exists() }
}
// 2. Detectar apps de gestión de root (Magisk, SuperSU, etc.)
private fun verificarAppsRoot(): Boolean {
val paquetesRoot = listOf(
"com.topjohnwu.magisk",
"eu.chainfire.supersu",
"com.noshufou.android.su",
"com.koushikdutta.superuser",
"com.zachspong.temprootremovejb",
"com.ramdroid.appquarantine"
)
return paquetesRoot.any { paquete ->
try {
packageManager.getPackageInfo(paquete, 0)
true
} catch (e: PackageManager.NameNotFoundException) { false }
}
}
// 3. Propiedades del build que indican root o ROM custom
private fun verificarPropiedadesBuild(): Boolean {
val buildTags = Build.TAGS
return buildTags != null && (
buildTags.contains("test-keys") || // ROM no oficial
Build.TYPE == "userdebug" || // build de debug
Build.TYPE == "eng" // build de engineering
)
}
// 4. Verificar si /system está montado como read-write
private fun verificarMontajesRW(): Boolean {
return try {
val proceso = Runtime.getRuntime().exec("mount")
val salida = proceso.inputStream.bufferedReader().readText()
salida.contains("/system rw") || salida.contains("/system ext4 rw")
} catch (e: Exception) { false }
}
}
Detección de emulador
object EmulatorDetector {
fun esEmulador(): Boolean {
return verificarPropiedades() ||
verificarHardware() ||
verificarTelefonia()
}
private fun verificarPropiedades(): Boolean {
return Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.MODEL.contains("google_sdk") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK built for x86") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.BRAND.startsWith("generic") ||
Build.DEVICE.startsWith("generic") ||
Build.PRODUCT.contains("sdk") ||
Build.HARDWARE.contains("goldfish") ||
Build.HARDWARE.contains("ranchu")
}
private fun verificarHardware(): Boolean {
// Los emuladores generalmente no tienen hardware real de sensor
return Build.BOARD.isEmpty() || Build.BRAND.isEmpty()
}
private fun verificarTelefonia(context: Context): Boolean {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
val imei = tm?.deviceId ?: return false
// IMEI "000000000000000" es típico de emuladores
return imei.all { it == '0' }
}
}
Play Integrity API — la solución oficial
Google ofrece la Play Integrity API (reemplazó a SafetyNet Attestation). Le preguntás a Google si el dispositivo y la app son confiables, y Google responde con un veredicto firmado que verificás en tu servidor:
// build.gradle
implementation("com.google.android.play:integrity:1.3.0")
// En la app — solicitar el veredicto
class IntegrityChecker(private val context: Context) {
private val integrityManager = IntegrityManagerFactory.create(context)
suspend fun verificarIntegridad(): IntegrityVerdict {
// Nonce único por solicitud (generado en tu servidor)
val nonce = obtenerNonceDelServidor()
val token = suspendCoroutine { continuation ->
integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce)
.build()
)
.addOnSuccessListener { response ->
continuation.resume(response.token())
}
.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
}
// Enviar el token a TU servidor para verificar
// Tu servidor llama a la API de Google para decodificar el veredicto
return apiService.verificarIntegridadToken(token)
}
}
// El veredicto del servidor incluye:
// - appIntegrity: PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, UNEVALUATED
// - deviceIntegrity: MEETS_DEVICE_INTEGRITY, MEETS_BASIC_INTEGRITY, etc.
// - accountDetails: LICENSED (el usuario tiene la app de Play Store)
La verificación debe hacerse en el servidorNunca verifiques el veredicto de Play Integrity directamente en la app. Un atacante puede interceptar y falsificar la respuesta. El token debe ir a tu backend, que lo verifica con la API de Google usando tu clave de proyecto.
Cómo reaccionar al detectar root o emulador
class SecurityChecker @Inject constructor(
private val rootDetector: RootDetector,
private val emulatorDetector: EmulatorDetector
) {
sealed class SecurityStatus {
object OK : SecurityStatus()
data class Comprometido(val razones: List<String>) : SecurityStatus()
}
fun verificar(): SecurityStatus {
val razones = mutableListOf<String>()
if (rootDetector.estaRooteado()) razones.add("root")
if (emulatorDetector.esEmulador() && BuildConfig.BUILD_TYPE == "release") {
razones.add("emulador")
}
return if (razones.isEmpty()) SecurityStatus.OK
else SecurityStatus.Comprometido(razones)
}
}
// En la Activity principal:
when (val status = securityChecker.verificar()) {
SecurityStatus.OK -> continuar()
is SecurityStatus.Comprometido -> {
// Loggear el evento (sin datos sensibles)
Firebase.analytics.logEvent("security_check_failed") {
param("reasons", status.razones.joinToString(","))
}
// Mostrar mensaje genérico — no revelar qué fue detectado
mostrarDialogoSeguridad()
}
}
Limitaciones reales — la honestidad importa
La detección de root y emulador no es una solución definitiva. Un atacante determinado puede evadir todas estas técnicas:
- Magisk Hide puede ocultar el root de la mayoría de las detecciones
- Los emuladores modernos imitan mejor el hardware real
- Un atacante puede modificar tu APK para deshabilitar las verificaciones
El valor real de estas técnicas es elevar el costo del ataque y filtrar ataques automatizados. Combinalas siempre con otras capas de seguridad (cifrado, pinning, ofuscación) y con controles en el servidor.