19 KiB
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
-
¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
-
Descubre The PEASS Family, nuestra colección de exclusivos NFTs
-
Obtén el oficial PEASS & HackTricks swag
-
Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
-
Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.
Información copiada de https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html** (puedes encontrar soluciones allí)**
Las aplicaciones de Android pueden contener bibliotecas nativas compiladas. Las bibliotecas nativas son código que el desarrollador escribió y luego compiló para una arquitectura de computadora específica. Con mayor frecuencia, esto significa código que está escrito en C o C++. Las razones benignas o legítimas por las que un desarrollador puede hacer esto son para operaciones matemáticamente intensivas o sensibles al tiempo, como bibliotecas de gráficos. Los desarrolladores de malware han comenzado a moverse hacia el código nativo porque la ingeniería inversa de binarios compilados tiende a ser un conjunto de habilidades menos común que el análisis de bytecode DEX. Esto se debe en gran parte a que el bytecode DEX se puede descompilar a Java, mientras que el código nativo compilado a menudo debe analizarse como ensamblaje.
Objetivo
El objetivo de esta sección no es enseñarte ensamblador (ASM) o cómo revertir el código compilado de manera más general, sino cómo aplicar las habilidades de ingeniería inversa de binarios más generales, específicamente a Android. Debido a que el objetivo de este taller no es enseñarte las arquitecturas ASM, todos los ejercicios incluirán una versión ARM y una versión x86 de la biblioteca que se analizará para que cada persona pueda elegir la arquitectura con la que se sienta más cómoda.
Aprendiendo ensamblador ARM
Si no tienes experiencia previa en ingeniería inversa de binarios/ensamblador, aquí hay algunos recursos sugeridos. La mayoría de los dispositivos Android funcionan con ARM, pero todos los ejercicios en este taller también incluyen una versión x86 de la biblioteca.
Para aprender y/o revisar el ensamblador ARM, sugiero encarecidamente los conceptos básicos del ensamblador ARM de Azeria Labs.
Introducción a la Interfaz Nativa de Java (JNI)
La Interfaz Nativa de Java (JNI) permite a los desarrolladores declarar métodos de Java que se implementan en código nativo (generalmente compilado en C/C++). La interfaz JNI no es específica de Android, pero está disponible de manera más general para aplicaciones de Java que se ejecutan en diferentes plataformas.
El Kit de Desarrollo Nativo de Android (NDK) es el conjunto de herramientas específico de Android en la parte superior de JNI. Según la documentación:
En Android, el Kit de Desarrollo Nativo (NDK) es un conjunto de herramientas que permite a los desarrolladores escribir código C y C++ para sus aplicaciones de Android.
Juntos, JNI y NDK permiten a los desarrolladores de Android implementar parte de la funcionalidad de su aplicación en código nativo. El código Java (o Kotlin) llamará a un método nativo declarado en Java que está implementado en la biblioteca nativa compilada.
Referencias
Documentación de Oracle JNI
- Especificación JNI
- Funciones JNI <– Siempre tengo esta abierta y me refiero a ella mientras revierto bibliotecas nativas de Android
Referencias de JNI y NDK de Android
- Consejos de JNI de Android <– Recomiendo leer la sección "Bibliotecas nativas" para empezar
- Introducción al NDK <– Esta es una guía para que los desarrolladores desarrollen bibliotecas nativas y entender cómo se construyen las cosas, lo que hace que sea más fácil revertir.
Objetivo de análisis - Bibliotecas nativas de Android
Para esta sección, nos enfocamos en cómo revertir la funcionalidad de la aplicación que se ha implementado en bibliotecas nativas de Android. Cuando decimos bibliotecas nativas de Android, ¿a qué nos referimos?
Las bibliotecas nativas de Android se incluyen en los APK como bibliotecas de objetos compartidos .so
, en el formato de archivo ELF. Si has analizado binarios de Linux anteriormente, es el mismo formato.
Estas bibliotecas por defecto se incluyen en el APK en la ruta de archivo /lib/<cpu>/lib<name>.so
. Esta es la ruta predeterminada, pero los desarrolladores también podrían elegir incluir la biblioteca nativa en /assets/<custom_name>
si así lo desean. Con mayor frecuencia, estamos viendo que los desarrolladores de malware eligen incluir bibliotecas nativas en rutas distintas a /lib
y usando diferentes extensiones de archivo para intentar "ocultar" la presencia de la biblioteca nativa.
Debido a que el código nativo se compila para CPUs específicas, si un desarrollador quiere que su aplicación se ejecute en más de un tipo de hardware, debe incluir cada una de esas versiones de la biblioteca nativa compilada en la aplicación. La ruta predeterminada mencionada anteriormente incluye un directorio para cada tipo de CPU oficialmente compatible con Android.
CPU | Ruta de la biblioteca nativa |
---|---|
ARM genérico de 32 bits | lib/armeabi/libcalc.so |
x86 | lib/x86/libcalc.so |
x64 | lib/x86_64/libcalc.so |
ARMv7 | lib/armeabi-v7a/libcalc.so |
ARM64 | lib/arm64-v8a/libcalc.so |
Cargando la biblioteca
Antes de que una aplicación de Android pueda llamar y ejecutar cualquier código que se implemente en una biblioteca nativa, la aplicación (código Java) debe cargar la biblioteca en memoria. Hay dos llamadas de API diferentes que harán esto:
System.loadLibrary("calc")
El siguiente contenido es de un libro de hacking sobre técnicas de hacking de plataformas en la nube, SaaS, CI/CD... El contenido siguiente es del archivo /hive/hacktricks/mobile-pentesting/android-app-pentesting/reversing-native-libraries.md. Traduzca el texto relevante al español y devuelva la traducción manteniendo la sintaxis de markdown. No traduzca cosas como código, nombres de técnicas de hacking, nombres de plataformas en la nube/SaaS (como Workspace, aws, gcp...), la palabra 'leak' y las etiquetas de markdown. Además, no agregue nada aparte de la traducción y la sintaxis de markdown.
System.load("lib/armeabi/libcalc.so")
La diferencia entre las dos llamadas de API es que loadLibrary
solo toma el nombre corto de la biblioteca como argumento (es decir, libcalc.so = "calc" y libinit.so = "init") y el sistema determinará correctamente la arquitectura en la que se está ejecutando y, por lo tanto, el archivo correcto a utilizar. Por otro lado, load
requiere la ruta completa de la biblioteca. Esto significa que el desarrollador de la aplicación debe determinar la arquitectura y, por lo tanto, el archivo de biblioteca correcto para cargar.
Cuando se llama a cualquiera de estos dos API (loadLibrary
o load
) desde el código Java, la biblioteca nativa que se pasa como argumento ejecuta su JNI_OnLoad
si se implementó en la biblioteca nativa.
Para reiterar, antes de ejecutar cualquier método nativo, la biblioteca nativa debe cargarse llamando a System.loadLibrary
o System.load
en el código Java. Cuando se ejecuta cualquiera de estas 2 API, también se ejecuta la función JNI_OnLoad
en la biblioteca nativa.
La conexión de código Java a código nativo
Para ejecutar una función de la biblioteca nativa, debe haber un método nativo declarado en Java que el código Java pueda llamar. Cuando se llama a este método nativo declarado en Java, se ejecuta la función "asociada" de la biblioteca nativa (ELF/.so).
Un método nativo declarado en Java aparece en el código Java como se muestra a continuación. Aparece como cualquier otro método de Java, excepto que incluye la palabra clave native
y no tiene código en su implementación, porque su código está en realidad en la biblioteca nativa compilada.
public native String doThingsInNativeLibrary(int var0);
Para llamar a este método nativo, el código Java lo llamaría como cualquier otro método Java. Sin embargo, en el backend, JNI y NDK ejecutarían en su lugar la función correspondiente en la biblioteca nativa. Para hacer esto, debe conocer la asociación entre un método nativo declarado en Java con una función en la biblioteca nativa.
Existen 2 formas diferentes de hacer esta asociación o enlace:
- Enlace dinámico utilizando la resolución de nombres de método nativo JNI, o
- Enlace estático utilizando la llamada de API
RegisterNatives
Enlace dinámico
Para enlazar o asociar dinámicamente el método nativo declarado en Java y la función en la biblioteca nativa, el desarrollador nombra el método y la función de acuerdo con las especificaciones para que el sistema JNI pueda hacer la vinculación dinámica.
Según las especificaciones, el desarrollador nombraría la función de la siguiente manera para que el sistema pueda vincular dinámicamente el método nativo y la función. Un nombre de método nativo se concatena a partir de los siguientes componentes:
- el prefijo Java_
- un nombre de clase completo mangleado
- un separador de guión bajo (“_”)
- un nombre de método mangleado
- para métodos nativos sobrecargados, dos guiones bajos (“__”) seguidos de la firma de argumento mangleada
Para hacer el enlace dinámico para el método nativo declarado en Java a continuación y digamos que está en la clase com.android.interesting.Stuff
public native String doThingsInNativeLibrary(int var0);
La función en la biblioteca nativa debería tener el nombre:
Java_com_android_interesting_Stuff_doThingsInNativeLibrary
Si no hay una función en la biblioteca nativa con ese nombre, eso significa que la aplicación debe estar haciendo enlace estático.
Enlace estático
Si el desarrollador no quiere o no puede nombrar las funciones nativas de acuerdo con la especificación (por ejemplo, quiere eliminar los símbolos de depuración), entonces debe usar el enlace estático con la API RegisterNatives
(doc) para hacer la asociación entre el método nativo declarado en Java y la función en la biblioteca nativa. La función RegisterNatives
se llama desde el código nativo, no desde el código Java, y se llama con mayor frecuencia en la función JNI_OnLoad
, ya que RegisterNatives
debe ejecutarse antes de llamar al método nativo declarado en Java.
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
Cuando se realiza ingeniería inversa, si la aplicación utiliza el método de enlace estático, nosotros como analistas podemos encontrar la estructura JNINativeMethod
que se está pasando a RegisterNatives
para determinar qué subrutina en la biblioteca nativa se ejecuta cuando se llama al método nativo declarado en Java.
La estructura JNINativeMethod
requiere una cadena con el nombre del método nativo declarado en Java y una cadena con la firma del método, por lo que deberíamos poder encontrar estas en nuestra biblioteca nativa.
Firma del Método
La estructura JNINativeMethod
requiere la firma del método. Una firma del método indica los tipos de los argumentos que toma el método y el tipo de lo que devuelve. Este enlace documenta Firmas de Tipo JNI en la sección "Firmas de Tipo".
- Z: booleano
- B: byte
- C: char
- S: short
- I: int
- J: long
- F: flotante
- D: doble
- L fully-qualified-class ; :clase completamente calificada
- [ type: type[]: tipo de matriz
- ( arg-types ) ret-type: tipo de método
- V: vacío
Para el método nativo.
public native String doThingsInNativeLibrary(int var0);
La firma de tipo es
(I)Ljava/lang/String;
Aquí hay otro ejemplo de un método nativo y su firma. Para lo siguiente es la declaración del método.
public native long f (int n, String s, int[] arr);
Tiene la firma de tipo:
(ILjava/lang/String;[I)J
Ejercicio #5 - Encontrar la dirección de la función nativa
En el Ejercicio #5 vamos a aprender a cargar bibliotecas nativas en un desensamblador e identificar la función nativa que se ejecuta cuando se llama a un método nativo. Para este ejercicio en particular, el objetivo no es ingeniería inversa del método nativo, solo encontrar el enlace entre la llamada al método nativo en Java y la función que se ejecuta en la biblioteca nativa. Para este ejercicio, usaremos la muestra Mediacode.apk. Esta muestra está disponible en ~/samples/Mediacode.apk
en la VM. Su hash SHA256 es a496b36cda66aaf24340941da8034bd53940d1b08d83a97f17a65ae144ebf91a.
Objetivo
El objetivo de este ejercicio es:
- Identificar los métodos nativos declarados en el bytecode DEX
- Determinar qué bibliotecas nativas se cargan (y, por lo tanto, dónde se pueden implementar los métodos nativos)
- Extraer la biblioteca nativa del APK
- Cargar la biblioteca nativa en un desensamblador
- Identificar la dirección (o nombre) de la función en la biblioteca nativa que se ejecuta cuando se llama al método nativo
Instrucciones
- Abra Mediacode.apk en jadx. Consulte el Ejercicio #1 para obtener más información.
- Esta vez, si expande la pestaña Recursos, verá que este APK tiene un directorio
lib/
. Las bibliotecas nativas para este APK están en las rutas de CPU predeterminadas. - Ahora necesitamos identificar cualquier método nativo declarado. En jadx, busque y liste todos los métodos nativos declarados. Debería haber dos.
- Alrededor del método nativo declarado, vea si hay algún lugar donde se cargue una biblioteca nativa. Esto proporcionará orientación sobre en qué biblioteca nativa buscar la función a implementar.
- Extraiga la biblioteca nativa del APK creando un nuevo directorio y copiando el APK en esa carpeta. Luego ejecute el comando
unzip Mediacode.APK
. Verá todos los archivos extraídos del APK, que incluyen el directoriolib/
. - Seleccione la arquitectura de la biblioteca nativa que desea analizar.
- Inicie ghidra ejecutando
ghidraRun
. Esto abrirá Ghidra. - Para abrir la biblioteca nativa para su análisis, seleccione "Nuevo proyecto", "Proyecto no compartido", seleccione una ruta para guardar el proyecto y asígnele un nombre. Esto crea un proyecto en el que puede cargar archivos binarios.
- Una vez que haya creado su proyecto, seleccione el icono del dragón para abrir el navegador de código. Vaya a "Archivo" > "Importar archivo" para cargar la biblioteca nativa en la herramienta. Puede dejar todos los valores predeterminados.
- Verá la siguiente pantalla. Seleccione "Analizar".
- Usando la información de enlace anterior, identifique la función en la biblioteca nativa que se ejecuta cuando se llama al método nativo declarado en Java.
Solución
Reversión de código de bibliotecas nativas de Android - JNIEnv
Al comenzar a realizar ingeniería inversa de bibliotecas nativas de Android, una de las cosas que no sabía que necesitaba saber era sobre JNIEnv
. JNIEnv
es una estructura de punteros de función a Funciones JNI. Cada función JNI en bibliotecas nativas de Android toma JNIEnv*
como primer argumento.
Desde la documentación de Android JNI Tips:
Las declaraciones C de JNIEnv y JavaVM son diferentes de las declaraciones C++. El archivo de inclusión "jni.h" proporciona diferentes typedefs dependiendo de si se incluye en C o C++. Por esta razón, es una mala idea incluir argumentos JNIEnv en archivos de encabezado incluidos por ambos idiomas. (Dicho de otra manera: si su archivo de encabezado requiere #ifdef __cplusplus, es posible que deba hacer un trabajo adicional