contenu proc/maps

Détection du chargement dynamique dans les applications Android avec /proc/maps

TL; DR: Grâce au chargement dynamique, les auteurs de logiciels malveillants peuvent charger secrètement du code malveillant dans leur application afin d'éviter d'être détectés. Nous pouvons détecter un tel chargement via le fichier généré par le noyau /proc/[PID]/maps de l'application.
Récemment, nous avons créé un script simple qui nous permet de détecter le chargement dynamique dans les applications Android. Cela nous a offert une bonne occasion de discuter du chargement dynamique en général dans ce blog. 

Chargement dynamique et liaison :

Afin de comprendre le reste du blog, il est important de passer en revue les bases de la liaison et du chargement, et de souligner les différences entre la liaison et le chargement, et entre statique et dynamique.

Qu'est-ce que le lien :

Le processus de compilation comporte plusieurs parties. La liaison est la dernière étape avant d'obtenir un exécutable exécutable. Un programme est généralement plus qu'un simple fichier autonome et s'appuie sur d'autres bibliothèques ou fichiers pour fonctionner. En tant que tel, il ne vous suffit pas de compiler votre code en code machine pour pouvoir l'exécuter, mais aussi pour en quelque sorte lien les différents fichiers dans un exécutable cohérent.

Comment cela se fait-il en pratique ? Après compilation et assemblage, l'assembleur génère des fichiers objets, qui correspondent généralement à chaque module de votre programme. Ces fichiers peuvent être soit déplaçables, soit exécutables. Les fichiers objets de la première variété doivent subir une liaison avant de pouvoir être exécutés, mais ceux du second type peuvent être exécutés immédiatement. Dans l'écosystème GNU/Linux, l'extension habituelle d'un fichier objet est .o. Les fichiers d'objets partagés, appelés bibliothèques, sont des fichiers d'objets déplaçables destinés à être utilisés par de nombreux programmes différents et ne peuvent pas être exécutés seuls. Sous GNU/Linux, ils ont l'extension .so (Shared Object), alors que sous Windows, ils ont l'extension .dll (Dynamic-link Library).

L'éditeur de liens prend ensuite les fichiers, résout les symboles (fonctions et variables) et les pointe vers les bonnes adresses mémoire en écrivant tout dans la table des symboles de l'exécutable. Par souci de performance, il a également déménage votre code afin que les morceaux de code associés finissent par être mappés sur des adresses mémoire proches, quelle que soit la manière dont vous avez initialement organisé votre programme, car la lisibilité humaine n'est plus un problème et les performances sont la principale priorité. 

Les nombreux noms différents de l'exécutable :

Sous Linux et Windows, ces extensions apparemment différentes ont les mêmes formats sous-jacents que les exécutables. Sous Windows, ce sont des PE (Portable Executable), et ont généralement les extensions .exe et .dll, et sous Linux, ce sont des ELF (Executable and Linkable File), et ont les extensions .bin, .so ou .o (Oui , les fichiers objets que nous avons mentionnés précédemment sont également des ELF).

Liaison dynamique :

Nous disons que quelque chose a été lié dynamiquement lorsqu'au lieu d'effectuer ce processus au moment de la compilation, il est effectué au moment du chargement ou de l'exécution. Ce qui nous amène au chargement : un chargeur copie simplement le contenu de la sortie de l'éditeur de liens dans la mémoire et exécute le programme. Lorsque la liaison dynamique est utilisée, le processus de liaison se produit juste avant le chargement, lors de l'exécution du programme, ce qui conduit souvent à une confusion entre les deux.
Le chargement dynamique, d'autre part, signifie que des parties du code peuvent être chargées en mémoire à tout moment pendant l'exécution. Les deux processus peuvent et sont fréquemment effectués ensemble.

Alors, pourquoi est-ce un problème ?

Le chargement dynamique est certainement utile. Par exemple, plutôt que de charger toutes les bibliothèques que votre programme utilisera au moment du chargement, vous pouvez les charger uniquement lorsque vous en avez besoin, utilisant ainsi moins de mémoire, ou les charger uniquement de manière conditionnelle dans certains cas.

Mais il présente également un moyen simple pour les développeurs de logiciels malveillants de masquer leur code malveillant. Ils peuvent mettre tout leur code légitime dans l'APK et déplacer tout le code néfaste dans un DEX (Dalvik Executable) que l'application téléchargera puis chargera dynamiquement pendant l'utilisation et ainsi faire apparaître leur application comme inoffensive lors d'une inspection statique de base du APK.

Comment les classes DEX sont-elles chargées :

Android offre une option pour charger dynamiquement des fichiers .dex à l'aide d'une classe appelée DexClassLoader. Pour charger une classe, il suffit d'écrire :

// Init the loader
DexClassLoader dexClassLoader = new DexClassLoader(path_to_dex, null, null, parent_class);

// Load the class:
Class dynamic_class = dexClassLoader.loadClass("DynamicClass");

// Load a method we could call it
Method method = dynamic_class.getMethod("method1");

Et puis nous pouvons utiliser l'invoke() de la méthode pour utiliser la méthode.

Le fichier /proc/[PID]/maps :

Sous Unix, tout est un fichier, et même si ce n'en est pas vraiment un, il est manipulé et accessible comme tel. Cela inclut les structures de données du noyau, et Linux ne fait pas exception à la règle. Le noyau Linux nous permet d'accéder et de lire ses structures de données via le /proc/ système de pseudo-fichiers. Chaque processus a alors son propre dossier dans /proc/[PID]. Les fichiers et sous-dossiers ici contiennent de nombreuses informations utiles et importantes sur le processus, mais aujourd'hui, nous allons nous concentrer sur un seul fichier : /proc/[PID]/maps.

/proc/[PID]/maps affiche un graphique de la mémoire mappée d'un processus. Lorsque nous parlons de mémoire mappée, nous entendons un segment de mémoire virtuelle qui a une correspondance un à un avec un fichier. Ce mappage permet à une application de modifier et d'accéder à des fichiers en lisant et en écrivant directement dans la mémoire. Cela signifie que lorsqu'un programme accède à un fichier, cela finira par être enregistré dans son fichier /proc/[PID]/maps.

/proc/[PID]/maps nous montre également les autorisations dont dispose le processus pour chaque segment. Cela peut nous aider à déterminer quels fichiers le processus a modifiés et quels fichiers il a lus.

Voici à quoi ressemble un court segment d'un fichier /proc/PID/maps normal :

7f9cefbf7000-7f9cefbf8000 r--p 00000000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf8000-7f9cefbf9000 r-xp 00001000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf9000-7f9cefbfa000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfa000-7f9cefbfb000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfb000-7f9cefbfc000 rw-p 00003000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfc000-7f9cefc08000 r--p 00000000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc08000-7f9cefc1b000 r-xp 0000c000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc1b000-7f9cefc24000 r--p 0001f000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc24000-7f9cefc25000 r--p 00027000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc25000-7f9cefc26000 rw-p 00028000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc26000-7f9cefc27000 r--p 00000000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc27000-7f9cefc28000 r-xp 00001000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc28000-7f9cefc29000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc29000-7f9cefc2a000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2a000-7f9cefc2b000 rw-p 00003000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2b000-7f9cefc47000 r--p 00000000 103:03 1584005            /usr/lib/libX11.so.6.3.0
7f9cefc47000-7f9cefcd1000 r-xp 0001c000 103:03 1584005            /usr/lib/libX11.so.6.3.0

Chaque ligne du fichier enregistre un seul segment de mémoire dans l'espace d'adressage de mémoire virtuelle contigu alloué au processus.

  • Adresse – La première colonne indique l'adresse de début et de fin du segment.
  • Permissions – Cette colonne indique les autorisations dont dispose le processus pour le segment. r/w/x sont les lectures/écritures/exécutions habituelles, tandis que la dernière lettre est s or p, signifiant respectivement partagé ou privé.
  • Compenser – Il s'agit du décalage depuis le début du fichier, afin de pouvoir calculer l'adresse de départ des données mappées. Parfois, un segment n'est pas mappé à partir d'un fichier (auquel cas la colonne de chemin aura un identifiant pour la nature du segment, comme expliqué ci-dessous), auquel cas le décalage est simplement laissé à 0.
  • Appareil – Lorsque le segment a été mappé à partir d'un fichier, le numéro hexadécimal du périphérique sur lequel le fichier est stocké s'affiche dans cette colonne.
  • Inode (nœud d'indexation) – Si le segment provient d'un fichier, il s'agit du numéro d'inode du fichier.
  • Chemin – C'est le chemin du fichier, s'il y en a un. Cela peut être [heap], [stack] ou [vsdo] si le segment est la structure éponyme.

Notre humble scénario :

Dans les applications Android incriminées, le chargement dynamique du code est généralement effectué à partir du répertoire d'accueil de l'application, de sorte que les fichiers chargés en mémoire à partir de ce dictionnaire (ou d'autres emplacements courants) doivent apparaître dans le fichier de cartes. Pour automatiser la tâche de vérification de ce fichier, nous avons construit un script très simple qui utilise regex pour rechercher la chaîne '/data/data' dans le fichier de cartes d'un PID donné dans l'appareil connecté et renvoie les lignes qui correspondent. /data/data est bien sûr le répertoire d'accueil des applications où les fichiers et les données sont stockés. C'est le seul emplacement à partir duquel une application peut charger des fichiers DEX.

Interagir avec ADB via Python était parfois un défi, car l'utilisation du shell peut être gênante, mais nous avons trouvé un ensemble d'outils utiles appelé outils électriques qui fournissent exactement cette fonctionnalité. Ils offrent un vaste ensemble de fonctionnalités conçues pour aider au piratage et au prototypage. Jetez-y un coup d'œil.

Vous pouvez voir le script complet que nous utilisons ici Github essentiel.

Passer au contenu