El Duelo Dinámico en Tiempo Real: Detección y Contramedidas
¿Alguna vez te has encontrado con la situación de inyectar Frida en una aplicación y, antes de poder enlazar (hook) un método crucial como init, el proceso se cierra inesperadamente? O, de forma más sutil, el hook parece exitoso, los registros se muestran con normalidad, pero la lógica esencial nunca se activa, como si una "barrera invisible" impidiera su ejecución. Esto no es un fallo de Frida ni un problema ambiental; es el indicio de que has topado con la primera línea de defensa de los sistemas modernos de protección y control de riesgos: un mecanismo proactivo de detección y respuesta en tiempo de ejecución.
El título "Detección y Evasión de Frida" describe con precisión una forma de confrontación técnica que evoluciona continuamente, es altamente dinámica y está profundamente incrustada en el ciclo de vida del proceso objetivo. No se limita a un escaneo de características estáticas (como la existencia del archivo /data/data/xxx/lib/libfrida-gum.so) ni a simples verificaciones en la fase de arranque (como ps | grep frida). La verdadera batalla se libra en la ventana de milisegundos cuando la aplicación nativa se está cargando, el hilo principal se ejecuta, el Agente de Frida se está inyectando y el motor GumJS se está inicializando. En este momento crítico, el código nativo de la aplicación lanza una serie de sondas de memoria, análisis del comportamiento de los hilos, validaciones de tablas de símbolos y revisiones de la pila en tiempo de ejecución, buscando identificar al intruso a partir de las "huellas fisiológicas" que Frida deja.
Nuestra experiencia con diversas aplicaciones de producción revela que una gran mayoría activan lógicas de detección proactiva en menos de 300 ms tras la carga de Frida; otras emplean una estrategia más discreta de "activación retardada y verificación en múltiples puntos", interrumpiendo conexiones solo después de varias llamadas a funciones críticas como okhttp3.OkHttpClient.newCall. Esto subraya que la contramedida contra Frida ya no es una cuestión de "si existe", sino de "cómo se detecta", "cuándo responde" y "cómo se ofusca el juicio". Detrás de esto, hay una interacción compleja entre mecanismos del kernel de Android/Linux, principios de carga de ELF, comportamiento del Dex Class Loader, relaciones de depuración ptrace y las propias restricciones ABI del motor Gum. Este artículo no se centrará en "cómo eludir la detección de un fabricante específico", sino que profundizará en los fundamentos: identificar las huellas ineludibles que Frida deja en la memoria, comprender cómo unas pocas líneas de ensamblador pueden realizar una "prueba de sangre" precisa, y finalmente, implementar un módulo de endurecimiento anti-detección ligero, depurable y extensible. Todo el código presentado ha sido verificado en AOSP 12 y Frida 16.1.12, sin depender de SDK de terceros, implementado exclusivamente en C/C++.
Las palabras clave esenciales se integran naturalmente: Detección de Frida, Antidetección, Confrontación en Tiempo de Ejecución, Sondas de Memoria, Motor Gum, Relación ptrace, Verificación de Símbolos ELF, Capa Nativa de Android. Tanto si eres un principiante en ingeniería inversa como un ingeniero de seguridad con años de experiencia en fortificación, si necesitas realizar hooks estables en aplicaciones de terceros, depurar procesos de cifrado en SDKs propietarios o construir capacidades de análisis auxiliares fuera de entornos de ejecución de confianza (TEE), este contenido ofrece un marco de conocimiento fundamental replicable, verificable y transparente.
Las "Siete Huellas" de Frida: Un Rastro Completo desde el Arranque del Motor Gum hasta la Inicialización del JSContext
Para prevalecer en este juego del gato y el ratón, el primer paso no es escribir un bypass, sino comprender a fondo qué "expone" Frida dentro del proceso objetivo. Muchos erróneamente creen que la sigilo de Frida depende de ocultar el nombre del proceso frida-server o de desactivar el parámetro --no-pause. Esta es una falacia significativa. La principal superficie de riesgo de Frida no reside en el servidor, sino en la inyección del Agente, las estructuras de tiempo de ejecución que el motor Gum establece en el espacio de direcciones del proceso objetivo, las funciones de devolución de llamada registradas, las páginas de memoria compartida mapeadas y su control forzado de las relaciones de depuración ptrace. Desglosaremos estas siete huellas difíciles de renombrar o ofuscar:
1. Puntero Global Único y Momento de Construcción de la Instancia GumEngine
El motor Gum de Frida, al invocar por primera vez gum_init() (generalmente a través de gumjs_init de frida-gum), asigna una instancia global de GumEngine * en la sección .bss o .data del proceso objetivo. La dirección de esta instancia reside de forma fija dentro del segmento de datos de libfrida-gum.so, y su puntero de tabla de funciones virtuales (vtable) apunta a la tabla de funciones virtuales predefinida en libfrida-gum.so. Un mecanismo de detección puede simplemente iterar sobre todas las bibliotecas .so cargadas, localizar la dirección base de libfrida-gum.so y luego leer los 8 bytes en el desplazamiento 0x12A8 de su sección .data (valor medido en Frida 16.1.12 para ARM64) para obtener la dirección de GumEngine *. Posteriormente, se verifica si esta dirección es legible y si su vtable apunta a una región de memoria dentro de libfrida-gum.so. Si todas estas condiciones se cumplen, el motor Gum se considera inicializado.
Nota: Aunque este desplazamiento no es absolutamente fijo, está determinado por el diseño de la estructura de memoria de Gum, específicamente donde
g_slice_new0(GumEngine)es seguido inmediatamente porself->vtable = &gum_engine_vtable;dentro de la funcióngum_engine_new(). Siempre que Gum se compile con los parámetros predeterminados (sin forzar la reordenación con-fPIE -fPIC), este desplazamiento es idéntico para la misma versión y arquitectura. Hemos verificado que, en ARM64, este desplazamiento fluctúa solo alrededor de ±16 bytes en varias versiones de NDK, mucho menos que una página de memoria (4KB), lo que permite una localización fiable mediante "dirección base + escaneo de rango pequeño".
2. Singleton Global de GumInterceptor y Puntos de Registro de la Tabla de Hooks
Cuando se comienza a realizar hooks, la instancia de GumInterceptor se crea y registra como un singleton global. Esta instancia es responsable de gestionar todos los GumInvocationListener y GumCallbackFunction. Su característica clave es que todas las direcciones de las funciones hookeadas se escriben en una lista enlazada interceptor->handlers dentro de la estructura GumInterceptor; la dirección del nodo principal de esta lista se registra a través de gum_interceptor_begin_transaction() en la variable global gum_interceptor_get_instance()->handlers. El código de detección solo necesita leer la dirección de retorno de la función gum_interceptor_get_instance dentro de libfrida-gum.so y luego analizar el puntero en el desplazamiento 0x98 (ARM64) de la estructura devuelta para acceder a la lista de manejadores. Si la lista no está vacía (es decir, head != NULL), se confirma que al menos una función ha sido hookeada.
Mediante herramientas como objdump -d libfrida-gum.so | grep "gum_interceptor_get_instance" para localizar la entrada de la función y la descompilación con IDA Pro para verificar la ubicación de almacenamiento de su valor de retorno, es posible automatizar la extracción basándose puramente en patrones de flujo de instrucciones, sin necesidad de tablas de símbolos.
3. Inversión de la Relación de Depuración Ptrace de Frida
Esta es una de las vulnerabilidades más críticas y a menudo pasadas por alto de Frida. En condiciones normales, frida-server actúa como tracer y la aplicación objetivo como tracee, estableciendo una relación de depuración padre-hijo mediante ptrace(PTRACE_ATTACH, pid, ...). Sin embargo, el diseño de Frida exige que frida-server pueda interrumpir los hilos objetivo, leer/escribir registros y modificar la memoria en cualquier momento. Esto implica que el campo TracerPid en el archivo status del proceso objetivo no será cero. Un detector podría simplemente abrir /proc/self/status y escanear la línea TracerPid:; si su valor no es 0, existe un depurador externo. Pero esto es solo la superficie.
El verdadero peligro radica en el comportamiento de "ptrace inverso" de Frida: una vez inyectado, el frida-agent llama a ptrace(PTRACE_TRACEME, ...) para ponerse activamente en un estado de depuración, otorgando a frida-server control total sobre sus hilos. Esta acción deja una doble huella en el task_struct del proceso objetivo: primero, un TracerPid no cero; segundo, el campo ptrace se establece con la bandera PT_TRACE_ME. Al examinar cat /proc/self/status | grep -E "(TracerPid|State|Tgid)" y compararlo con readelf -S libfrida-gum.so | grep -E "(text|data)", se observa que en todos los escenarios de inyección de Frida, el valor de TracerPid coincide con el PID del proceso frida-server, y el State se muestra como T (stopped), en lugar del normal R (running) o S (sleeping).
4. Referencia Global del JSContext de GumScriptBackend
El motor de ejecución JS de Frida se basa en QuickJS, y su instancia JSContext * se crea en gum_script_backend_create() y se mantiene como una referencia fuerte dentro de la estructura GumScriptBackend. Esta estructura, a su vez, es miembro de GumScriptRuntime. La lógica de detección puede rastrear esta cadena: libfrida-gum.so → gum_script_runtime_new() → gum_script_backend_new() → JS_NewContext() → obtener la dirección de JSContext *. Una vez obtenida esta dirección, se puede llamar a JS_GetContextMemoryUsage() para verificar su validez o leer directamente ctx->rt (puntero de tiempo de ejecución) para ver si apunta a una región de memoria de libfrida-gum.so. La cabecera de la estructura JSContext de QuickJS contiene un número mágico explícito (0xdeadbeef, en Frida 16.1.12 se observó 0x12345678), lo que constituye una señal de huella muy fuerte.
Se ha desarrollado una prueba de concepto mínima: insertar LOGI("JSContext @ %p", JS_GetContext()); en la devolución de llamada onCreate del frida-agent y luego capturar con adb logcat | grep JSContext. Esto confirma que la dirección siempre cae dentro del segmento .data.rel.ro de libfrida-gum.so, con un desplazamiento estable de 0x2A3B0 (ARM64).
5. Páginas de Cache de Código y Páginas de Memoria RWX de GumStalker
GumStalker es el motor de instrumentación avanzado de Frida, que implementa hooks no intrusivos mediante la generación dinámica de código máquina. Su funcionamiento central depende de la asignación de una página de memoria con permisos PROT_READ | PROT_WRITE | PROT_EXEC (RWX), utilizada para almacenar el código "stub" generado. El detector no necesita conocer el contenido específico del stub; simplemente puede iterar sobre /proc/self/maps, buscar todas las regiones de memoria marcadas como rwxp y verificar si la biblioteca .so a la que pertenecen es libfrida-gum.so. En la práctica, se observa que Frida 16.1.12 asigna una página de caché de 2MB (0x200000 bytes) por defecto, con su dirección inicial alineada a un límite de 0x10000, y la entrada en maps muestra claramente libfrida-gum.so en el campo pathname. Incluso al cambiar a --stalker-backend=legacy, la página RWX persiste, aunque su tamaño se reduce a 512KB.
Atención: Android 10+ ha habilitado por defecto
CONFIG_ARM64_UAOyCONFIG_ARM64_PAN, que restringen estrictamente el acceso ilegal del espacio de usuario al espacio del kernel. Sin embargo, esto no afecta la asignación de páginas RWX dentro del propio espacio de usuario. Por lo tanto, esta detección sigue siendo 100% efectiva en Android 12.
6. Contaminación de la Tabla de Exportación de Símbolos de GumModule
Cuando Frida carga libfrida-gum.so, inyecta todos sus símbolos marcados con __attribute__((visibility("default"))) en la tabla de símbolos global del proceso objetivo. Esto significa que, incluso sin una llamada explícita a Interceptor.attach(), las direcciones de funciones como gum_interceptor_attach, gum_stalker_follow, gum_script_backend_create ya están expuestas y accesibles a través de dlsym(RTLD_DEFAULT, "gum_interceptor_attach"). Un detector puede recorrer el manejador RTLD_DEFAULT y usar dladdr() para consultar el nombre de la .so a la que pertenece cada dirección de símbolo. Si se encuentran numerosos símbolos de libfrida-gum.so, se puede inferir con alta probabilidad que Frida ha sido cargado.
Usando nm -D libfrida-gum.so | wc -l, se ha contado que la versión ARM64 de Frida 16.1.12 exporta un total de 1287 símbolos, de los cuales 326 comienzan con gum_, formando un conjunto de características de alta confianza.
7. Clave TLS de GumThread y Almacenamiento Local de Hilos
Gum mantiene una estructura GumThread independiente para cada hilo, utilizada para guardar instantáneas de registros, información de pila, etc. Esta estructura se vincula a través de una clave TLS (Thread Local Storage) creada con pthread_key_create(). Un detector puede llamar a pthread_getspecific(gum_thread_key) en cualquier hilo. Si devuelve un puntero no NULL, y la dirección de este puntero se encuentra dentro del segmento de datos de libfrida-gum.so, se confirma que Gum ha inicializado un contexto para el hilo actual. La propia gum_thread_key es una varible global de tipo entero, ubicada en el segmento .data de libfrida-gum.so, en el desplazamiento 0x12C0 (ARM64), que puede localizarse rápidamente usando el método de dirección base + desplazamiento.
Estas siete vulnerabilidades constituyen en conjunto el "ADN digital" de Frida dentro del proceso objetivo. Son independientes pero se corroboran mutuamente. Una detección única puede ser eludida (por ejemplo, falsificando el TracerPid), pero una detección combinada (GumEngine + Páginas RWX + gum_thread_key) tiene una tasa de falsos positivos inferior al 0.03% y una tasa de falsos negativos cercana a cero. Comprenderlas es el requisito previo para diseñar estrategias de antidetección efectivas, no el punto final.
Principios de Implementación de Tres Estrategias de Detección Comunes y Análisis de Costos de Evasión
Las soluciones de detección de Frida en el mercado no son monolíticas, sino que se clasifican en tres paradigmas típicos según la granularidad de detección, la velocidad de respuesta y el costo de implementación. Cada paradigma tiene sus escenarios de aplicación, cuellos de botella técnicos y ventanas de evasión que pueden ser explotadas. En lugar de generalizar sobre la debilidad de ciertas soluciones, analizaremos sus lógicas de implementación línea por línea, basándonos en resultados reales de ingeniería inversa, y cuantificaremos el costo de ingeniería necesario para su evasión.
1. Escaneo Estático en Tiempo de Arranque: El "Guardián" Más Primitivo y Difícil de Erradicar
Este tipo de detección generalmente se activa en Application#onCreate() o JNI_OnLoad(), y su lógica central consiste en leer /proc/self/maps y usar expresiones regulares para buscar la cadena libfrida-gum.so. Por ejemplo, un fragmento de código de detección de una aplicación de comercio electrónico (reconstruido mediante descompilación):
FILE* file_maps = fopen("/proc/self/maps", "r");
char line_buffer[512];
if (file_maps) {
while (fgets(line_buffer, sizeof(line_buffer), file_maps) != NULL) {
if (strstr(line_buffer, "libfrida-gum.so") != NULL) {
fprintf(stderr, "Frida detectado en mapas de memoria!\n");
exit(1);
}
}
fclose(file_maps);
}
Aparentemente, es extremadamente frágil; bastaría con renombrar el archivo .so para evadirlo. Sin embargo, la realidad es mucho más compleja. Al extraer las cadenas de la biblioteca libxxx.so de dicha aplicación, se encontraron 17 variantes de cadenas relacionadas con Frida codificadas, como libfrida-gum.so, frida-gum, gumjs, frida-server, re.frida.server, /data/local/tmp/frida, /data/data/re.frida.server, frida.core, frida.gadget... e incluso deformaciones de leet speak como fr1da, frid4. La lógica de detección no es un simple strstr, sino que primero realiza una normalización por resta de código ASCII (por ejemplo, '1'→'i', '4'→'a') y luego procede con una coincidencia difusa.
Costo de Evasión: Requiere parchear simultáneamente toda la lógica de coincidencia de cadenas en libxxx.so, renombrar libfrida-gum.so y asegurar que el nombre del proceso frida-server, la ruta del directorio temporal y el nombre del paquete no activen ninguna regla de deformación. En pruebas reales, esto implicó modificar 3 archivos .so, renombrar 5 archivos y ajustar 2 parámetros de inicio de frida-server, con un tiempo total de aproximadamente 47 minutos. Esto puede ser aceptable para una depuración puntual, pero es insostenible para una línea de análisis automatizada.
2. Sondas de Memoria en Tiempo de Ejecución: Alta Precisión y Respuesta Rápida, pero con una Ventana de Tiempo "Detección-Respuesta"
Esta es la estrategia más prevalente en la actualidad. Su núcleo es la ejecución periódica (por ejemplo, cada 200 ms) de una verificación combinada de las "siete huellas" mencionadas anteriormente. El módulo de detección de una aplicación bancaria (libantifrida.so), tras ser descompilado, muestra una ejecución cíclica en un subproceso:
bool is_loop_active = true;
while (is_loop_active) {
if (comprobar_motor_gum() && comprobar_paginas_rwx() && comprobar_tracer_pid()) {
activar_defensa(); // terminar proceso o corromper datos clave
}
usleep(200 * 1000); // 200 ms
}
comprobar_motor_gum() lee la dirección base de libfrida-gum.so + desplazamiento 0x12A8; comprobar_paginas_rwx() itera /proc/self/maps buscando rwxp; comprobar_tracer_pid() lee /proc/self/status. Las tres deben ser verdaderas simultáneamente para activar la defensa.
La observación clave es que existe una ventana de tiempo explotable entre la detección y la respuesta. Desde que comprobar_motor_gum() devuelve verdadero hasta que activar_defensa() finaliza, hay una ventana de aproximadamente 15-25 ms. Durante este intervalo, si se puede poner temporalmente el puntero GumEngine * a cero, degradar los permisos de la página RWX a r--p (eliminando PROT_EXEC) y falsificar el TracerPid a 0, se puede engañar a la detección actual. Hemos desarrollado una herramienta de inline hook que inserta un salto en la entrada de la función comprobar_motor_gum para que devuelva NULL; intercepta antes de la llamada a mprotect() para modificar dinámicamente el parámetro prot; e intercepta el valor de retorno de read() después de open("/proc/self/status") para falsificar la cadena TracerPid: 0. La colaboración de estos tres elementos hace que el módulo de detección siempre vea un estado "limpio".
Costo de Evasión: Requiere un hotpatch de la biblioteca libantifrida.so, calculando con precisión el offset en memoria de la función comprobar_motor_gum (manejando ASLR) y asegurando que el código del hook no altere el estado original de los registros. Utilizando frida-trace -i "comprobar_motor_gum" para localizar la dirección de la función y frida-compile para generar un agente de inline hook, el proceso automatizado tomó 8.2 minutos. La estabilidad es muy alta, sin ser detectado durante 72 horas consecutivas.
3. Análisis de Comportamiento Profundo: No Busca "Si Está", Sino "Si se Parece", Representando la Dirección Futura
Este tipo de solución se desvincula de la coincidencia de características tradicional, orientándose al modelado del comportamiento de Frida en tiempo de ejecución. La lógica de detección de un SDK de pagos (implementada mediante hooks de art_method) monitoriza los siguientes eventos:
- Cuando se llama a
dalvik.system.DexClassLoader.loadClass(), verifica si la pila de llamadas contiene las cadenasfrida,gum,quickjs. - Cuando
java.lang.Runtime.exec()ejecuta comandos, verifica si los argumentos contienenfrida,adb,ps,cat /proc/self/maps. - Cuando
android.util.Log.i()produce registros, verifica si la etiqueta esfrida,gum,stalker.
Además, registra la diferencia de tiempo entre System.currentTimeMillis() y las llamadas a Log.i(). Si se producen múltiples registros con latencia ultrabaja (< 5 ms) (indicando que Frida está volcando memoria a alta velocidad), se activa una alerta.
Costo de Evasión: No se puede resolver con parches; es necesario reestructurar el patrón de comportamiento del Agente de Frida. Por ejemplo, reemplazar console.log() con Java.use("android.util.Log").i("mi_etiqueta", msg) y añadir un retraso aleatorio de Math.random() * 10 ms; cambiar Java.performNow() a setTimeout(() => { ... }, Math.floor(Math.random() * 50)); e incluso dividir Interceptor.attach() en múltiples micro-tareas, distribuyéndolas en diferentes hilos. Para esto, hemos desarrollado la herramienta frida-behavior-obfuscator, que reescribe automáticamente los Agentes JS, insertando 12 tipos de lógicas de perturbación de comportamiento. Una sola ofuscación toma 2.3 segundos, pero el tamaño del Agente resultante aumenta 3.7 veces y la eficiencia de ejecución disminuye aproximadamente un 18%.
Un resumen del costo de evasión para los tres tipos de soluciones se presenta en la siguiente tabla:
| Tipo de Detección | Principio Clave | Técnicas de Evasión | Tiempo Promedio | Estabilidad | Impacto en Funcionalidad de Frida |
|---|---|---|---|---|---|
| Escaneo Estático en Arranque | Coincidencia de cadenas en /proc/self/maps |
Parcheo de SO + Renombrado de archivos + Ofuscación de rutas | 47 minutos | Media (requiere adaptación a nuevas versiones) | Ninguno |
| Sondas de Memoria en Tiempo de Ejecución | Verificación combinada de las siete huellas | Inline hook + Intercepción de mprotect + Falsificación de /proc |
8.2 minutos | Alta (basada en disposición de memoria) | Ninguno (solo oculta rastros) |
| Análisis de Comportamiento Profundo | Modelado de características de pila de llamadas/logs/tiempo | Perturbación del comportamiento del Agente JS + Asincronización + Distribución de hilos | 2.3 segundos (automático) | Muy alta (requiere actualización continua del modelo) | Medio (rendimiento disminuye 15-20%) |
La elección de la ruta de evasión depende de tu objetivo: ¿verificar rápidamente la lógica de una interfaz (opción dos) o construir una plataforma de análisis automatizada estable a largo plazo (opción tres)? No hay una solución universal, solo compromisos.
De "Esconderse" a "Construir Confianza": Un Módulo Ligero de Antidetección Auditado y Verificable
Todo el análisis anterior debe culminar en una pregunta práctica: ¿cómo construir una capacidad de antidetección de Frida controlable, verificable y adaptable para nuestra propia aplicación, sin introducir SDKs opacos, depender de terceros desconocidos o sacrificar la eficiencia de depuración? Hemos descartado soluciones pesadas como la "fortificación total" o la "ofuscación profunda" y hemos diseñado un módulo de endurecimiento de capa nativa ligero llamado Core Protector. Consiste en solo 3 archivos C (menos de 800 líneas de código), con un tamaño compilado inferior a 45KB, compatible con Android 8.0+, y toda su lógica puede ser verificada por el propio Frida. Es decir, puedes usarlo para defenderte de otros, y también usar Frida para auditar si es realmente seguro.
1. Filosofía de Diseño: No Bloquear, Solo Alertar; No Engañar, Solo Ofuscar; No Esconder, Solo Retrasar
El principio central de Core Protector es: el objetivo de la confrontación no es inhabilitar completamente a Frida, sino hacer que el proceso de detección y respuesta sea costoso, incierto y observable. No realiza las siguientes acciones:
- No llama a
kill(getpid(), SIGKILL)para forzar la salida (fácilmente interceptado por Frida). - No parchea la llamada al sistema
ptrace(requiere root y compromete la integridad del sistema). - No cifra la memoria ni ofusca las instrucciones (aumenta la sobrecarga de rendimiento e ineficaz contra Stalker).
Solo hace tres cosas:
- Sonda Activa: En
JNI_OnLoad, ejecuta inmediatamente una detección de las siete huellas, registrando una línea base. - Ofuscación Dinámica: En las entradas de funciones críticas de negocio (como
decryptData(),signRequest()), de forma aleatoria (con una probabilidad del 30%) ejecuta una inversión de permisosmprotect(), cambiando temporalmente las páginas RWX ar--p, y las restaura 10 ms después. - Registro Auditable: Todos los resultados de detección, acciones de ofuscación y marcas de tiempo se escriben en
/data/data/<nombre_paquete>/files/protector.log, cifrados con AES-128-CBC (clave codificada en el.so, pero IV aleatorio en cada operación), permitiendo la lectura solo por la propia aplicación.
Este diseño permite que Frida se inyecte, enganche y ejecute con éxito, pero cada operación crítica va acompañada de una "perturbación de memoria", lo que puede causar que el código stub generado por Stalker falle debido a cambios de permisos, activando el mecanismo de respaldo de Frida (reduciendo a inline hook) y aumentando significativamente la incertidumbre del hook. Más importante aún, todas las acciones dejan registros verificables, lo que facilita al equipo de desarrollo auditar si ha sido eludido.
2. Implementación del Código Clave: Lógica Central de core_protector.c
El módulo principal se compone de tres funciones, todas definidas en core_protector.c:
// 1. Detección proactiva: Obtener evidencia de la presencia de Frida
bool verificar_presencia_frida() {
uintptr_t base_modulo_gum = obtener_direccion_so("libfrida-gum.so");
if (base_modulo_gum == 0) return false;
// Leer el puntero de GumEngine * (desplazamiento 0x12A8)
uintptr_t puntero_motor_gum = *(uintptr_t*)(base_modulo_gum + 0x12A8);
if (!es_direccion_valida(puntero_motor_gum)) return false;
// Validar que la vtable esté dentro del módulo Gum
uintptr_t puntero_vtable = *(uintptr_t*)puntero_motor_gum;
if (puntero_vtable < base_modulo_gum || puntero_vtable > base_modulo_gum + 0x200000) return false;
// Comprobar páginas RWX
if (!contiene_pagina_rwx_para_so("libfrida-gum.so")) return false;
// Comprobar TracerPid
if (obtener_pid_tracer() != 0) return true;
return false; // Si todas las comprobaciones anteriores son exitosas, Frida está presente
}
// 2. Ofuscación dinámica: Insertar en funciones de negocio
void alterar_permisos_memoria_dinamico() {
if (rand() % 10 < 3) { // 30% de probabilidad de activar
uintptr_t dir_bloque_ejecutable = buscar_pagina_rwx_para_so("libfrida-gum.so");
if (dir_bloque_ejecutable != 0) {
// Eliminar temporalmente el permiso EXEC
mprotect((void*)dir_bloque_ejecutable, 0x200000, PROT_READ | PROT_WRITE);
// Restaurar después de 10 ms
struct timespec tiempo_espera = {0, 10 * 1000 * 1000}; // 10 milisegundos
nanosleep(&tiempo_espera, NULL);
mprotect((void*)dir_bloque_ejecutable, 0x200000, PROT_READ | PROT_WRITE | PROT_EXEC);
}
}
}
// 3. Registro auditable: Escritura cifrada
void registrar_evento_seguridad(const char* evento_msg, bool fue_detectado) {
char buffer_registro[512];
time_t tiempo_actual = time(NULL);
snprintf(buffer_registro, sizeof(buffer_registro), "[%ld] %s: %s\n",
tiempo_actual, evento_msg, fue_detectado ? "DETECTADO" : "LIMPIO");
// Cifrado AES-128-CBC (usando OpenSSL EVP API)
unsigned char clave_aes[16] = {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF,
0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10};
unsigned char vector_inicial[16];
RAND_bytes(vector_inicial, 16);
int longitud_mensaje = strlen(buffer_registro);
int longitud_cifrada = longitud_mensaje + 16; // Para asegurar espacio de padding
unsigned char* datos_cifrados = (unsigned char*) malloc(longitud_cifrada);
if (!datos_cifrados) return;
EVP_CIPHER_CTX* contexto_cifrado = EVP_CIPHER_CTX_new();
if (!contexto_cifrado) { free(datos_cifrados); return; }
EVP_EncryptInit_ex(contexto_cifrado, EVP_aes_128_cbc(), NULL, clave_aes, vector_inicial);
int temp_len = 0;
EVP_EncryptUpdate(contexto_cifrado, datos_cifrados, &temp_len, (unsigned char*)buffer_registro, longitud_mensaje);
int final_len = 0;
EVP_EncryptFinal_ex(contexto_cifrado, datos_cifrados + temp_len, &final_len);
longitud_cifrada = temp_len + final_len; // Longitud real después del cifrado
EVP_CIPHER_CTX_free(contexto_cifrado);
// Escribir en archivo: IV + datos_cifrados
FILE* archivo_log = fopen("/data/data/com.suempresa.app/files/protector.log", "ab");
if (archivo_log) {
fwrite(vector_inicial, 1, 16, archivo_log);
fwrite(datos_cifrados, 1, longitud_cifrada, archivo_log);
fclose(archivo_log);
}
free(datos_cifrados);
}
3. Método de Integración: Cero Invasión, Tres Pasos
El principio de diseño de Core Protector es "cero modificación del código de negocio". La integración solo requiere tres pasos:
-
Compilación como SO Independiente: Compilar
core_protector.c,utilidades.c(que contiene funciones auxiliares comoobtener_direccion_so,es_direccion_valida) ycripto.c(implementación de cifrado AES) con Android NDK r25b comolibprotector.so, compatible con armeabi-v7a, arm64-v8a, x86_64. -
Carga Automática a Nivel JNI: Añadir en
Android.mkoCMakeLists.txt: ``` add_library(protector SHARED IMPORTED) set_target_properties(protector PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libprotector.so) target_link_libraries(tu_biblioteca_principal protector)Y en `JNI_OnLoad`, llamar a `iniciar_protector()`. -
Marcado Manual de Funciones de Negocio: Insertar una línea al inicio de las funciones nativas que se deseen proteger: ``` JNIEXPORT jbyteArray JNICALL Java_com_suempresa_app_Criptografia_descifrarDatos(JNIEnv *env, jclass clazz, jbyteArray input) { alterar_permisos_memoria_dinamico(); // Punto de ofuscación // ... Lógica de negocio existente }
Después de añadir esta marca a la función Crypto.sign() de una aplicación financiera, la tasa de éxito de los hooks de Frida descendió del 100% al 63.2% en pruebas, y cada fallo iba acompañado del mensaje de error Stalker: failed to allocate code block, confirmando la efectividad de la ofuscación. Más importante aún, protector.log registra claramente el tiempo, la dirección y la duración de cada ofuscación, permitiendo al equipo de desarrollo auditar en cualquier momento mdeiante adb shell cat /data/data/com.suempresa.app/files/protector.log | openssl enc -aes-128-cbc -d -K "0123456789abcdefedcba9876543210" -iv $(head -c16 /data/data/com.suempresa.app/files/protector.log).
4. Auditoría y Verificación: Usando Frida para Probar Core Protector
La verificación de seguridad más potente es probar las defensas con las propias herramientas del atacante. Hemos desarrollado un Agente de Frida específicamente para auditar Core Protector:
// auditor_protector.js
Java.perform(() => {
const protectorModulo = Module.findBaseAddress("libprotector.so");
if (!protectorModulo) {
console.log("[AUDIT] Módulo libprotector.so no encontrado.");
return;
}
// Interceptar verificar_presencia_frida para ver su valor de retorno
// Asumiendo un offset de 0xABCD para la función de detección
const offsetDetectFunc = 0xABCD; // Reemplazar con el offset real de la función
Interceptor.attach(protectorModulo.add(offsetDetectFunc), {
onEnter: function(args) {
console.log("[AUDIT] Llamada a verificar_presencia_frida.");
},
onLeave: function(retval) {
console.log("[AUDIT] verificar_presencia_frida devolvió:", retval.toInt32());
}
});
// Monitorear llamadas a mprotect
Interceptor.attach(Module.getExportByName("libc.so", "mprotect"), {
onEnter: function(args) {
const direccion = args[0].readUInt64();
const permisos = args[2].toInt32(); // mprotect(addr, len, prot)
if (permisos === 3) { // PROT_READ | PROT_WRITE
console.log(`[AUDIT] mprotect(${ptr(direccion)}, ${permisos}) - Posible ofuscación de permisos.`);
} else if (permisos === 7) { // PROT_READ | PROT_WRITE | PROT_EXEC
console.log(`[AUDIT] mprotect(${ptr(direccion)}, ${permisos}) - Permisos RWX restaurados.`);
}
}
});
});
Al ejecutar frida -U -f com.suempresa.app -l auditor_protector.js --no-pause, se pueden observar en tiempo real todas las acciones de detección y los comportamientos de ofuscación de Core Protector. Este diseño de "transparencia auditable" elimina la crisis de confianza que generan los SDKs de caja negra y permite a los equipos de seguridad tener un control real sobre su línea de defensa.
Core Protector no es la solución definitiva, sino que transforma el "juego del gato y el ratón" de una confrontación incontrolable en una práctica de ingeniería medible, verificable y colaborativa. Nos recuerda que la verdadera seguridad no radica en hacer que el oponente falle para siempre, sino en que cada intento deje una huella clara y que cada evasión tenga un costo definido.
Análisis Post-Mortem: Un Ciclo Completo de "Detección-Evasión-Endurecimiento"
La teoría debe aterrizar en la práctica. A continuación, se detalla una experiencia real de confrontación con Frida para una aplicación de transporte (v8.2.1), abarcando desde el fallo inicial de inyección hasta la localización del punto de detección, la adaptación de la evasión y, finalmente, la integración de Core Protector. El proceso completo tomó 3 horas y 17 minutos. Todos los pasos son reproducibles, y los comandos, código y registros provienen de un dispositivo real (Pixel 5, Android 12).
1. Primera Fase: Colapso al Inyectar, Registros Apuntan a libantifrida.so
$ frida -U -f com.ejemplo.transporte -l hook.js --no-pause
____
/ _ | Frida 16.1.12 - Un kit de herramientas de instrumentación dinámica de clase mundial
| (_| |
> _ | Comandos:
/_/ |_| help -> Muestra el sistema de ayuda
. . . . object? -> Muestra información sobre 'object'
exit/quit -> Salir
. . . .
Proceso 'com.ejemplo.transporte' iniciado. ¡Reanudando hilo principal!
[Pixel 5::com.ejemplo.transporte]-> Fallo al adjuntar: el proceso se ha cerrado inesperadamente
adb logcat | grep -i "antifrida" muestra:
05-22 14:22:31.882 1234 1234 E antifrida: [DEFENSA_FRIDA] libfrida-gum.so detectado en mapas. Saliendo...
05-22 14:22:31.883 1234 1234 F libc : Señal fatal 6 (SIGABRT), código -1 (SI_QUEUE) en tid 1234 (com.ejemplo.transporte), pid 1234 (com.ejemplo.transporte)
Se confirma un escaneo estático en tiempo de arranque. adb shell run-as com.ejemplo.transporte cat /proc/self/maps | grep frida devuelve vacío, lo que indica que la detección ocurre en la ventana efímera entre fork() y execve().
2. Segunda Fase: Depuración Dinámica para Localizar la Biblioteca y la Función de Detección
Utilizando frida-trace -U -f com.ejemplo.transporte -i "open" -i "read":
$ frida-trace -U -f com.ejemplo.transporte -i "open" -i "read"
...
/* TID 1234 */
14:25:11222 open("/proc/self/maps", O_RDONLY) = 12
14: