AvanzadoFridahookinganálisis dinámicoAndroidingeniería inversaSSL pinning

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.

MalwareIntel Research··13 min lectura
Serie: Malware Móvil — Parte 9

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

ComponenteDetalle
Dispositivo AndroidRooteado (Magisk recomendado). Emuladores como Genymotion también funcionan
Python3.7+ en el host (PC/Mac)
ADBAndroid Debug Bridge instalado y funcionando
Frida toolsfrida-tools (CLI) y frida (bindings Python)
frida-serverBinario 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:

ObjetoFunció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

AspectoFridaXposed Framework
PlataformasAndroid, iOS, Windows, macOS, LinuxSolo Android
Lenguaje de scriptsJavaScriptJava
Instalaciónfrida-server en /data/local/tmp/Módulo de sistema (requiere reinicio)
PersistenciaNo persiste tras cerrar el procesoPersiste (módulo cargado al boot)
Root requeridoSí (o Gadget mode sin root)
PerformanceImpacto mínimo por hookImpacto mínimo
Hot reloadSí (modificar script sin reiniciar)No (reinstalar módulo + reiniciar)
Hooks nativosSí (Interceptor para C/C++)Limitado (necesita bridges)
ComunidadAmplia, activa, documentación extensaMadura pero menos activa
Uso principalAnálisis de seguridad, reversing, pentestingModificación de apps, personalización
DetecciónDetectable pero bypasseableDetectable (modifica framework)
iOSNo

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

Preguntas frecuentes

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.