Medusa : Un framework d'audit de sécurité mobile modulaire basé sur Frida

1. Un framework pour structurer les tests de sécurité, au-delà des scripts dispersés

L'analyse dynamique d'applications Android génère souvent des défis d'ingénierie répétitifs : réécrire des hooks Frida similaires pour intercepter des fonctions cryptographiques, surveiller les accès au stockage ou contourner des validations réseau. L'absence de standardisation entraîne une perte de temps, des encohérences entre les équipes et une difficulté à reproduire les résultats. Medusa s'attaque à ce manque de structure en proposant un framework où les capacités d'analyse sont formalisées en actifs de test réutilisables, versionnables et traçables.

Conçu pour les ingénieurs en sécurité et les équipes DevSecOps, Medusa ne remplace pas les outils d'analyse statique ou de proxy, mais enrichit la couche d'observation du comportement runtime. Il permet de définir des scénarios de test déclaratifs, de combiner des modules d'injection logicielle et de produire des rapports d'audit détaillés. Voici comment il transforme la pratique de l'audit mobile.

2. Conception et architecture : résoudre les limites des approches manuelles

2.1 Anti-modèles courants et solutions de Medusa

Les approches manuelles avec Frida présentent des défis récurrents : duplication de code pour des cibles similaires (comme le contournement de SSL Pinning), perte du contexte d'exécution (timing de l'injection, état de l'application), résultats non structurés et difficulté de collaboration. Medusa y répond par des abstractions fondamentales :

  • Test Case (Scénario de test) : Fichier YAML déclaratif définissant les conditions d'exécution, les actions à effectuer et les assertions sur les résultats. Il intègre une logique contextuelle (ciblage par nom de paquet, version SDK).
  • Module : Unité logicielle autonome contenant la logique Frida JavaScript, avec une interface d'entrée/sortie définie. Il évite la pollution de l'espace de noms global.
  • Engine (Moteur) : Composant chargeant les modules, gérant l'injection, la gestion d'erreurs et la collecte de logs. Il expose des primitives comme hook_method configurables via le YAML.
  • Report (Rapport) : Sortie structurée (HTML/JSON) liant chaque entrée de log à sa configuration source, incluant des métriques d'exécution et des extraits de mémoire pertinents.

2.2 Organisation des ressources

La structure standard d'un projet Medusa sépare clairement les préoccupations :


medusa/
├── config/                 # Paramètres globaux de connexion et logging
├── modules/                # Bibliothèque de modules réutilisables
│   ├── network/            # Modules liés au réseau
│   ├── crypto/             # Modules d'extraction cryptographique
│   └── storage/            # Modules de surveillance du stockage
├── testcases/              # Scénarios de test YAML
│   ├── android/
│   └── ios/
├── reports/                # Répertoire des rapports générés
└── medusa.py               # Point d'entrée principal

Cette architecture favorise la réutilisasion et la maintenance. Les modules peuvent être partagés via un système de contrôle de version et être composés dans différents scénarios de test.

2.3 Approche déclarative vs. impérative

La différence fondamentale réside dans le paradigme de programmation. Alors que les scripts Frida directs nécessitent une écriture impérative détaillant chaque étape, Medusa adopte une approche déclarative où l'on spécifie l'objectif souhaité. Le moteur se charge de l'implémentation, offrant une meilleure gestion des erreurs et un cycle de vie des hooks contrôlé par des événements (on_app_start, on_activity_resume).

Exemple simplifié de déclaration pour contourner la fixation de certificat OkHttp :


name: "Contournement fixation OkHttp"
target:
  package: "com.exemple.*"
  min_sdk: 21
actions:
  - hook_method:
      module: "network/neutralisation_pin_okhttp"
      args:
        classe_cible: "okhttp3.CertificatePinner"
        nom_methode: "check"
      evenement_declenchement: "on_app_start"
assertions:
  - journal_contient: "Contournement actif pour l'hôte:"

Ce fichier YAML constitue un contrat de test. La logique de contournement réside dans le module référencé, qui est testable et maintenable indépendamment.

3. Mise en œuvre pratique : d'une configuration à un rapport auditable

3.1 Préparation de l'environnement

L'installation requiert des versions spécifiques pour assurer la compatibilité. Par exemple, Frida Server version 15.1.17 est recommandée pour sa stabilité sur Android 12+ avec l'API Java.choose. L'environnement Python doit être propre, idéalement dans un environnement virtuel, avec les dépendances frida et frida-tools installées en versions synchronisées.

Après l'installation de Medusa, le fichier de configuraton config/default.yml doit être renseigné avec l'identifiant de l'appareil cible (obtenu via adb devices).

3.2 Exécution d'un scénario et analyse des résultats

L'exécution d'un test se lance via la commande principale, en spécifiant le scénario, le paquet cible et le répertoire de sortie pour le rapport. Le processus :

  1. Spawn (démarrage) ou attachement au processus de l'application cible.
  2. Injection des modules selon la séquence définie dans le scénario YAML.
  3. Collecte structurée des logs et génération du rapport HTML.

Le rapport généré fournit une vue d'ensemble de l'exécution : temps total, taux de réussite des hooks, nombre de déclenchements, et surtout, la journalisation complète avec un lien vers la ligne YAML correspondante. Les captures de mémoire ciblées (comme le contenu d'un certificat au moment d'un hook) sont incluses pour l'audit.

3.3 Débogage et gestion des problèmes courants

Lors d'échecs (comme un timeout de Java.choose), la structure de Medusa aide au diagnostic. Le rapport indique précisément quel module ou quelle action a échoué. Les causes fréquentes incluent une version de Frida Server incompatible, un timing d'injection incorrect ou une classe cible masquée (obfuscée). Ajuster le timeout de spawn, changer le mode d'attache ou vérifier la configuration de l'appareil font partie des étapes de résolution systématisées par le framework.

4. Approfondissement : anatomie d'un module et ses limites

4.1 Principes de conception d'un module de contournement

Prenons l'exemple d'un module destiné à neutraliser la fixation de certificat. Une implémentation robuste doit anticiper les différentes couches de défense qu'une application pourrait déployer (implémentations OkHttp différentes, validateurs personnalisés, bibliothèques natives). Le module tente séquentiellement plusieurs points d'injection connus et journalise celle qui fonctionne. Cette approche par défaut rend le module résilient aux variations d'implémentation.


// Structure logique d'un module (pseudo-code)
function neutraliserFixationCertificat(config) {
    // Tentative 1: Hook sur CertificatePinner.check (OkHttp standard)
    try {
        const ClassePinner = Java.use(config.classe_cible);
        const methode = ClassePinner[config.nom_methode];
        if (methode) {
            methode.overload(...config.surcharges).implementation = function(hote, certificats) {
                // Journaliser et retourner sans lever d'exception
                console.log('[MODULE] Neutralisation active: ' + hote);
                return;
            };
            return { statut: 'succes', couche: 'primaire' };
        }
    } catch (e) {
        console.log('[MODULE] Echec tentative primaire: ' + e.message);
    }
    // Tentative 2: Fallback vers d'autres mécanismes (HostVerifier, etc.)
    // ...
    return { statut: 'echec', raison: 'Aucun point d\'hook applicable' };
}

4.2 Paramétrage fin des actions

Les paramètres dans la section args du YAML ont une signification pratique directe :

  • surcharges (overload) : Signature complète de la méthode à hooker. Doit correspondre exactement à l'implémentation dans l'APK cible, nécessitant parfois une analyse préalable avec javap.
  • delai_attente (timeout) : Durée maximale pour une action d'injection. Doit être calibrée entre la réactivité et la tolérance aux applications lentes à charger.
  • evenement_declenchement (on_event) : Événement du cycle de vie Android déclenchant l'injection (ex: on_app_start), crucial pour éviter les hooks sur des classes non encore chargées.

4.3 Scénarios hors périmètre et stratégies alternatives

Medusa excelle dans l'instrumentation Java de haut niveau, mais ses limites doivent être comprises :

Scénario d'échec Cause Approche complémentaire
Utilisation d'un TrustManager personnalisé sans passer par les API OkHttp La cible d'injection standard est absente Utiliser un module ciblant directement X509TrustManager.checkServerTrusted
Pile SSL implémentée en natif (ex: BoringSSL via JNI) Les hooks Java sont inopérants Requiert une instrumentation native avec Interceptor.attach sur les fonctions C
Mécanismes de détection d'instrumentation actifs Le framework d'injection lui-même est ciblé Nécessite des techniques d'évasion spécifiques en dehors du cadre de Medusa

Les échecs documentés dans les rapports constituent une base de diagnostic essentielle pour définir la stratégie d'audit suivante.

5. Extension du framework : création de modules personnalisés

5.1 Exemple : détection de tokens en mémoire en clair

Un besoin courant est de vérifier si des jetons d'authentification (tokens) sont présents en mémoire sous forme non chiffrée. Cela implique de surveiller la création d'objets String dont le contenu correspond à des motifs de tokens (JWT, jetons d'accès).

Un module personnalisé pourrait :

  1. Hooker le constructeur String(char[]).
  2. Filtrer les chaînes correspondant à des expressions régulières (ex: structure JWT).
  3. Journaliser le contenu suspect ainsi qu'un extrait de la pile d'appels pour le contexte.

// Pseudo-code pour un module de surveillance mémoire
function surveillerTokensEnMemoire(parametres) {
    const expressionReguliere = new RegExp(parametres.expression || "eyJ[a-zA-Z0-9_-]+\\.", "i");
    const StringClass = Java.use("java.lang.String");
    
    StringClass.$init.overload('[C').implementation = function(caracteres) {
        const chaine = String.fromCharCode.apply(null, caracteres);
        if (expressionReguliere.test(chaine)) {
            // Récupération de la pile d'appels pour analyse
            const pile = Java.use("java.lang.Exception").$new().getStackTrace();
            console.log('[SURVEILLANCE] Token potentiel détecté: ' + chaine.substring(0, 80) + '...');
            // Journalisation de la pile (tronquée)
        }
        this.$init(caracteres);
    };
}

Ce module peut ensuite être intégré dans un scénario de test YAML ciblé, démontrant l'extensibilité de Medusa pour des besoins spécifiques.

Étiquettes: Medusa Frida Android audit de sécurité analyse dynamique

Publié le 30 juin à 20h29