Una herramienta basada en Frida para rastrear el uso de la API JNI en las aplicaciones de Android.
Las bibliotecas nativas contenidas en las aplicaciones de Android a menudo utilizan la API JNI para utilizar el tiempo de ejecución de Android. El seguimiento de esas llamadas a través de la ingeniería inversa manual puede ser un proceso lento y doloroso. jnitrace funciona como una herramienta de rastreo de análisis dinámico similar a Frida-Trace o Strace, pero para el JNI.
La forma más fácil de correr con jnitrace es instalar usando PIP:
pip install jnitrace
Después de una instalación de PIP, es fácil ejecutar jnitrace :
jnitrace -l libnative-lib.so com.example.myapplication
jnitrace requiere un mínimo de dos parámetros para ejecutar un rastro:
-l libnative-lib.so -se usa para especificar las bibliotecas para rastrear. Este argumento se puede usar varias veces o * se puede usar para rastrear todas las bibliotecas. Por ejemplo, -l libnative-lib.so -l libanother-lib.so o -l * .com.example.myapplication : es el paquete de Android para rastrear. Este paquete ya debe estar instalado en el dispositivo.Los argumentos opcionales se enumeran a continuación:
-R <host>:<port> - se utiliza para especificar la ubicación de red del servidor frida remoto. Si A: no se especifica, LocalHost: 27042 es utilizado por DeAfull.-m <spawn|attach> - se utiliza para especificar el mecanismo de fijación de frida para usar. Se puede engendrar o colocar. Spawn es la opción predeterminada y recomendada.-b <fuzzy|accurate|none> - se usa para controlar la salida de la tracción posterior. Por defecto, jnitrace ejecutará el backtracer en modo accurate . Esta opción se puede cambiar a modo fuzzy o usarse para detener el ritmo de retroceso utilizando la opción none . Vea los documentos de Frida para obtener una explicación sobre las diferencias.-i <regex> - se usa para especificar los nombres de métodos que deben rastrearse. Esto puede ser útil para reducir el ruido en aplicaciones JNI particularmente grandes. La opción se puede suministrar varias veces. Por ejemplo, -i Get -i RegisterNatives incluiría solo métodos JNI que contienen Get o RegisterNitivos en su nombre.-e <regex> - se usa para especificar los nombres de métodos que deben ignorarse en el rastro. Esto puede ser útil para reducir el ruido en aplicaciones JNI particularmente grandes. La opción se puede suministrar varias veces. Por ejemplo, -e ^Find -e GetEnv excluiría de los resultados todos los nombres de métodos JNI que comienzan a buscar o contienen getenv.-I <string> - se utiliza para especificar las exportaciones de una biblioteca que se deben rastrear. Esto es útil para bibliotecas donde solo desea rastrear una pequeña cantidad de métodos. Las funciones que Jnitrace considera exportadas son cualquier función que se puede llamar directamente desde el lado de Java, como tal, que incluye métodos vinculados al uso de registros. La opción se puede suministrar varias veces. Por ejemplo, -I stringFromJNI -I nativeMethod([B)V podría usarse para incluir una exportación de la biblioteca llamada Java_com_nativetest_MainActivity_stringFromJNI y un método unido usando registros de registro con la firma de nativeMethod([B)V .-E <string> se usa para especificar las exportaciones de una biblioteca que no deben rastrearse. Esto es útil para bibliotecas donde tiene un grupo de llamadas nativas ocupadas que desea ignorar. Las funciones que Jnitrace considera exportadas son cualquier función que se puede llamar directamente desde el lado de Java, como tal, que incluye métodos vinculados al uso de registros. La opción se puede suministrar varias veces. Por ejemplo, -E JNI_OnLoad -E nativeMethod excluiría del rastreo de la llamada de función JNI_OnLoad y cualquier método con el nombre nativeMethod .-o path/output.json - se utiliza para especificar una ruta de salida donde jnitrace almacenará todos los datos trazados. La información se almacena en formato JSON para permitir el procesamiento posterior posterior de los datos de rastreo.-p path/to/script.js : la ruta proporcionada se usa para cargar un script Frida en el proceso de destino antes de que se haya cargado el script jnitrace . Esto se puede utilizar para derrotar el código antifrida o anti-fondos antes de que comience jnitrace .-a path/to/script.js : la ruta proporcionada se usa para cargar el script Frida en el proceso de destino después de que se ha cargado jnitrace .--hide-data -Se utiliza para reducir la cantidad de salida que se muestra en la consola. Esta opción ocultará datos adicionales que se muestran como hexdumps o como referencias de cadena.--ignore-env -Uso de esta opción ocultará todas las llamadas que la aplicación está haciendo con la estructura JNIENV.--ignore-vm : el uso de esta opción ocultará todas las llamadas que la aplicación está haciendo con Javavm Struct.--aux <name=(string|bool|int)value> -Se utiliza para pasar parámetros personalizados al generar una aplicación. Por ejemplo, --aux='uid=(int)10' generará la aplicación para el usuario 10 en lugar del usuario predeterminado 0.Nota
Recuerde que Frida-Server debe estar ejecutándose antes de ejecutar jnitrace . Si se han seguido las instrucciones predeterminadas para instalar FRIDA, el siguiente comando iniciará el servidor listo para jnitrace :
adb shell /data/local/tmp/frida-server
El motor que impulsa Jnitrace está disponible como un proyecto separado. Ese proyecto le permite importar Jnitrace para rastrear las llamadas de API JNI individuales, en un método familiar para usar el Interceptor Frida para adjuntar a las funciones y direcciones.
import { JNIInterceptor } from "jnitrace-engine" ;
JNIInterceptor . attach ( "FindClass" , {
onEnter ( args ) {
console . log ( "FindClass method called" ) ;
this . className = Memory . readCString ( args [ 1 ] ) ;
} ,
onLeave ( retval ) {
console . log ( "tLoading Class:" , this . className ) ;
console . log ( "tClass ID:" , retval . get ( ) ) ;
}
} ) ;Más información: https://github.com/chame1eon/jnitrace-ingine
La construcción jnitrace desde la fuente requiere que node primero se instale. Después de instalar node , se deben ejecutar los siguientes comandos:
npm installnpm run watch npm run watch ejecutará frida-compile en el fondo compilando la fuente al archivo de salida, build/jnitrace.js . jnitrace.py se carga de build/jnitrace.js de forma predeterminada, por lo que no se requieren otros cambios para ejecutar las actualizaciones.
Al igual que Frida-Trace, la salida se colorea en función del hilo de llamadas API.
Inmediatamente debajo de la ID de subproceso en la pantalla está el nombre del método de la API JNI. Los nombres de métodos coinciden exactamente con los vistos en el archivo de encabezado jni.h
Las líneas posteriores contienen una lista de argumentos indicados por un |- . Después de los caracteres |- son el tipo de argumento seguido del valor del argumento. Para JMethods, JFields y JClasses, el tipo Java se mostrará en aparatos ortopédicos. Esto depende de jnitrace haber visto el método original, el campo o la búsqueda de clase. Para cualquier método que pase los amortiguadores, jnitrace extraerá los buffers de los argumentos y lo mostrará como un hexdump por debajo del valor del argumento.
Los valores de retorno se muestran en la parte inferior de la lista como |= y no estarán presentes para los métodos nulo.
Si el backtrace está habilitado, se mostrará una frida backtrace debajo de la llamada del método. Tenga en cuenta, según los documentos de Frida, el retroceso difuso no siempre es preciso y el contrario preciso puede proporcionar resultados limitados.
El objetivo de este proyecto era crear una herramienta que pudiera rastrear las llamadas de la API JNI de manera eficiente para la mayoría de las aplicaciones de Android.
Desafortunadamente, el enfoque más simple de unir a todos los punteros de la función en la estructura JNIENV sobrecarga la aplicación. Causa un bloqueo basado en la gran cantidad de llamadas de funciones realizadas por otras bibliotecas no relacionadas que también usan las mismas funciones en libart.so .
Para lidiar con esa barrera de rendimiento, jnitrace crea una sombra que puede suministrar a las bibliotecas que quiere rastrear. Que JNienv contiene una serie de trampolines de funciones que rebotan las llamadas de la API JNI a través de algunos Nativecallbacks personalizados para rastrear la entrada y la salida de esas funciones.
La API genérica de Frida hace un gran trabajo al proporcionar una plataforma para construir esos trampolines de funciones con un esfuerzo mínimo. Sin embargo, ese enfoque simple no funciona para toda la API JNIENV. El problema clave con el rastreo de todos los métodos es el uso de argumentos variádicos en la API. No es posible crear el Nativecallback para estas funciones con anticipación, ya que no se sabe de antemano todas las diferentes combinaciones de métodos Java que se llamarán.
La solución es monitorear el proceso de llamadas para GetMethodID o GetStaticMethodID , utilizada para buscar identificadores de métodos desde el tiempo de ejecución. Una vez que jnitrace ve una búsqueda jmethodID , tiene un mapeo conocido de ID a la firma del método. Más tarde, cuando se realiza una llamada de método JNI Java, se utiliza un Nativecallback inicial para extraer la ID de método en la llamada. Esa firma del método se analiza para extraer los argumentos del método. Una vez que jnitrace ha extraído los argumentos en el método, puede crear dinámicamente un nativecallback para ese método. Ese nuevo nativecallback se devuelve y un poco de arquitectura específica de shellcode se ocupa de la configuración de la pila y los registros para permitir que esa llamada se ejecute con éxito. Esos nativecallbacks para métodos específicos se almacenan en caché para permitir que la devolución de llamada se ejecute de manera más eficiente si es un método si se lo llama varias veces.
El otro lugar donde un simple nativecallback no es suficiente para extraer los argumentos de una llamada de método, es para llamadas que usan un puntero VA_ARGS como argumento final. En este caso, jnitrace usa algún código para extraer los argumentos del puntero provisto. Nuevamente, esto es específico de la arquitectura.
Todos los datos trazados en estas llamadas de función se envían a la aplicación de la consola Python que los formatea y lo muestra al usuario.
La mayoría de las pruebas de esta herramienta se han realizado en un emulador de Android x86_64 que ejecuta Marshmallow. Cualquier problema experimentado ejecutándose en otro dispositivo, por favor presente un problema, pero también, si es posible, se recomienda intentar ejecutarse en un emulador similar.
Para cualquier problema experimentado con jnitrace , cree un problema en GitHub. Incluya la siguiente información en el problema presentado: