Un outil basé sur Frida pour tracer l'utilisation de l'API JNI dans les applications Android.
Les bibliothèques natives contenues dans les applications Android utilisent souvent l'API JNI pour utiliser l'exécution Android. Le suivi de ces appels par l'ingénierie inverse manuelle peut être un processus lent et douloureux. jnitrace fonctionne comme un outil de traçage d'analyse dynamique similaire à Frida-Trace ou Strace mais pour le JNI.
Le moyen le plus simple de courir avec jnitrace est d'installer en utilisant PIP:
pip install jnitrace
Après une installation PIP, il est facile d'exécuter jnitrace :
jnitrace -l libnative-lib.so com.example.myapplication
jnitrace nécessite un minimum de deux paramètres pour exécuter une trace:
-l libnative-lib.so - est utilisé pour spécifier les bibliothèques à tracer. Cet argument peut être utilisé plusieurs fois ou * peut être utilisé pour suivre toutes les bibliothèques. Par exemple, -l libnative-lib.so -l libanother-lib.so ou -l * .com.example.myapplication - est le package Android à tracer. Ce package doit déjà être installé sur l'appareil.Les arguments facultatifs sont répertoriés ci-dessous:
-R <host>:<port> - est utilisé pour spécifier l'emplacement du réseau du serveur Frida distant. Si A: n'est pas spécifié, localhost: 27042 est utilisé par DeAfult.-m <spawn|attach> - est utilisé pour spécifier le mécanisme d'attache Frida à utiliser. Il peut être en train de frayer ou de fixer. Spawn est l'option par défaut et recommandée.-b <fuzzy|accurate|none> - est utilisé pour contrôler la sortie de retour arrière. Par défaut, jnitrace exécutera le backtraceur en mode accurate . Cette option peut être changée en mode fuzzy ou utilisée pour arrêter le retour de retour en utilisant l'option none . Voir les documents Frida pour une explication sur les différences.-i <regex> - est utilisé pour spécifier les noms de méthode qui doivent être tracés. Cela peut être utile pour réduire le bruit dans les applications JNI particulièrement grandes. L'option peut être fournie plusieurs fois. Par exemple, -i Get -i RegisterNatives n'inclurait que des méthodes JNI contenant des objets ou des inscriptions à leur nom.-e <regex> - est utilisé pour spécifier les noms de méthode qui doivent être ignorés dans la trace. Cela peut être utile pour réduire le bruit dans les applications JNI particulièrement grandes. L'option peut être fournie plusieurs fois. Par exemple, -e ^Find -e GetEnv exclurait des résultats tous les noms de méthode JNI qui commencent à trouver ou à contenir GETENV.-I <string> - est utilisé pour spécifier les exportations d'une bibliothèque qui doit être tracée. Ceci est utile pour les bibliothèques où vous voulez seulement retracer un petit nombre de méthodes. Les fonctions que JnitRace considère que les exportations sont toutes les fonctions qui sont directement invitables du côté Java, en tant que telles, qui incluent des méthodes liées à l'aide de registres. L'option peut être fournie plusieurs fois. Par exemple, -I stringFromJNI -I nativeMethod([B)V pourrait être utilisé pour inclure une exportation de la bibliothèque appelée Java_com_nativetest_MainActivity_stringFromJNI et une méthode liée à l'aide de noms de registre avec la signature de nativeMethod([B)V .-E <string> est utilisé pour spécifier les exportations d'une bibliothèque qui ne doit pas être tracée. Ceci est utile pour les bibliothèques où vous avez un groupe d'appels natifs occupés que vous souhaitez ignorer. Les fonctions que JnitRace considère que les exportations sont toutes les fonctions qui sont directement invitables du côté Java, en tant que telles, qui incluent des méthodes liées à l'aide de registres. L'option peut être fournie plusieurs fois. Par exemple, -E JNI_OnLoad -E nativeMethod excluait de l'appel de fonction de trace JNI_OnLoad et toutes les méthodes avec le nom nativeMethod .-o path/output.json - est utilisé pour spécifier un chemin de sortie où jnitrace stockera toutes les données tracées. Les informations sont stockées au format JSON pour permettre la post-traitement ultérieure des données de trace.-p path/to/script.js - Le chemin fourni est utilisé pour charger un script Frida dans le processus cible avant le chargement du script jnitrace . Cela peut être utilisé pour vaincre le code anti-frarie ou anti-débugage avant le début jnitrace .-a path/to/script.js - Le chemin fourni est utilisé pour charger le script Frida dans le processus cible après le chargement jnitrace .--hide-data - Utilisé pour réduire la quantité de sortie affichée dans la console. Cette option masquera des données supplémentaires affichées sous forme de hexdumps ou de dé-références de chaîne.--ignore-env - L'utilisation de cette option masquera tous les appels que l'application fait à l'aide de la structure JNienv.--ignore-vm - L'utilisation de cette option masquera tous les appels que l'application fait à l'aide de la structure Javavm.--aux <name=(string|bool|int)value> - Utilisé pour transmettre des paramètres personnalisés lors de la frai d'une application. Par exemple --aux='uid=(int)10' engendrera l'application pour l'utilisateur 10 au lieu de l'utilisateur par défaut 0.Note
N'oubliez pas que Frida-Server doit fonctionner avant d'exécuter jnitrace . Si les instructions par défaut pour l'installation de Frida ont été suivies, la commande suivante démarrera le serveur prêt pour jnitrace :
adb shell /data/local/tmp/frida-server
Le moteur qui alimente Jnitrace est disponible en tant que projet distinct. Ce projet vous permet d'importer Jnitrace pour suivre les appels API JNI individuels, dans une méthode familière à l'utilisation de l' Interceptor Frida pour se connecter aux fonctions et adresses.
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 ( ) ) ;
}
} ) ;Plus d'informations: https://github.com/chame1eon/jnitrace-engine
La construction jnitrace de la source nécessite que node soit d'abord installé. Après avoir installé node , les commandes suivantes doivent être exécutées:
npm installnpm run watch npm run watch exécutera frida-compile en arrière-plan en compilant la source dans le fichier de sortie, build/jnitrace.js . jnitrace.py se charge de build/jnitrace.js par défaut, donc aucune autre modification n'est requise pour exécuter les mises à jour.
Comme Frida-Trace, la sortie est colorée en fonction du fil d'appel de l'API.
Immédiatement en dessous de l'ID de thread de l'affichage se trouve le nom de la méthode de l'API JNI. Les noms de méthode correspondent exactement à ceux observés dans le fichier d'en-tête jni.h
Les lignes suivantes contiennent une liste d'arguments indiqués par A |- . Une fois que les caractères |- sont le type d'argument suivi de la valeur de l'argument. Pour JMethods, Jfields et Jclasses, le type Java sera affiché en accolades bouclées. Cela dépend de jnitrace ayant vu la méthode, le champ ou la recherche de classe d'origine. Pour toutes les méthodes qui passent des tampons, jnitrace extrait les tampons des arguments et l'affichera sous forme de hexdump sous la valeur de l'argument.
Les valeurs de retour sont affichées en bas de la liste comme |= et ne seront pas présentes pour les méthodes vides.
Si le retour de retour est activé, un retour de retour de Frida sera affiché sous l'appel de la méthode. Veuillez noter que, selon les documents Frida, le retour de retour flou n'est pas toujours exact et le retour de retour précis peut fournir des résultats limités.
L'objectif de ce projet était de créer un outil qui pourrait tracer des appels API JNI efficacement pour la plupart des applications Android.
Malheureusement, l'approche la plus simple de la connexion à tous les pointeurs de fonction de la structure JNienv surcharge l'application. Il provoque un crash basé sur le nombre d'appels de fonction faits par d'autres bibliothèques non liées à l'aide des mêmes fonctions dans libart.so .
Pour faire face à cette barrière de performance, jnitrace crée une ombre Jniienv qu'elle peut fournir aux bibliothèques qu'il souhaite suivre. Que JNIENV contient une série de trampolines de fonction qui font rebondir les appels de l'API JNI via certains NativeCallbacks Frida personnalisés pour suivre l'entrée et la sortie de ces fonctions.
L'API générique Frida fait un excellent travail pour fournir une plate-forme pour construire ces trampolines de fonction avec un minimum d'effort. Cependant, cette approche simple ne fonctionne pas pour toute l'API JNienv. Le problème clé du traçage de toutes les méthodes est l'utilisation d'arguments variades dans l'API. Il n'est pas possible de créer le NativeCallback pour ces fonctions à l'avance, car on ne sait pas au préalable toutes les différentes combinaisons de méthodes Java qui seront appelées.
La solution consiste à surveiller le processus pour les appels pour GetMethodID ou GetStaticMethodID , utilisés pour rechercher des identificateurs de méthode à partir de l'exécution. Une fois que jnitrace voit une recherche jmethodID , il a une cartographie connue de la signature ID à la méthode. Plus tard, lorsqu'un appel de méthode JNI Java est passé, un NativeCallback initial est utilisé pour extraire l'ID de méthode dans l'appel. Cette signature de méthode est ensuite analysée pour extraire les arguments de la méthode. Une fois que jnitrace a extrait les arguments dans la méthode, il peut créer dynamiquement un NativeCallback pour cette méthode. Ce nouveau NativeCallback est renvoyé et un peu de shellcode spécifique à l'architecture s'occupe de la configuration de la pile et des registres pour permettre à cet appel de s'exécuter avec succès. Ces nativecallbacks pour des méthodes spécifiques sont mis en cache pour permettre au rappel d'exécuter plus efficacement si une méthode est appelée plusieurs fois.
L'autre endroit où un simple NativeCallback n'est pas suffisant pour extraire les arguments d'un appel de méthode, est destiné aux appels à l'aide d'un pointeur VA_ARGS comme argument final. Dans ce cas, jnitrace utilise un code pour extraire les arguments du pointeur fourni. Encore une fois, c'est spécifique à l'architecture.
Toutes les données tracées dans ces appels de fonction sont envoyées à l'application de console Python qui les formats et les affiche à l'utilisateur.
La plupart des tests de cet outil ont été effectués sur un émulateur Android X86_64 exécutant Marshmallow. Tout problème rencontré sur un autre appareil, veuillez déposer un problème, mais si possible, il est recommandé d'essayer de fonctionner sur un émulateur similaire.
Pour tous les problèmes qui exécutent jnitrace , veuillez créer un problème sur GitHub. Veuillez inclure les informations suivantes dans le problème déposé: