Anti-análisis y hardening
1. Alcance de este documento, antes de nada
Aquí se documenta qué comprueba cada técnica y cómo se identifica su presencia en el binario, para que el analista entienda por qué su entorno le está dando resultados raros. No se documenta cómo desactivarlas, ni cómo parchearlas, ni cómo evadirlas.
La razón por la que este documento existe no es la curiosidad: es que estas protecciones se
manifiestan como fallos de herramienta. Una aplicación que cierra sola al arrancar en un
emulador, un jadx que decompila cien clases vacías, un Frida que se desconecta a los tres
segundos. Quien no sabe que existe una capa de anti-análisis interpreta esos síntomas como un
bug en su instrumental y pierde horas. La sección 10 es la tabla de síntoma → causa que
resuelve eso, y enlaza con
Diagnóstico de fallos.
La fuente principal es la OWASP Mobile Application Security Testing Guide (MASTG), que es un catálogo público, con identificadores estables y orientado a verificar que una aplicación implementa estas defensas. Se cita por identificador. Cuando una técnica muy conocida no aparece en la página de MASTG que le correspondería, se dice explícitamente y se busca otra fuente con autor identificable, en vez de atribuírsela a OWASP.
2. Qué es y por qué existe
La ofuscación (Ofuscadores comerciales) y el empaquetado (Packers y protectores) atacan el análisis estático: hacen caro leer el binario. No sirven de nada contra el análisis dinámico, donde el analista deja que la aplicación se descifre a sí misma y observa lo que hace.
El anti-análisis es la respuesta a eso: código que, en ejecución, comprueba si el entorno es el de un usuario normal o el de alguien que está mirando, y reacciona. La industria lo vende bajo la sigla RASP (Runtime Application Self-Protection), y las fichas de producto de los ofuscadores comerciales lo listan como un bloque aparte. DexGuard, por ejemplo, declara seis: certificate checks, debugger and emulator checks, root detection, hook detection, tamper detection y malware protection.
Dos observaciones que conviene tener presentes al leer todo lo que sigue.
Estas comprobaciones son detecciones basadas en artefactos, no pruebas. MASTG lo dice de su categoría de detección de herramientas: «este tipo de detección se basa en artefactos. No prueba que el código o la memoria de la aplicación hayan sido modificados, pero puede proporcionar señales útiles de que el entorno de ejecución es sospechoso». Y advierte de la otra cara: «algunas comprobaciones pueden producir falsos positivos cuando un dispositivo contiene herramientas de seguridad, herramientas de desarrollo, herramientas de accesibilidad o software de monitorización empresarial».
La plataforma les ha ido quitando terreno. Varias de las técnicas clásicas ya no funcionan o funcionan a medias por cambios de Android, y el detalle importa porque explica falsos negativos:
| Cambio | Desde | Efecto sobre la detección |
|---|---|---|
| Visibilidad de procesos y servicios restringida | Android 7.0 (API 24) | «Las APIs a nivel de aplicación no exponen de forma fiable demonios no relacionados como frida-server» |
ActivityManager.getRunningServices limitado a los propios |
Android 8.0 (API 26) | La enumeración de procesos deja de servir |
| Identificadores no reseteables restringidos | Android 10 (API 29) | Build.getSerial(), getImei(), getDeviceId(), getSubscriberId() exigen READ_PRIVILEGED_PHONE_STATE o lanzan SecurityException |
| Package visibility | Android 11 (API 30) | Consultar si está instalado un gestor de root o un emulador lanza NameNotFoundException como si no lo estuviera: falso negativo salvo que se declare <queries> o QUERY_ALL_PACKAGES |
Ese último es el más útil para el analista: una aplicación que comprueba paquetes de root y
declara QUERY_ALL_PACKAGES o una lista <queries> larga en el manifiesto está anunciando su
propia detección, y eso se ve sin ejecutar nada.
3. Anti-debug
MASTG lo cubre en MASTG-KNOW-0028 («Anti-Debugging») y su test asociado
MASTG-TEST-0046. El encuadre, literal: «hay que lidiar con dos protocolos de depuración en
Android: se puede depurar a nivel Java con JDWP o en la capa nativa con un depurador
basado en ptrace». Y distingue entre defensas preventivas —impedir que el depurador
se conecte— y reactivas —detectar que ya está conectado y reaccionar.
| Comprobación | Qué mira exactamente |
|---|---|
| Flag debuggable | android:debuggable del manifiesto, vía context.getApplicationContext().getApplicationInfo().flags y ApplicationInfo.FLAG_DEBUGGABLE. También la propiedad global ro.debuggable |
isDebuggerConnected |
android.os.Debug.isDebuggerConnected(). Su equivalente nativo lee gDvm.debuggerConnected / gDvm.debuggerActive |
TracerPid |
El campo TracerPid de /proc/<pid>/status o /proc/self/status. Distinto de cero significa que alguien tiene un ptrace puesto |
| Medición de tiempos | Debug.threadCpuTimeNanos(). MASTG: «como la depuración ralentiza la ejecución del proceso, se puede usar la diferencia de tiempo de ejecución para adivinar si hay un depurador conectado» |
| Manipulación de estructuras de JDWP | En Android < 5.0, los campos jdwpAllowed, jdwpConfigured, jdwpTransport, jdwpPort, debuggerConnected, debuggerActive y jdwpState de DvmGlobals/gDvm. Desde 5.0, JdwpAdbState/JdwpSocketState, localizables por el símbolo _ZTVN3art4JDWP12JdwpAdbStateE de libart.so |
fork + ptrace |
Un proceso hijo que se engancha al padre con PTRACE_ATTACH y PTRACE_CONT, ocupando la única plaza de trazador disponible |
El matiz de TracerPid que casi nadie menciona, y que MASTG sí: «recuerda que esto solo
aplica al código nativo. Si estás depurando una aplicación solo de Java/Kotlin, el valor del
campo TracerPid debería ser 0». Un depurador JDWP no deja rastro ahí.
⚠️ ptrace(PTRACE_TRACEME) sobre sí mismo —la técnica más citada de todas: el proceso se
traza a sí mismo para que nadie más pueda— no aparece en MASTG-KNOW-0028, que solo
menciona PTRACE_ATTACH y PTRACE_CONT. La fuente con autor identificable donde sí está
documentada es la charla de Tim Strazzere y Jon Sawyer, «Android Hacker Protection Level 0»,
DEF CON 22 (2014), que publica el pseudocódigo del JNI_OnLoad de APKProtect con
ptrace(PTRACE_TRACEME, ...) y una función find_qemud_process(). No se atribuya a OWASP.
⚠️ Debug.waitingForDebugger() tampoco aparece en esa página de MASTG.
Cómo se identifica en el binario, sin ejecutar nada: la cadena TracerPid o
/proc/self/status en el string_ids del DEX o en las cadenas de un .so; la referencia a
Landroid/os/Debug;.isDebuggerConnected en method_ids; el símbolo ptrace importado por
una biblioteca nativa; y android:debuggable en el manifiesto —que además es de por sí un
dato que hay que reportar—.
4. Anti-emulador
MASTG-KNOW-0031 («Emulator Detection») y MASTG-TEST-0049. El detalle metodológico que
sorprende: MASTG documenta estas comprobaciones a través de los campos Java de
android.os.Build, no como lectura cruda de propiedades del sistema ro.*.
Campo de Build |
Valores o patrones que delatan un emulador |
|---|---|
FINGERPRINT |
Empieza por generic; contiene test-keys, generic/sdk/generic, generic x86, vbox86p, ttvm |
MODEL |
sdk, google_sdk, emulator, android sdk built for x86, android sdk built for x86_64, droid4x, tiantianvm, genymotion, andy, nox |
MANUFACTURER |
unknown, genymotion, droid4x, tiantianvm, andy |
HARDWARE |
goldfish, ranchu, vbox86, nox, ttvm |
PRODUCT |
sdk, google_sdk, sdk_x86, sdk_google, vbox86p, droid4x, andy, ttvm, nox; empieza por itoolsavm |
BRAND / DEVICE |
Empiezan por generic; generic x86, vbox86p, ttvm, andy, nox |
BOARD |
unknown; contiene nox |
ID |
frf91 |
TAGS |
test-keys |
USER |
android-build |
SERIAL / RADIO |
unknown; ambos deprecados en favor de Build.getSerial() y Build.getRadioVersion() |
MASTG recomienda «normalizar a minúsculas al comprobar estos valores», detalle que importa si uno busca las cadenas en el binario.
Ficheros y sockets característicos, comprobados con java.io.File.exists():
| Ruta | Qué es |
|---|---|
/dev/socket/qemud |
Socket del demonio de QEMU |
/dev/qemu_pipe |
Tubería de comunicación de QEMU |
/dev/goldfish_pipe |
Tubería del emulador Goldfish |
/sys/qemu_trace |
Marcador de traza de QEMU |
/dev/socket/genyd |
Socket del demonio de Genymotion |
/dev/socket/baseband_genyd |
Socket de banda base de Genymotion |
Telefonía: getLine1Number() devolviendo un número de la serie 15555215554…15555215584
—los sufijos pares—, getNetworkOperatorName() devolviendo android en minúscula, y
getVoiceMailNumber() devolviendo 15552175049. getLine1Number() está deprecado desde
Android 13 (API 33) en favor de SubscriptionManager.getPhoneNumber(int).
Paquetes de emuladores, con los prefijos com.vphone., com.bignox., com.nox.mopen.app,
me.haima., com.bluestacks, cn.itools., com.kop., com.kaopu., com.microvirt., y el
paquete exacto com.google.android.launcher.layouts.genymotion.
OpenGL: GLES20.glGetString(GL_RENDERER) conteniendo Bluestacks o Translator.
⚠️ Tres artefactos clásicos que no aparecen en MASTG-KNOW-0031, comprobado por búsqueda
sobre el fuente de la página: ro.kernel.qemu, /system/lib/libc_malloc_debug_qemu.so e IMEI
000000000000000. La propiedad ro.kernel.qemu es real y muy citada, pero no procede de
OWASP. Donde sí están documentadas propiedades del sistema con nombre propio es en el trabajo
de Maddie Stone, «Unpacking the Packed Unpacker» (Virus Bulletin 2018 / Black Hat USA
2018), que analiza un packer que «realiza más de 45 comprobaciones distintas en tiempo de
ejecución» y nombra init.svc.qemud, qemu.sf.fake_camera e init.svc.goldfish-setup, más
detección de Xposed vía la clase de/robv/android/xposed/XposedBridge.
Cómo se identifica en el binario: cualquiera de las cadenas de las tablas anteriores en el
string_ids del DEX o en un .so. Es la categoría más fácil de detectar estáticamente,
porque las comprobaciones son literalmente comparaciones contra constantes de texto — salvo
que estén cifradas (Ofuscadores comerciales, sección 2.2),
que es exactamente el motivo de que se cifren.
5. Detección de root y de Frida
5.1 Root — MASTG-KNOW-0027, MASTG-TEST-0045
| Categoría | Artefactos concretos |
|---|---|
Binarios su |
/sbin/su, /system/bin/su, /system/bin/failsafe/su, /system/xbin/su, /system/sd/xbin/su, /data/local/su, /data/local/xbin/su, /data/local/bin/su; además recorrer System.getenv("PATH") buscando su, y la vía nativa con la syscall stat |
| Otros binarios | /system/xbin/busybox |
| Ficheros de apps de rooting | /system/app/Superuser.apk, /system/etc/init.d/99SuperSUDaemon, /dev/com.koushikdutta.superuser.daemon/, /system/xbin/daemonsu |
| Paquetes de gestores | eu.chainfire.supersu, com.noshufou.android.su, com.koushikdutta.superuser, com.topjohnwu.magisk |
| Ejecución | Runtime.getRuntime().exec("su"), que lanza IOException si no existe |
| Procesos | Nombres que contengan supersu o superuser; el demonio daemonsu |
| Particiones | system o data montadas con flag rw |
| ROM personalizada | android.os.Build.TAGS conteniendo test-keys —MASTG lo ejemplifica con el método detectTestKeys() de RootBeer— |
| Certificados | Ausencia de los certificados OTA de Google |
5.2 Frida y herramientas de instrumentación — MASTG-KNOW-0030, MASTG-TEST-0048
| Categoría | Artefacto exacto |
|---|---|
| Mapeo de memoria | /proc/self/maps y /proc/self/smaps; agente mapeado como frida-agent-64.so desde /data/local/tmp/re.frida.server/; gadget embebido como libfrida-gadget.so |
| Procesos y servicios | El binario frida-server; getRunningServices, comando ps |
| Puerto TCP | «frida-server escucha en el puerto TCP 27042 por defecto» |
| Cadenas en memoria | LIBFRIDA, «que históricamente ha aparecido en los binarios de Frida Gadget y Frida Agent» |
| Nombres de hilo | gum-js-loop y gdbus, leídos en /proc/self/task/<tid>/status |
| Sockets Unix | /proc/self/net/unix, /proc/self/fd; sockets Unix abstractos con el prefijo frida-zymbiote- más un identificador aleatorio |
| Firma de la app | GET_SIGNING_CERTIFICATES desde API 28, contrastado con un certificado fijado en la aplicación |
⚠️ La cadena frida:rpc, que se cita mucho, no aparece en MASTG-KNOW-0030; la
documentada es LIBFRIDA. Y el prefijo de socket que documenta MASTG hoy es
frida-zymbiote-; re.frida.server aparece solo como componente de la ruta del agente.
La limitación que MASTG declara es la que más afecta al analista: «desde Android 7.0, nivel de
API 24, la visibilidad de procesos y servicios está restringida, y las APIs a nivel de
aplicación no exponen de forma fiable demonios no relacionados como frida-server». Por eso
las comprobaciones útiles hoy son las que miran dentro del propio proceso —/proc/self/*,
nombres de hilo, cadenas en memoria— y no las que enumeran el sistema.
La fuente primaria que la propia MASTG cita es «The Jiu-Jitsu of Detecting Frida», de Bernhard Mueller.
Cómo se identifica en el binario: las rutas /proc/self/maps, /system/xbin/su,
/data/local/tmp, los nombres de paquete com.topjohnwu.magisk y eu.chainfire.supersu, y
las cadenas frida, LIBFRIDA, gum-js-loop en el DEX o en los .so. Y, de nuevo, la
declaración de QUERY_ALL_PACKAGES o de una lista <queries> con gestores de root en el
manifiesto.
6. Comprobación de integridad en ejecución
Dos familias distintas: comprobar quién firmó y comprobar qué se está ejecutando.
6.1 Verificación de la firma desde el propio código
Es la defensa contra el reempaquetado: la aplicación comprueba que sigue estando firmada con el certificado de su autor y no con uno cualquiera.
Las APIs, con sus niveles confirmados en la referencia oficial:
| API | Nivel | Notas |
|---|---|---|
PackageManager.GET_SIGNING_CERTIFICATES |
28 | Valor 0x08000000. «Devuelve los certificados de firma asociados a este paquete. Cada entrada es un certificado que el paquete ha demostrado estar autorizado a usar, normalmente un certificado de firma anterior desde el que ha rotado» |
PackageManager.GET_SIGNATURES |
1, deprecado en 28 | Valor 0x40. «Este constante quedó obsoleta en el nivel de API 28: usa GET_SIGNING_CERTIFICATES en su lugar» |
PackageManager.hasSigningCertificate(String, byte[], int) |
28 | La documentación lo recomienda en lugar de getPackageInfo con GET_SIGNATURES «porque tiene en cuenta la posibilidad de rotación del certificado de firma». Acepta CERT_INPUT_RAW_X509 o CERT_INPUT_SHA256 |
PackageManager.getInstallerPackageName(String) |
5, deprecado en 30 | «Identifica de qué market vino el paquete». Sustituido por getInstallSourceInfo(String) |
PackageManager.getInstallSourceInfo(String) |
30 | Devuelve InstallSourceInfo. Sin permiso INSTALL_PACKAGES, getOriginatingPackageName() devuelve siempre null |
PackageManager.getVerifiedSigningInfo(String, int) |
36 | Verifica la firma de un fichero arbitrario sin exigir que sea un archivo de paquete |
SigningInfo |
28 | Constantes VERSION_JAR, VERSION_SIGNING_BLOCK_V2, VERSION_SIGNING_BLOCK_V3, VERSION_SIGNING_BLOCK_V4. Métodos getApkContentsSigners(), getSigningCertificateHistory(), getSchemeVersion(), hasMultipleSigners(), hasPastSigningCertificates(), signersMatchExactly(SigningInfo) |
La forma canónica es: obtener el SigningInfo, calcular el SHA-256 del certificado y
compararlo con un valor incrustado en el código —normalmente cifrado, y con frecuencia la
comparación misma movida a nativo—. getSchemeVersion() permite además exigir un esquema
mínimo: rechazar una instalación que solo tenga v1 elimina de golpe una clase de
manipulaciones (Esquemas de firma).
Guardsquare lo vende exactamente así: «DexGuard da a tu aplicación la capacidad de asegurar que ha sido firmada con el certificado original».
⚠️ El checksum del classes.dex leído de la propia entrada ZIP del APK y la
comprobación de getInstallerPackageName como controles anti-tamper son técnicas reales y
frecuentes, pero no se han encontrado documentadas como tales en ninguna página de MASTG de
las consultadas. Las APIs sí están confirmadas; su uso con ese fin, no. Se mencionan aquí
como observación, no como cita.
6.2 Integridad del código en ejecución
MASTG-KNOW-0032 («Runtime Integrity Verification») organiza esto en cuatro preguntas, que es la mejor taxonomía disponible:
| Pregunta | Categoría | Controles |
|---|---|---|
| ¿Ha cambiado el destino de una llamada indirecta? | Control Flow Integrity Checks | Detección de hooks en PLT/GOT, en vtables, verificación de los puntos de entrada de ART |
| ¿Ha cambiado el código ejecutable o los datos protegidos? | Code Integrity Verification | Checksums de memoria, detección de hooks inline |
| ¿Se ha cargado código ejecutable nuevo en el proceso? | Runtime Code Injection Detection | Detección de inyección de bibliotecas dinámicas |
| ¿Ha modificado un framework el runtime de la app? | Framework Runtime Modification Detection | Detección de Xposed |
Los artefactos concretos que nombra: permisos sospechosos en /proc/self/maps —rwxp donde
se espera r-xp—; la sección .data.rel.ro para las vtables; los campos de ArtMethod
entry_point_from_quick_compiled_code_, entry_point_from_interpreter_ y access_flags_,
más el flag kAccNative (0x0100); el trampolín ARM64 LDR X16, .+8 ; BR X16 con los bytes
50 00 00 58 00 02 1F D6 y su variante ARM32/Thumb LDR PC, [PC, #-4]; la inyección desde
rutas escribibles como /data/local/tmp; y la detección de Xposed por la clase
de/robv/android/xposed/XposedBridge en el classloader, o de LSPosed por
META-INF/xposed/java_init.list y META-INF/xposed/native_init.list.
Ese último par es una huella estática: se ve mirando el ZIP.
7. Lógica movida a nativo
La forma más simple y más efectiva de dificultar el análisis: sacar el código de Java/Kotlin
y ponerlo en C/C++ dentro de un .so
(Código nativo y ELF).
Por qué funciona tan bien, y no es porque el nativo sea intrínsecamente más difícil:
- El
DEXes un formato de alto nivel. Conserva tipos, firmas, jerarquía de clases y nombres de método. Un.socon los símbolos eliminados no conserva nada de eso: el decompilador de Ghidra tiene que inferir hasta el número de argumentos. - No hay decompilador de referencia gratuito y bueno.
jadxproduce Java legible en segundos; reconstruir C desde ARM64 es un trabajo manual de horas por función. - El anti-debug nativo es más potente, porque tiene acceso a
ptrace, a/procy al propio mapa de memoria del proceso (secciones 3 y 6.2). - Se puede empaquetar y ofuscar aparte con las mismas técnicas que un binario de escritorio.
Los packers de nivel 3 lo llevan al extremo con lo que Happer llama JNI Transformation: «los packers pueden usar código nativo para reimplementar métodos seleccionados de la aplicación».
Cómo se identifica: un lib/ grande frente a un classes.dex pequeño; muchos métodos
native declarados en el DEX —el flag ACC_NATIVE = 0x100 en el access_flags del
encoded_method, con code_off a cero
(Formato DEX, secciones 10 y 13)—; y en el .so, o bien
símbolos exportados con el patrón Java_<paquete>_<clase>_<método> —registro por
descubrimiento—, o bien solo JNI_OnLoad —registro dinámico con RegisterNatives(), que
oculta la correspondencia—.
Consecuencia práctica: hay que bajar a Ghidra. Ver Análisis binario y estático.
8. PairIP
Es la protección de integridad que Google Play aplica del lado de la tienda, sin que el desarrollador toque su código.
Lo que Google publica. La documentación oficial no usa nunca el nombre «PairIP»: lo llama automatic protection o Automatic Integrity Protection. Sus dos funciones, verbatim:
«La protección automática de Google Play es un servicio que ayuda a proteger tus aplicaciones y juegos contra la redistribución no autorizada y la piratería.»
«La protección automática puede añadir una comprobación de instalador de Google Play al código de tu aplicación, que ocurre en tiempo de ejecución cuando se abre la aplicación.»
«La protección automática puede añadir comprobaciones en tiempo de ejecución al código de tu aplicación para detectar modificaciones, y usar técnicas avanzadas de ofuscación para impedir que las comprobaciones se eliminen o se sometan a ingeniería inversa.»
La segunda función tiene requisitos de elegibilidad publicados: 25.000 $/mes de gasto de consumidores, o al menos 20.000 $ en cada uno de los seis meses anteriores, o al menos un millón de usuarios activos mensuales en una categoría sensible, o pertenencia a un programa de Google Play.
Cómo se identifica. El nombre real del paquete es com.pairip, no
com.google.android.pairip. Las huellas que se han podido verificar en fuentes de
identificación mantenidas:
| Huella | Dónde | Fuente |
|---|---|---|
Lcom/pairip/licensecheck/LicenseContentProvider; |
Cadena del DEX |
Regla google_aip_installer_check de APKiD, autor Ivan Baheux |
libpairipcore.so |
Nombre de la biblioteca nativa | Regla google_aip de APKiD, autor Eduardo Novella |
Export ExecuteProgram |
Símbolo de la ELF, junto a JNI_OnLoad y JNI_OnUnLoad |
Misma regla |
<activity android:name="com.pairip.licensecheck.LicenseActivity"/> |
Manifiesto | Issue #495 del repositorio de APKiD |
<provider android:name="com.pairip.licensecheck.LicenseContentProvider"/> |
Manifiesto | Ídem |
Clase com.pairip.VMRunner, método nativo executeVM([B[Ljava/lang/Object;)Ljava/lang/Object; |
DEX |
Análisis de Ahmethan Gültekin, 3 de marzo de 2025 |
Clase com.pairip.SignatureCheck con verifyIntegrity(context) |
DEX |
Repositorio Solaree/pairipcore |
El export ExecuteProgram y la clase VMRunner cuentan la historia: PairIP es una
protección basada en máquina virtual, del tipo descrito en la sección 3.3 de
Packers y protectores. El bytecode que interpreta viaja en
assets/.
⚠️ Varios puntos no confirmados que conviene no propagar: la clase
com.google.android.pairip.application.SuperpoweredApplication no existe en ninguna fuente
consultada; y los nombres concretos de los ficheros de assets/ no se han podido
verificar en ninguna fuente con autoría identificable.
Un matiz de detección que el propio issue de APKiD señala y que hay que respetar: la regla
actual confunde la protección automática (com.pairip) con el uso normal de la Play
Integrity API (com.google.android.play.core.integrity). Son cosas distintas: la primera la
inyecta Play, la segunda la integra el desarrollador.
El corpus de verificación no contiene ninguna muestra con PairIP. Buscada la subcadena
pairip en los 323 ficheros classes*.dex de los 86 contenedores —incluidos los cuatro
juegos, que es donde más se aplica—, cero coincidencias. Tampoco hay libpairipcore.so
en ningún lib/. El hueco sigue abierto y es honesto decirlo: todo lo de esta sección
procede de fuentes externas, no de observación propia.
9. Play Integrity
Es la otra mitad, y no debe confundirse con la anterior: PairIP la aplica Play al binario; la Play Integrity API la llama el desarrollador desde su código.
Su función, verbatim: «ayuda a comprobar que las acciones de usuario y las peticiones al servidor vienen de tu aplicación genuina, instalada por Google Play, ejecutándose en un dispositivo Android genuino y certificado».
Los veredictos que emite:
| Campo | Subcampo | Valores |
|---|---|---|
deviceIntegrity |
deviceRecognitionVerdict |
MEETS_DEVICE_INTEGRITY, MEETS_BASIC_INTEGRITY, MEETS_STRONG_INTEGRITY, MEETS_VIRTUAL_INTEGRITY. Un array vacío significa dispositivo comprometido o emulador |
deviceIntegrity |
recentDeviceActivity.deviceActivityLevel |
LEVEL_1 … LEVEL_4, UNEVALUATED |
appIntegrity |
appRecognitionVerdict |
PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, UNEVALUATED |
accountDetails |
appLicensingVerdict |
LICENSED, UNLICENSED, UNEVALUATED |
appAccessRiskVerdict |
appsDetected |
KNOWN_INSTALLED, UNKNOWN_INSTALLED, KNOWN_CAPTURING, UNKNOWN_CAPTURING, KNOWN_CONTROLLING, UNKNOWN_CONTROLLING, KNOWN_OVERLAYS, UNKNOWN_OVERLAYS |
environmentDetails |
playProtectVerdict |
NO_ISSUES, NO_DATA, POSSIBLE_RISK, MEDIUM_RISK, HIGH_RISK, UNEVALUATED |
Son opcionales, y hay que activarlos: MEETS_STRONG_INTEGRITY, appAccessRiskVerdict,
playProtectVerdict, recentDeviceActivity y deviceRecall (en beta).
Relación con SafetyNet Attestation. Es su sucesora directa. La página oficial de retirada
lo dice así: «la SafetyNet Attestation API quedó obsoleta en 2022 y se retiró por completo
en enero de 2025. Los desarrolladores deben migrar a la Play Integrity API, que consolida
varias ofertas de integridad —incluido el veredicto de integridad de SafetyNet Attestation—
bajo una única API». Y sobre el estado actual: «si intentas llamar a la SafetyNet Attestation
API recibirás un error. La API attest devuelve una tarea que siempre invoca el listener de
fallo con una ApiException y un código de estado 7 (NETWORK_ERROR)».
⚠️ La página, en su estado actual, ya no publica la tabla de hitos intermedios: solo da «2022»
y «enero de 2025», sin día.
Qué significa para quien analiza una aplicación legítimamente. Tres cosas, y ninguna es técnica de evasión:
- Los veredictos son señales, no verdad.
MEETS_DEVICE_INTEGRITYausente en un análisis sobre emulador es el resultado esperado y correcto: el dispositivo, efectivamente, no es uno certificado. - La comprobación es del lado del servidor. El veredicto viaja firmado por Google al backend de la aplicación. Lo que se observa en el dispositivo es la petición, no la decisión. Un análisis que solo mire el cliente no va a entender por qué la aplicación rechaza operar.
- La presencia de la API es una huella estática legítima y reportable: las clases
com.google.android.play.core.integrity.*en elDEXdicen que la aplicación depende de ella, lo cual es información de arquitectura útil y perfectamente publicable.
10. Los síntomas, que es lo que uno ve primero
El analista no encuentra estas protecciones: las padece. Esta tabla va en la dirección real de descubrimiento —del síntoma a la causa— y complementa Diagnóstico de fallos.
| Síntoma | Causa probable | Sección |
|---|---|---|
La aplicación cierra sola al arrancar en el emulador, sin traza en logcat |
Anti-emulador | 4 |
| Funciona en el emulador, cierra en el dispositivo rooteado | Detección de root | 5.1 |
| Se cierra en cuanto se engancha el depurador; o el depurador no llega a enganchar | Anti-debug: TracerPid, o ptrace sobre sí misma |
3 |
| Arranca, funciona unos segundos y muere sin excepción | Comprobación temporizada o comprobación en un hilo aparte | 3, 6.2 |
Muere en cuanto se inyecta Frida, o el proceso desaparece al adjuntar |
Detección de instrumentación: puerto 27042, /proc/self/maps, nombres de hilo |
5.2 |
| Arranca, pero cualquier operación de red devuelve error de autorización | Play Integrity, veredicto rechazado en el servidor | 9 |
| Reempaquetada y firmada de nuevo, se instala y arranca pero se cierra o se degrada | Verificación de la firma desde el propio código | 6.1 |
El decompilador produce cientos de clases con métodos vacíos o native |
Lógica movida a nativo | 7 |
El classes.dex tiene doscientas clases de infraestructura y nada más |
Packer | 03 |
jadx produce código con goto ilegales que no compila |
Ofuscación de flujo de control | 02, 2.4 |
Todas las cadenas del DEX son basura binaria |
Cifrado de cadenas | 02, 2.2 |
Y la comprobación previa, que cuesta un minuto y ahorra varias horas: antes de ejecutar
nada, mirar el manifiesto y las cadenas. QUERY_ALL_PACKAGES, una lista <queries> con
com.topjohnwu.magisk, un com.pairip.licensecheck.LicenseActivity, un libpairipcore.so,
la cadena TracerPid, la cadena /system/xbin/su: todo eso es estático, gratis, y dice de
antemano qué va a pasar.
Fuentes
- OWASP MASTG — MASTG-KNOW-0028, «Anti-Debugging» —
https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0028/
Consultado el 12 de agosto de 2026, con el fuente en
raw.githubusercontent.com/OWASP/owasp-mastg/master/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0028.md. De aquí sale íntegra la tabla de la sección 3, incluido el matiz deTracerPidy las estructuras de JDWP. Test asociado: MASTG-TEST-0046. - OWASP MASTG — MASTG-KNOW-0031, «Emulator Detection» — https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0031/ Consultado el 12 de agosto de 2026. De aquí salen las cuatro tablas de la sección 4 y los cambios de plataforma de la sección 2. Test asociado: MASTG-TEST-0049.
- OWASP MASTG — MASTG-KNOW-0027, «Root Detection» — https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0027/ Consultado el 12 de agosto de 2026. De aquí sale la tabla de la sección 5.1. Test asociado: MASTG-TEST-0045.
- OWASP MASTG — MASTG-KNOW-0030, «Reverse Engineering Tool Detection» — https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0030/ Consultado el 12 de agosto de 2026. De aquí sale la tabla de la sección 5.2, las dos citas sobre artefactos y falsos positivos, y la referencia a «The Jiu-Jitsu of Detecting Frida» de Bernhard Mueller. Test asociado: MASTG-TEST-0048.
- OWASP MASTG — MASTG-KNOW-0032, «Runtime Integrity Verification» — https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0032/ Consultado el 12 de agosto de 2026. De aquí salen la taxonomía de cuatro preguntas de la sección 6.2 y todos sus artefactos concretos.
PackageManager— https://developer.android.com/reference/android/content/pm/PackageManager ySigningInfo— https://developer.android.com/reference/android/content/pm/SigningInfo Consultados el 12 de agosto de 2026. De aquí sale íntegra la tabla de la sección 6.1, con los niveles de API y los valores constantes.- Strazzere, T. y Sawyer, J. — «Android Hacker Protection Level 0», DEF CON 22 (2014) —
https://www.defcon.org/images/defcon-22/dc-22-presentations/Strazzere-Sawyer/DEFCON-22-Strazzere-and-Sawyer-Android-Hacker-Protection-Level-UPDATED.pdf
Consultado el 12 de agosto de 2026. De aquí sale
ptrace(PTRACE_TRACEME)yfind_qemud_process()de la sección 3, que MASTG no documenta. - Stone, M. — «Unpacking the Packed Unpacker», Virus Bulletin 2018 / Black Hat USA 2018 —
https://i.blackhat.com/us-18/Thu-August-9/us-18-Stone-Unpacking-The-Packed-Unpacker.pdf
Consultado el 12 de agosto de 2026. De aquí salen las «más de 45 comprobaciones distintas»
y las propiedades
init.svc.qemud,qemu.sf.fake_cameraeinit.svc.goldfish-setupde la sección 4. - Prevent unauthorized redistribution with automatic protection (Google Play Console) — https://support.google.com/googleplay/android-developer/answer/10183279 Consultado el 12 de agosto de 2026. De aquí salen las tres citas de la sección 8 y los requisitos de elegibilidad.
- APKiD —
apkid/rules/dex/protectors.yarayelf/protectors.yara—raw.githubusercontent.com/rednaga/APKiD/master/apkid/rules/Consultados el 12 de agosto de 2026. De aquí salen las reglasgoogle_aip_installer_check(autor Ivan Baheux) ygoogle_aip(autor Eduardo Novella) conLcom/pairip/licensecheck/LicenseContentProvider;,libpairipcore.soy el exportExecuteProgramde la sección 8. - APKiD issue #495 — https://github.com/rednaga/APKiD/issues/495 y repositorio
Solaree/pairipcore — https://github.com/Solaree/pairipcore
Consultados el 12 de agosto de 2026. De aquí salen las entradas de manifiesto
com.pairip.licensecheck.LicenseActivityyLicenseContentProvider, las clasescom.pairip.VMRunnerycom.pairip.SignatureCheck, y la advertencia sobre la confusión entrecom.pairipycom.google.android.play.core.integrity. - Gültekin, A. — «Reversing Google's New VM-Based Integrity Protection: PairIP»,
3 de marzo de 2025 —
https://blog.byterialab.com/reversing-googles-new-vm-based-integrity-protection-pairip/
Consultado el 12 de agosto de 2026. De aquí salen
VMRunner, la firma deexecuteVMy el registro porRegisterNativesenJNI_OnLoadde la sección 8. Se han tomado únicamente los marcadores de identificación. - Play Integrity API — Overview y Verdicts — https://developer.android.com/google/play/integrity/overview y https://developer.android.com/google/play/integrity/verdicts Consultados el 12 de agosto de 2026. De aquí salen la definición y la tabla completa de veredictos de la sección 9.
- SafetyNet Attestation deprecation timeline — https://developer.android.com/privacy-and-security/safetynet/deprecation-timeline Consultado el 12 de agosto de 2026. De aquí salen las dos citas sobre la retirada de la sección 9.
- DexGuard factsheet 2024 (Guardsquare) — https://www.guardsquare.com/hubfs/Website/Resources/Fact%20sheets/factsheet-DexGuard-2024.pdf Consultado el 12 de agosto de 2026. De aquí sale la lista de seis mecanismos RASP de la sección 2 y la cita sobre comprobación de certificado de la sección 6.1.
- Xue, Luo et al. — «Happer» — https://www4.comp.polyu.edu.hk/~csxluo/Happer.pdf Consultado el 12 de agosto de 2026. De aquí sale la JNI Transformation de la sección 7.
- Mediciones propias sobre el corpus — ejecutadas el 12 de agosto de 2026 sobre
~/corpus-apkcon Python 3.12. De aquí sale la comprobación de ausencia depairipen los 323classes*.dexde la sección 8.