hacktricks/mobile-pentesting/android-app-pentesting/reversing-native-libraries.md
2023-06-03 13:10:46 +00:00

21 KiB
Raw Blame History

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Informations copiées de https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html** (vous pouvez trouver des solutions là-bas)**

Les applications Android peuvent contenir des bibliothèques natives compilées. Les bibliothèques natives sont du code que le développeur a écrit puis compilé pour une architecture informatique spécifique. Le plus souvent, cela signifie du code écrit en C ou C++. Les raisons bénignes ou légitimes pour lesquelles un développeur peut le faire sont des opérations intensives en mathématiques ou sensibles au temps, telles que les bibliothèques graphiques. Les développeurs de logiciels malveillants ont commencé à passer au code natif car l'ingénierie inverse de binaires compilés tend à être un ensemble de compétences moins courant que l'analyse du bytecode DEX. Cela est largement dû au fait que le bytecode DEX peut être décompilé en Java, tandis que le code natif compilé doit souvent être analysé en tant qu'assemblage.

Objectif

L'objectif de cette section n'est pas de vous apprendre l'assemblage (ASM) ou comment inverser le code compilé de manière plus générale, mais plutôt comment appliquer les compétences d'ingénierie inverse binaire plus générales, spécifiquement à Android. Parce que l'objectif de cet atelier n'est pas de vous enseigner les architectures ASM, tous les exercices incluront une version ARM et une version x86 de la bibliothèque à analyser afin que chaque personne puisse choisir l'architecture avec laquelle elle est plus à l'aise.

Apprentissage de l'assemblage ARM

Si vous n'avez pas d'expérience préalable en ingénierie inverse binaire / assemblage, voici quelques ressources suggérées. La plupart des appareils Android fonctionnent sur ARM, mais tous les exercices de cet atelier incluent également une version x86 de la bibliothèque.

Pour apprendre et / ou réviser l'assemblage ARM, je suggère fortement les bases de l'assemblage ARM de Azeria Labs.

Introduction à l'interface native Java (JNI)

L'interface native Java (JNI) permet aux développeurs de déclarer des méthodes Java qui sont implémentées dans du code natif (généralement compilé en C/C++). L'interface JNI n'est pas spécifique à Android, mais est disponible plus généralement aux applications Java qui s'exécutent sur différentes plates-formes.

Le kit de développement natif Android (NDK) est l'ensemble d'outils spécifique à Android sur JNI. Selon les docs:

Dans Android, le kit de développement natif (NDK) est un ensemble d'outils qui permet aux développeurs d'écrire du code C et C++ pour leurs applications Android.

Ensemble, JNI et NDK permettent aux développeurs Android d'implémenter une partie de la fonctionnalité de leur application en code natif. Le code Java (ou Kotlin) appellera une méthode native déclarée en Java qui est implémentée dans la bibliothèque native compilée.

Références

Oracle JNI Docs

Références JNI & NDK Android

  • Conseils JNI Android < Je recommande vivement de lire la section "Bibliothèques natives" pour commencer
  • Commencer avec le NDK < Il s'agit d'un guide pour aider les développeurs à développer des bibliothèques natives et à comprendre comment les choses sont construites, ce qui facilite l'inversion.

Cible d'analyse - Bibliothèques natives Android

Pour cette section, nous nous concentrons sur la manière d'inverser la fonctionnalité de l'application qui a été implémentée dans des bibliothèques natives Android. Lorsque nous disons bibliothèques natives Android, qu'entendons-nous?

Les bibliothèques natives Android sont incluses dans les APK sous forme de bibliothèques d'objets partagés .so, dans le format de fichier ELF. Si vous avez déjà analysé des binaires Linux, c'est le même format.

Ces bibliothèques sont incluses par défaut dans l'APK au chemin de fichier /lib/<cpu>/lib<name>.so. C'est le chemin par défaut, mais les développeurs peuvent également choisir d'inclure la bibliothèque native dans /assets/<custom_name> s'ils le souhaitent. Plus souvent, nous voyons les développeurs de logiciels malveillants choisir d'inclure des bibliothèques natives dans des chemins autres que /lib et d'utiliser des extensions de fichier différentes pour tenter de "cacher" la présence de la bibliothèque native.

Étant donné que le code natif est compilé pour des CPU spécifiques, si un développeur veut que son application s'exécute sur plus d'un type de matériel, il doit inclure chacune de ces versions de la bibliothèque native compilée dans l'application. Le chemin par défaut mentionné ci-dessus inclut un répertoire pour chaque type de CPU officiellement pris en charge par Android.

CPU Chemin de la bibliothèque native
ARM 32 bits générique 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

Chargement de la bibliothèque

Avant qu'une application Android ne puisse appeler et exécuter un code implémenté dans une bibliothèque native, l'application (code Java) doit charger la bibliothèque en mémoire. Il existe deux appels d'API différents qui feront cela:

System.loadLibrary("calc")

Voici le contenu traduit en français :

Rétro-ingénierie de bibliothèques natives

Introduction

Les bibliothèques natives sont des fichiers binaires compilés qui contiennent du code machine pour une architecture spécifique. Les bibliothèques natives sont souvent utilisées pour fournir des fonctionnalités supplémentaires à une application, telles que l'accès à des fonctionnalités du système d'exploitation ou à des bibliothèques tierces. Les bibliothèques natives sont souvent utilisées dans les applications Android pour fournir des fonctionnalités telles que la prise en charge de la caméra, l'accès aux bases de données, etc.

Extraire les bibliothèques natives

Les bibliothèques natives sont généralement stockées dans le répertoire lib de l'APK. Pour extraire les bibliothèques natives, vous pouvez utiliser l'outil apktool. L'outil apktool est un outil open source qui permet de décompiler et de recompiler des APK.

Pour extraire les bibliothèques natives, vous pouvez utiliser la commande suivante :

apktool d -s -o <output_directory> <apk_file>

Cette commande extraira les bibliothèques natives dans le répertoire de sortie spécifié.

Analyse des bibliothèques natives

Les bibliothèques natives sont des fichiers binaires compilés, ce qui signifie qu'elles ne peuvent pas être lues directement. Pour analyser les bibliothèques natives, vous devez les désassembler en code assembleur.

Il existe plusieurs outils pour désassembler les bibliothèques natives, tels que IDA Pro, Hopper, Ghidra, etc. Ces outils permettent de désassembler les bibliothèques natives en code assembleur et de les analyser.

Recherche de vulnérabilités

Une fois que vous avez désassemblé les bibliothèques natives, vous pouvez rechercher des vulnérabilités dans le code. Les vulnérabilités courantes dans les bibliothèques natives comprennent les dépassements de tampon, les fuites de mémoire, les erreurs de format de chaîne, etc.

Pour rechercher des vulnérabilités dans le code, vous pouvez utiliser des outils tels que grep, strings, objdump, etc. Ces outils permettent de rechercher des chaînes de caractères spécifiques dans le code, de rechercher des appels de fonctions dangereuses, etc.

Conclusion

La rétro-ingénierie des bibliothèques natives est une compétence importante pour les testeurs de pénétration et les chercheurs en sécurité. En comprenant comment les bibliothèques natives sont utilisées dans les applications Android et comment les analyser, vous pouvez identifier les vulnérabilités et aider à sécuriser les applications.

System.load("lib/armeabi/libcalc.so")

La différence entre les deux appels d'API est que loadLibrary ne prend que le nom court de la bibliothèque en argument (c'est-à-dire libcalc.so = "calc" et libinit.so = "init") et le système détermine correctement l'architecture sur laquelle il s'exécute et donc le fichier correct à utiliser. D'autre part, load nécessite le chemin complet de la bibliothèque. Cela signifie que le développeur de l'application doit déterminer l'architecture et donc le fichier de bibliothèque correct à charger lui-même.

Lorsque l'un de ces deux API (loadLibrary ou load) est appelé par le code Java, la bibliothèque native qui est passée en argument exécute son JNI_OnLoad si elle a été implémentée dans la bibliothèque native.

Pour réitérer, avant d'exécuter des méthodes natives, la bibliothèque native doit être chargée en appelant System.loadLibrary ou System.load dans le code Java. Lorsque l'une de ces 2 API est exécutée, la fonction JNI_OnLoad dans la bibliothèque native est également exécutée.

La connexion entre le code Java et le code natif

Pour exécuter une fonction de la bibliothèque native, il doit y avoir une méthode native déclarée en Java que le code Java peut appeler. Lorsque cette méthode native déclarée en Java est appelée, la fonction native "associée" de la bibliothèque native (ELF/.so) est exécutée.

Une méthode native déclarée en Java apparaît dans le code Java comme ci-dessous. Elle apparaît comme n'importe quelle autre méthode Java, sauf qu'elle inclut le mot-clé native et n'a pas de code dans sa mise en œuvre, car son code est en fait dans la bibliothèque native compilée.

public native String doThingsInNativeLibrary(int var0);

Pour appeler cette méthode native, le code Java l'appellerait comme n'importe quelle autre méthode Java. Cependant, en arrière-plan, le JNI et le NDK exécuteraient plutôt la fonction correspondante dans la bibliothèque native. Pour ce faire, il doit connaître l'appariement entre une méthode native déclarée en Java et une fonction dans la bibliothèque native.

Il existe 2 façons différentes de faire cet appariement, ou de le lier :

  1. Liaison dynamique en utilisant la résolution de nom de méthode native JNI, ou
  2. Liaison statique en utilisant l'appel d'API RegisterNatives

Liaison dynamique

Pour lier, ou appairer, la méthode native déclarée en Java et la fonction dans la bibliothèque native de manière dynamique, le développeur nomme la méthode et la fonction selon les spécifications de sorte que le système JNI puisse faire la liaison dynamiquement.

Selon la spécification, le développeur nommerait la fonction comme suit pour que le système puisse lier dynamiquement la méthode native et la fonction. Un nom de méthode native est concaténé à partir des composants suivants :

  1. le préfixe Java_
  2. un nom de classe qualifié entièrement manglé
  3. un séparateur de soulignement ("_")
  4. un nom de méthode manglé
  5. pour les méthodes natives surchargées, deux traits de soulignement ("__") suivis de la signature d'argument manglée

Pour effectuer une liaison dynamique pour la méthode native déclarée en Java ci-dessous et disons qu'elle se trouve dans la classe com.android.interesting.Stuff

public native String doThingsInNativeLibrary(int var0);

La fonction dans la bibliothèque native devrait être nommée:

Java_com_android_interesting_Stuff_doThingsInNativeLibrary

Si une fonction avec ce nom n'existe pas dans la bibliothèque native, cela signifie que l'application doit effectuer une liaison statique.

Liaison statique

Si le développeur ne veut pas ou ne peut pas nommer les fonctions natives selon la spécification (par exemple, s'il veut supprimer les symboles de débogage), il doit utiliser la liaison statique avec l'API RegisterNatives (doc) afin de faire correspondre la méthode native déclarée en Java avec la fonction dans la bibliothèque native. La fonction RegisterNatives est appelée à partir du code natif, pas du code Java, et est le plus souvent appelée dans la fonction JNI_OnLoad, car RegisterNatives doit être exécutée avant d'appeler la méthode native déclarée en Java.

jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);

typedef struct { 
    char *name; 
    char *signature; 
    void *fnPtr; 
} JNINativeMethod;

Lors de l'ingénierie inverse, si l'application utilise la méthode de liaison statique, nous, en tant qu'analystes, pouvons trouver la structure JNINativeMethod qui est transmise à RegisterNatives afin de déterminer quelle sous-routine de la bibliothèque native est exécutée lorsque la méthode native déclarée en Java est appelée.

La structure JNINativeMethod nécessite une chaîne de caractères pour le nom de la méthode native déclarée en Java et une chaîne de caractères pour la signature de la méthode, nous devrions donc être en mesure de les trouver dans notre bibliothèque native.

Signature de méthode

La structure JNINativeMethod nécessite la signature de la méthode. Une signature de méthode indique les types des arguments que la méthode prend et le type de ce qu'elle renvoie. Ce lien documente les Signatures de type JNI dans la section "Signatures de type".

  • Z: boolean
  • B: byte
  • C: char
  • S: short
  • I: int
  • J: long
  • F: float
  • D: double
  • L fully-qualified-class ; :classe entièrement qualifiée
  • [ type: type[]
  • ( arg-types ) ret-type: type de méthode
  • V: void

Pour la méthode native

public native String doThingsInNativeLibrary(int var0);

La signature de type est

(I)Ljava/lang/String;

Voici un autre exemple de méthode native et de sa signature. Pour cela, voici la déclaration de la méthode :

public native long f (int n, String s, int[] arr); 

Il a la signature de type :

(ILjava/lang/String;[I)J

Exercice #5 - Trouver l'adresse de la fonction native

Dans l'exercice #5, nous allons apprendre à charger des bibliothèques natives dans un désassembleur et à identifier la fonction native qui est exécutée lorsqu'une méthode native est appelée. Pour cet exercice particulier, le but n'est pas de rétro-ingénierie la méthode native, mais simplement de trouver le lien entre l'appel à la méthode native en Java et la fonction qui est exécutée dans la bibliothèque native. Pour cet exercice, nous utiliserons l'échantillon Mediacode.apk. Cet échantillon est disponible à ~/samples/Mediacode.apk dans la VM. Son hachage SHA256 est a496b36cda66aaf24340941da8034bd53940d1b08d83a97f17a65ae144ebf91a.

Objectif

L'objectif de cet exercice est de :

  1. Identifier les méthodes natives déclarées dans le bytecode DEX
  2. Déterminer quelles bibliothèques natives sont chargées (et donc où les méthodes natives peuvent être implémentées)
  3. Extraire la bibliothèque native de l'APK
  4. Charger la bibliothèque native dans un désassembleur
  5. Identifier l'adresse (ou le nom) de la fonction dans la bibliothèque native qui est exécutée lorsque la méthode native est appelée

Instructions

  1. Ouvrez Mediacode.apk dans jadx. Référez-vous à l'Exercice #1
  2. Cette fois, si vous développez l'onglet Ressources, vous verrez que cet APK a un répertoire lib/. Les bibliothèques natives pour cet APK sont dans les chemins CPU par défaut.
  3. Maintenant, nous devons identifier toutes les méthodes natives déclarées. Dans jadx, recherchez et répertoriez toutes les méthodes natives déclarées. Il devrait y en avoir deux.
  4. Autour de la méthode native déclarée, voyez s'il y a un endroit où une bibliothèque native est chargée. Cela fournira des indications sur la bibliothèque native dans laquelle chercher la fonction à implémenter.
  5. Extrayez la bibliothèque native de l'APK en créant un nouveau répertoire et en copiant l'APK dans ce dossier. Ensuite, exécutez la commande unzip Mediacode.APK. Vous verrez tous les fichiers extraits de l'APK, y compris le répertoire lib/.
  6. Sélectionnez l'architecture de la bibliothèque native que vous souhaitez analyser.
  7. Démarrez ghidra en exécutant ghidraRun. Cela ouvrira Ghidra.
  8. Pour ouvrir la bibliothèque native pour l'analyse, sélectionnez "Nouveau projet", "Projet non partagé", sélectionnez un chemin pour enregistrer le projet et donnez-lui un nom. Cela crée un projet dans lequel vous pouvez ensuite charger des fichiers binaires.
  9. Une fois que vous avez créé votre projet, sélectionnez l'icône du dragon pour ouvrir le navigateur de code. Allez dans "Fichier" > "Importer un fichier" pour charger la bibliothèque native dans l'outil. Vous pouvez laisser tous les paramètres par défaut.
  10. Vous verrez l'écran suivant. Sélectionnez "Analyser".
  11. À l'aide des informations de liaison ci-dessus, identifiez la fonction dans la bibliothèque native qui est exécutée lorsque la méthode native déclarée en Java est appelée.

Chargement du fichier dans le navigateur de code Ghidra

Capture d'écran de Mediacode ouvert dans jadx

Solution

Rétro-ingénierie du code des bibliothèques natives Android - JNIEnv

Lorsque vous commencez à rétro-ingénierie des bibliothèques natives Android, l'une des choses que je ne savais pas que je devais savoir était à propos de JNIEnv. JNIEnv est une structure de pointeurs de fonction vers les fonctions JNI. Chaque fonction JNI dans les bibliothèques natives Android prend JNIEnv* comme premier argument.

De la documentation JNI Tips