Anti-análisis y hardening
Antes conviene leer «Packers y protectores», «Código nativo y ELF»
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 · Los síntomas, que es lo que uno ve primero» 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.
Versiones de referencia: OWASP MASTG v2.0.0 y su rama master, APKiD 3.1.0 y su rama
master, y Android hasta el nivel de API 36, consultadas en agosto y septiembre de 2026.
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. No todo el RASP viene de un ofuscador:
APKiD 3.1.0 identifica también Appdome, con las reglas appdome_dex —la clase
Lruntime/loading/InjectedActivity;, que según el comentario de la regla se inyecta en todo— y
appdome_elf —las parejas de símbolos __start_adinit/__stop_adinit,
__start_hook/__stop_hook o __start_ipcent/__stop_ipcent—; en master se suma
appdome_elf_b, que busca libloader.so.
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, hoy marcado deprecated y sustituido en master por MASTG-TEST-0352 y
MASTG-TEST-0353; en la versión publicada, MASTG v2.0.0 (30 de junio de 2026), los sustitutos
son MASTG-TEST-0046-1 y MASTG-TEST-0046-2. 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, hoy marcado deprecated y
sustituido por MASTG-TEST-0351. 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 → §2.2 · Cifrado de cadenas»),
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
MASTG-TEST-0045 está marcado deprecated; lo sustituyen MASTG-TEST-0324 y MASTG-TEST-0325.
| 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
MASTG-TEST-0048 está marcado deprecated; lo sustituye MASTG-TEST-0341.
| 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 (la clase) |
28 | Métodos getApkContentsSigners(), getSigningCertificateHistory(), hasMultipleSigners(), hasPastSigningCertificates() |
SigningInfo.getSchemeVersion() |
35 | Añadido con las APIs de archivado. Exige Android 15, no 28 |
SigningInfo.VERSION_JAR y VERSION_SIGNING_BLOCK_V2/V3/V4, más signersMatchExactly(SigningInfo) |
36 | Mismo nivel que getVerifiedSigningInfo de la fila anterior |
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»). Pero solo desde
Android 15, y las constantes con las que comparar su resultado, desde el 16: una aplicación
con minSdk menor necesita una vía alternativa.
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 sí está documentado
como control anti-tamper: MASTG-KNOW-0029, «File Integrity Checks», con implementación de
referencia sobre ZipEntry.getCrc() —new ZipFile(getPackageCodePath()), getEntry("classes.dex"),
comparación contra un valor guardado en recursos— y su test asociado MASTG-TEST-0047,
hoy marcado deprecated y sustituido por MASTG-TEST-0338. La
misma página enumera AndroidManifest.xml, los *.dex y los *.so como los ficheros que
habitualmente se protegen así.
⚠️ Lo que no está documentado en MASTG es la comprobación de getInstallerPackageName
como control anti-tamper: una búsqueda de código sobre el repositorio de OWASP devuelve cero
apariciones del identificador. Es una técnica real y frecuente, pero aquí se menciona 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; solo en master, desde el PR #496 |
libpairipcore.so |
Nombre de la biblioteca nativa | Regla googleIntegrityProtection de APKiD 3.1.0, que en master se llama google_aip_elf, 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 · Tres niveles de sofisticación». 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 issue #495 de APKiD señaló y que hay que respetar: la regla de
la versión 3.1.0, googleIntegrityProtection, detecta libpairipcore.so pero lo describe
como «Google Play Integrity», es decir, 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 PR
#496, fusionado en master el 21 de mayo de 2026 y aún sin versión publicada, las separa:
google_aip_installer_check y google_aip_elf para la protección automática y una regla
nueva, google_playintegrity_api, para la API. Esta última también lleva la etiqueta
protector, así que con master esa etiqueta salta con cualquier uso de la Play Integrity
API.
El «muestrario» no contiene ninguna muestra con PairIP. Buscado en los 717 APK —58
standalone más 659 dentro de contenedores—, incluidos los cuatro juegos, que es donde más se
aplica: la subcadena pairip no aparece en ninguno de los 323 ficheros classes*.dex, y
no hay libpairipcore.so en ningún lib/. Cero coincidencias. 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 |
deviceIntegrity |
deviceAttributes.sdkVersion |
Nivel de API de Android del dispositivo (33 es Android 13). Sin evaluar, deviceAttributes va vacío |
appIntegrity |
appRecognitionVerdict |
PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, UNEVALUATED |
accountDetails |
appLicensingVerdict |
LICENSED, UNLICENSED, UNEVALUATED |
environmentDetails |
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_BASIC_INTEGRITY, MEETS_STRONG_INTEGRITY,
deviceAttributes, appAccessRiskVerdict, playProtectVerdict, recentDeviceActivity y
deviceRecall (en beta). Los dos del entorno llegan juntos dentro de environmentDetails.
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 · Anti-debug», 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 · Anti-emulador» y los cambios de plataforma de la «sección 2 · Qué es y por qué existe». 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 · Root — MASTG-KNOW-0027, MASTG-TEST-0045». 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 · Frida y herramientas de instrumentación — MASTG-KNOW-0030, MASTG-TEST-0048», 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 · Integridad del código en ejecución» 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 · Verificación de la firma desde el propio código», 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 · Anti-debug», 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 · Anti-emulador». - 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 · PairIP» 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_elf(autor Eduardo Novella) conLcom/pairip/licensecheck/LicenseContentProvider;,libpairipcore.soy el exportExecuteProgramde la «sección 8 · PairIP». - 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 · PairIP». 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 · Play Integrity».
- 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 · Play Integrity».
- DexGuard factsheet 2024 (Guardsquare) — «https://www.guardsquare.com/hubfs/Website/Resources/Fact sheets/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 · Qué es y por qué existe» y la cita sobre comprobación de certificado de la «sección 6.1 · Verificación de la firma desde el propio código».
- 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 · Lógica movida a nativo».
- OWASP MASTG — MASTG-TEST-0045 a MASTG-TEST-0049 —
https://mas.owasp.org/MASTG/tests/android/MASVS-RESILIENCE/MASTG-TEST-0045/ y siguientes,
con el fuente en
github.com/OWASP/mastg,tests/android/MASVS-RESILIENCE/Consultado el 25 de septiembre de 2026. De aquí sale que los cinco tests están marcadosstatus: deprecated, con sus sustitutos encovered_by: 0324 y 0325, 0352 y 0353, 0338, 0341 y 0351 (secciones «3», «4», «5.1», «5.2» y «6.1»). - Play Integrity API — Integrity verdicts —
https://developer.android.com/google/play/integrity/verdicts
Consultado el 25 de septiembre de 2026. De aquí salen
environmentDetailscomo contenedor deappAccessRiskVerdictyplayProtectVerdict,MEETS_BASIC_INTEGRITYcomo etiqueta opcional ydeviceAttributes.sdkVersion(«sección 9 · Play Integrity»). - APKiD — etiqueta
v3.1.0, PR #496 y reglasappdome_*— https://github.com/rednaga/APKiD/pull/496 y https://github.com/rednaga/APKiD/blob/v3.1.0/apkid/rules/dex/protectors.yara Consultados el 25 de septiembre de 2026. De aquí salen la reglagoogleIntegrityProtectionde la 3.1.0, su sustitución enmasterpor el commit5ae8f40del 21 de mayo de 2026 («sección 8 · PairIP») y las reglasappdome_dexyappdome_elf(«sección 2 · Qué es y por qué existe»). - OWASP MASTG, etiqueta
v2.0.0frente amaster— https://github.com/OWASP/mastg/releases/tag/v2.0.0, con el fuente enraw.githubusercontent.com/OWASP/mastg/v2.0.0/y.../master/,tests/android/MASVS-RESILIENCE/yknowledge/android/MASVS-RESILIENCE/Consultado el 25 de septiembre de 2026. De aquí sale que v2.0.0, del 30 de junio de 2026, es la última versión publicada; que en ella MASTG-TEST-0046 remite a MASTG-TEST-0046-1 y MASTG-TEST-0046-2 y no a 0352 y 0353, mientras que 0045, 0047, 0048 y 0049 tienen los mismos sustitutos que enmaster; y que MASTG-KNOW-0027 a 0032 no difieren en contenido entre las dos («secciones 3 · Anti-debug» a 6). rednaga/APKiD,apkid/rules/elf/protectors.yaraenmaster— https://github.com/rednaga/APKiD/blob/master/apkid/rules/elf/protectors.yara Consultado el 25 de septiembre de 2026. De aquí sale la reglaappdome_elf_b, ausente env3.1.0(«sección 2 · Qué es y por qué existe»).