Frida y Hooking: Análisis Dinámico Avanzado de Apps Móviles
Guía avanzada de análisis dinámico con Frida: setup en Android rooteado, JavaScript API, hooking de métodos Java y funciones nativas, bypass de SSL pinning, bypass de anti-debug, framework Objection, ejemplos prácticos y comparación con Xposed.
Qué es Frida
Frida es un toolkit de instrumentación dinámica desarrollado por Ole André Vadla Ravnås (NowSecure). Permite inyectar código JavaScript en procesos nativos en cualquier plataforma: Android, iOS, Windows, macOS, Linux. En el contexto de análisis de malware móvil, Frida es la herramienta estándar de la industria para entender qué hace una app en tiempo real sin necesidad de desensamblarla por completo.
La potencia de Frida reside en su capacidad para:
- Interceptar cualquier función (Java, Kotlin, C/C++ nativo) mientras se ejecuta
- Leer y modificar argumentos y valores de retorno en tiempo real
- Inyectar código propio dentro del proceso objetivo
- Trazar llamadas a APIs del sistema (criptografía, red, sistema de archivos)
- Todo esto con scripts JavaScript que se pueden modificar y recargar en caliente
Para el análisis de malware, esto significa poder observar exactamente qué datos envía un troyano a su C2, qué claves de cifrado usa, qué información roba del dispositivo y cómo evade las protecciones del sistema, todo sin modificar el APK original.
Setup: entorno de análisis con Frida
Requisitos
| Componente | Detalle |
|---|---|
| Dispositivo Android | Rooteado (Magisk recomendado). Emuladores como Genymotion también funcionan |
| Python | 3.7+ en el host (PC/Mac) |
| ADB | Android Debug Bridge instalado y funcionando |
| Frida tools | frida-tools (CLI) y frida (bindings Python) |
| frida-server | Binario para la arquitectura del dispositivo (arm, arm64, x86, x86_64) |
Instalación paso a paso
# 1. Instalar Frida en el host
pip install frida-tools frida
# 2. Verificar versión
frida --version
# Debe coincidir con la versión de frida-server
# 3. Descargar frida-server para la arquitectura del dispositivo
# Verificar arquitectura del dispositivo:
adb shell getprop ro.product.cpu.abi
# Resultado típico: arm64-v8a
# 4. Descargar desde: https://github.com/frida/frida/releases
# Ejemplo para arm64:
wget https://github.com/frida/frida/releases/download/16.x.x/frida-server-16.x.x-android-arm64.xz
unxz frida-server-16.x.x-android-arm64.xz
# 5. Subir frida-server al dispositivo
adb push frida-server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server
# 6. Ejecutar frida-server como root
adb shell su -c /data/local/tmp/frida-server &
# 7. Verificar que funciona
frida-ps -U # Lista procesos del dispositivo USB
Alternativa sin root: Frida Gadget
Para casos donde no se puede rootear el dispositivo (testing en dispositivos corporativos), se puede empaquetar Frida Gadget dentro del APK:
# 1. Descompilar el APK con apktool
apktool d target.apk -o target_decompiled
# 2. Copiar frida-gadget.so al directorio de libs nativas
cp frida-gadget-16.x.x-android-arm64.so \
target_decompiled/lib/arm64-v8a/libfrida-gadget.so
# 3. Modificar la actividad principal para cargar la librería
# En smali, añadir al método onCreate:
# const-string v0, "frida-gadget"
# invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
# 4. Recompilar y firmar
apktool b target_decompiled -o target_patched.apk
jarsigner -keystore debug.keystore target_patched.apk debug
Limitación: esta técnica modifica el APK, lo que puede ser detectado por verificaciones de integridad (SafetyNet, Play Integrity API, checksums internos).
JavaScript API de Frida
Conceptos fundamentales
Frida expone una API JavaScript que se ejecuta dentro del proceso objetivo. Los objetos principales:
| Objeto | Función |
|---|---|
Java.perform() | Wrapper para ejecutar código en el contexto del runtime de Java/ART |
Java.use() | Obtiene una referencia a una clase Java por su nombre completo |
Interceptor.attach() | Hook sobre funciones nativas (C/C++) |
Module.findExportByName() | Busca funciones exportadas en bibliotecas nativas |
Memory.read*() / Memory.write*() | Lectura/escritura directa de memoria |
send() / recv() | Comunicación entre el script y el host |
Estructura básica de un script Frida
// script.js - Template básico para hooking
'use strict';
// El callback se ejecuta cuando el runtime de Java está listo
Java.perform(function() {
console.log("[*] Script cargado en proceso: " + Process.id);
// Obtener referencia a una clase Java
var TargetClass = Java.use("com.example.malware.CryptoHelper");
// Hook del método 'encrypt'
TargetClass.encrypt.implementation = function(plaintext, key) {
// Leer los argumentos originales
console.log("[+] encrypt() llamado");
console.log(" plaintext: " + plaintext);
console.log(" key: " + key);
// Llamar al método original
var result = this.encrypt(plaintext, key);
// Leer el resultado
console.log(" resultado: " + result);
// Devolver el resultado sin modificar
return result;
};
});
# Ejecutar el script contra una app en ejecución
frida -U -l script.js com.example.malware
# O adjuntarse por PID
frida -U -l script.js -p 12345
# Spawn mode: lanzar la app e inyectar antes de que ejecute
frida -U -l script.js -f com.example.malware --no-pause
Hooking de métodos Java
Hooking básico: interceptar un método
Java.perform(function() {
// Hook de un método con argumentos tipados
var MainActivity = Java.use("com.malware.app.MainActivity");
MainActivity.sendData.overload('java.lang.String', 'int')
.implementation = function(url, port) {
console.log("[C2] Conexión a: " + url + ":" + port);
// Podemos modificar los argumentos
// return this.sendData("benign-server.com", 443);
// O dejar pasar la llamada original
return this.sendData(url, port);
};
});
Hooking de constructores
Java.perform(function() {
var HttpRequest = Java.use("com.malware.net.HttpRequest");
// Hook del constructor
HttpRequest.$init.overload('java.lang.String', 'java.util.Map')
.implementation = function(url, headers) {
console.log("[HTTP] Nueva petición a: " + url);
// Iterar sobre los headers
var iterator = headers.entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
console.log(" " + entry.getKey() + ": " + entry.getValue());
}
return this.$init(url, headers);
};
});
Hooking de métodos sobrecargados
Cuando una clase tiene múltiples métodos con el mismo nombre pero diferentes firmas (overloading):
Java.perform(function() {
var Crypto = Java.use("com.malware.crypto.AESHelper");
// Listar todas las sobrecargas de un método
var overloads = Crypto.decrypt.overloads;
console.log("[*] decrypt tiene " + overloads.length + " sobrecargas");
// Hook de todas las sobrecargas
overloads.forEach(function(overload) {
overload.implementation = function() {
console.log("[AES] decrypt llamado con " + arguments.length + " args");
for (var i = 0; i < arguments.length; i++) {
console.log(" arg[" + i + "]: " + arguments[i]);
}
return this.decrypt.apply(this, arguments);
};
});
});
Enumeración de clases cargadas
Útil cuando no se conoce la estructura interna del malware:
Java.perform(function() {
// Enumerar todas las clases cargadas que contengan "malware" en el nombre
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.includes("crypto") || className.includes("Cipher")) {
console.log("[CLASS] " + className);
}
},
onComplete: function() {
console.log("[*] Enumeración completada");
}
});
});
Hooking de funciones nativas
Para malware que usa código nativo (C/C++ en bibliotecas .so), Frida proporciona Interceptor:
// Hook de una función nativa exportada
var openPtr = Module.findExportByName("libc.so", "open");
Interceptor.attach(openPtr, {
onEnter: function(args) {
// args[0] es el path del archivo (char*)
var path = Memory.readUtf8String(args[0]);
if (path.includes("/proc/") || path.includes("/data/")) {
console.log("[open] " + path);
}
},
onLeave: function(retval) {
// retval es el file descriptor
}
});
// Hook de función de envío de red
var sendPtr = Module.findExportByName("libc.so", "send");
Interceptor.attach(sendPtr, {
onEnter: function(args) {
var fd = args[0].toInt32();
var buf = args[1];
var len = args[2].toInt32();
if (len > 0) {
var data = Memory.readByteArray(buf, Math.min(len, 256));
console.log("[send] fd=" + fd + " len=" + len);
console.log(hexdump(data, { header: false }));
}
}
});
Hook de funciones dentro de un .so del malware
// Encontrar la base del módulo
var malwareModule = Process.findModuleByName("libmalware.so");
if (malwareModule) {
console.log("[*] libmalware.so base: " + malwareModule.base);
// Hook por offset (conocido por análisis estático previo con Ghidra/IDA)
var decryptFunc = malwareModule.base.add(0x1234);
Interceptor.attach(decryptFunc, {
onEnter: function(args) {
console.log("[decrypt_native] input: " +
Memory.readByteArray(args[0], args[1].toInt32()));
this.outBuf = args[2];
},
onLeave: function(retval) {
console.log("[decrypt_native] output: " +
Memory.readByteArray(this.outBuf, 64));
}
});
}
SSL pinning bypass
El SSL pinning es una de las protecciones más comunes en apps bancarias y malware sofisticado. Impide interceptar el tráfico HTTPS con un proxy, incluso si se instala un certificado CA raíz en el dispositivo.
Script universal de bypass
// ssl_pinning_bypass.js
// Cubre las implementaciones más comunes
Java.perform(function() {
console.log("[*] Iniciando bypass de SSL pinning...");
// 1. TrustManager vacío (cubre implementaciones custom)
var TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var TrustManagerImpl = Java.registerClass({
name: "com.frida.TrustManager",
implements: [TrustManager],
methods: {
checkClientTrusted: function(chain, authType) { },
checkServerTrusted: function(chain, authType) { },
getAcceptedIssuers: function() { return []; }
}
});
// 2. OkHttp3 CertificatePinner
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
CertificatePinner.check.overload('java.lang.String', 'java.util.List')
.implementation = function(hostname, peerCertificates) {
console.log("[+] OkHttp3 pinning bypass: " + hostname);
return;
};
} catch(e) {
console.log("[-] OkHttp3 no encontrado");
}
// 3. WebViewClient (para WebViews)
try {
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
console.log("[+] WebView SSL error bypass");
handler.proceed();
};
} catch(e) { }
// 4. Network Security Config (Android 7+)
try {
var NetworkSecurityConfig = Java.use(
"android.security.net.config.NetworkSecurityTrustManager"
);
NetworkSecurityConfig.checkServerTrusted.overload(
'[Ljava.security.cert.X509Certificate;', 'java.lang.String'
).implementation = function(certs, authType) {
console.log("[+] NetworkSecurityConfig bypass");
return;
};
} catch(e) { }
console.log("[*] SSL pinning bypass activo");
});
Con este bypass activo, el tráfico del malware se puede interceptar con un proxy como Burp Suite o mitmproxy, revelando las URLs del C2, los datos exfiltrados y el protocolo de comunicación.
Anti-debug bypass
El malware móvil avanzado implementa múltiples técnicas anti-análisis. Frida permite desactivarlas una por una:
Bypass de detección de root
Java.perform(function() {
// Interceptar verificaciones comunes de root
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
RootBeer.isRooted.implementation = function() {
console.log("[+] RootBeer.isRooted() -> false");
return false;
};
// Ocultar archivos de root
var File = Java.use("java.io.File");
File.exists.implementation = function() {
var path = this.getAbsolutePath();
var rootPaths = ["/system/bin/su", "/system/xbin/su",
"/sbin/su", "/data/local/bin/su",
"/system/app/Superuser.apk",
"/system/app/SuperSU"];
if (rootPaths.indexOf(path) !== -1) {
console.log("[+] Ocultando archivo root: " + path);
return false;
}
return this.exists();
};
// Ocultar propiedades del sistema
var SystemProperties = Java.use("android.os.SystemProperties");
SystemProperties.get.overload('java.lang.String', 'java.lang.String')
.implementation = function(key, def) {
if (key === "ro.debuggable" || key === "ro.secure") {
return "1";
}
return this.get(key, def);
};
});
Bypass de detección de Frida
// El malware puede detectar Frida buscando el puerto 27042
// o strings en memoria. Contrarrestar:
Java.perform(function() {
// Ocultar puerto de frida-server
var Socket = Java.use("java.net.Socket");
Socket.$init.overload('java.lang.String', 'int')
.implementation = function(host, port) {
if (port === 27042) {
console.log("[+] Bloqueando detección de Frida via socket");
throw Java.use("java.net.ConnectException")
.$new("Connection refused");
}
return this.$init(host, port);
};
});
// A nivel nativo: ocultar frida en /proc/self/maps
var openPtr = Module.findExportByName("libc.so", "open");
Interceptor.attach(openPtr, {
onEnter: function(args) {
this.path = Memory.readUtf8String(args[0]);
},
onLeave: function(retval) {
if (this.path && this.path.includes("/proc/self/maps")) {
// Reemplazar el file descriptor con uno filtrado
// (implementación completa requiere interceptar read() también)
}
}
});
Bypass de SafetyNet / Play Integrity
Java.perform(function() {
// Para apps que verifican SafetyNet
try {
var SafetyNet = Java.use(
"com.google.android.gms.safetynet.SafetyNetClient"
);
SafetyNet.attest.implementation = function(nonce, apiKey) {
console.log("[+] SafetyNet.attest() interceptado");
// El resultado real se modifica en el callback
return this.attest(nonce, apiKey);
};
} catch(e) {
console.log("[-] SafetyNet API no disponible");
}
});
Objection framework
Objection es un framework construido sobre Frida que automatiza tareas comunes de análisis de seguridad móvil:
# Instalación
pip install objection
# Conectar a una app en ejecución
objection -g com.example.malware explore
# Comandos dentro de Objection:
# Listar actividades
android hooking list activities
# Listar servicios
android hooking list services
# Listar clases cargadas con filtro
android hooking list classes com.malware
# Listar métodos de una clase
android hooking list class_methods com.malware.CryptoHelper
# Hookear un método
android hooking watch class_method \
com.malware.CryptoHelper.encrypt \
--dump-args --dump-return
# Bypass SSL pinning (un comando)
android sslpinning disable
# Bypass root detection
android root disable
# Dump del keystore
android keystore list
# Explorar el filesystem de la app
env
ls
file download /data/data/com.example.malware/shared_prefs/config.xml
Objection es particularmente útil para análisis exploratorio rápido: con unos pocos comandos se puede desactivar SSL pinning, root detection, y comenzar a observar el comportamiento de la app.
Ejemplos prácticos de análisis de malware
Ejemplo 1: extraer claves de cifrado AES
Java.perform(function() {
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
SecretKeySpec.$init.overload('[B', 'java.lang.String')
.implementation = function(keyBytes, algorithm) {
console.log("[CRYPTO] Algoritmo: " + algorithm);
console.log("[CRYPTO] Key: " +
bytesToHex(keyBytes));
return this.$init(keyBytes, algorithm);
};
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload('[B').implementation = function(input) {
console.log("[CIPHER] Input (" + input.length + " bytes): " +
bytesToHex(input.slice(0, Math.min(input.length, 32))));
var result = this.doFinal(input);
console.log("[CIPHER] Output (" + result.length + " bytes): " +
bytesToHex(result.slice(0, Math.min(result.length, 32))));
return result;
};
function bytesToHex(bytes) {
var hex = [];
for (var i = 0; i < bytes.length; i++) {
hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
}
return hex.join('');
}
});
Ejemplo 2: interceptar comunicaciones C2
Java.perform(function() {
// Hook HttpURLConnection
var URL = Java.use("java.net.URL");
var HttpURLConnection = Java.use("java.net.HttpURLConnection");
URL.$init.overload('java.lang.String').implementation = function(url) {
if (!url.includes("googleapis.com") &&
!url.includes("google.com")) {
console.log("[URL] " + url);
}
return this.$init(url);
};
// Hook OutputStreamWriter (datos enviados al C2)
var OutputStreamWriter = Java.use("java.io.OutputStreamWriter");
OutputStreamWriter.write.overload('java.lang.String')
.implementation = function(data) {
console.log("[C2 SEND] " + data.substring(
0, Math.min(data.length(), 500)));
return this.write(data);
};
// Hook BufferedReader (datos recibidos del C2)
var BufferedReader = Java.use("java.io.BufferedReader");
BufferedReader.readLine.implementation = function() {
var line = this.readLine();
if (line !== null) {
console.log("[C2 RECV] " + line.substring(
0, Math.min(line.length(), 500)));
}
return line;
};
});
Ejemplo 3: monitorizar acceso a datos sensibles
Java.perform(function() {
// Detectar lectura de SMS
var SmsManager = Java.use("android.telephony.SmsManager");
if (SmsManager.getDefault) {
console.log("[*] Monitorizando acceso a SMS");
}
// Detectar lectura de contactos
var ContentResolver = Java.use("android.content.ContentResolver");
ContentResolver.query.overload(
'android.net.Uri', '[Ljava.lang.String;',
'java.lang.String', '[Ljava.lang.String;', 'java.lang.String'
).implementation = function(uri, projection, selection, selArgs, sortOrder) {
var uriStr = uri.toString();
if (uriStr.includes("contacts") || uriStr.includes("sms") ||
uriStr.includes("call_log")) {
console.log("[DATA ACCESS] Query a: " + uriStr);
}
return this.query(uri, projection, selection, selArgs, sortOrder);
};
// Detectar acceso a ubicación
var LocationManager = Java.use("android.location.LocationManager");
LocationManager.getLastKnownLocation.implementation = function(provider) {
console.log("[LOCATION] getLastKnownLocation(" + provider + ")");
var loc = this.getLastKnownLocation(provider);
if (loc !== null) {
console.log(" Lat: " + loc.getLatitude() +
" Lon: " + loc.getLongitude());
}
return loc;
};
});
Frida vs Xposed: comparación
| Aspecto | Frida | Xposed Framework |
|---|---|---|
| Plataformas | Android, iOS, Windows, macOS, Linux | Solo Android |
| Lenguaje de scripts | JavaScript | Java |
| Instalación | frida-server en /data/local/tmp/ | Módulo de sistema (requiere reinicio) |
| Persistencia | No persiste tras cerrar el proceso | Persiste (módulo cargado al boot) |
| Root requerido | Sí (o Gadget mode sin root) | Sí |
| Performance | Impacto mínimo por hook | Impacto mínimo |
| Hot reload | Sí (modificar script sin reiniciar) | No (reinstalar módulo + reiniciar) |
| Hooks nativos | Sí (Interceptor para C/C++) | Limitado (necesita bridges) |
| Comunidad | Amplia, activa, documentación extensa | Madura pero menos activa |
| Uso principal | Análisis de seguridad, reversing, pentesting | Modificación de apps, personalización |
| Detección | Detectable pero bypasseable | Detectable (modifica framework) |
| iOS | Sí | No |
Veredicto para análisis de malware: Frida es la herramienta estándar. Su soporte multiplataforma, hot reload, capacidad de hooking nativo y la API JavaScript flexible lo hacen superior para análisis dinámico. Xposed tiene su nicho en modificaciones persistentes de apps (mods, tweaks), pero no compite con Frida en análisis de seguridad.
Recursos
- Frida Official Documentation: documentación oficial y tutoriales
- Frida CodeShare: repositorio de scripts comunitarios
- Objection Wiki: documentación del framework Objection
- OWASP MASTG: guía de testing de seguridad móvil (usa Frida extensivamente)
- Frida Android Examples: ejemplos oficiales
- r2frida: integración de Frida con radare2
- Medusa: framework de análisis basado en Frida
- House: inspector de funciones para análisis con Frida
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.