Analyse dynamique des algorithmes de chiffrement iOS avec Frida : Observation de CCCrypt et AES-GCM en temps réel

L'impératif de l'analyse dynamique pour la sécurité mobile

L'audit de sécurité des applications iOS nécessite souvent d'aller au-delà de la simple lecture du code source ou du binaire statique. Dans un écosystème où les bibliothèques sont optimisées, les symboles dépouillés et les données souvent chiffrées avant même d'atteindre la couche réseau, l'instrumentation dynamique devient l'outil de choix. L'objectif n'est pas de contourner des protections, mais de valider des implémentations cryptographiques : un vecteur d'initialisation (IV) est-il réelelment aléatoire ? La clé AES est-elle dérivée de manière sécurisée ou codée en dur ?

Frida permet d'intervenir chirurgicalement dans l'espace mémoire d'un processus en cours d'exécution. Contrairement à l'analyse statique avec Hopper ou IDA Pro, qui montre une structure théorique, Frida révèle le comportement réel des données, capturant les clés et les messages en clair juste avant leur transformation.

Pourquoi l'analyse statique échoue face au chiffrement moderne

Optimisations Swift et Inlining

Avec le compilateur Swift et les optimisations de type -Owholemodule, des appels comme CryptoKit.AES.GCM.seal() sont souvent intégrés directement (inlined) dans les fonctions appelantes. Un analyste se retrouve face à des centaines de lignes d'instructions ARM64 complexes où les registres manipulent des pointeurs éphémères. Identifier la source exacte d'une clé devient virtuellement impossible sans observer les registres en temps réel.

Masquage des symboles système

Bien que les API comme CCCrypt fassent partie de CommonCrypto, les applications en production suppriment généralement leurs tables de symboles. Une recherche via nm sur le binaire ne renverra souvent aucun résultat pour les fonctions de chiffrement standard. Frida outrepasse cette restriction en se basant sur les adresses mémoire des modules chargés dynamiquement (libcommonCrypto.dylib par exemple).

Méthodologie de localisation des points d'ancrage (Hooks)

Une approche structurée permet d'identifier rapidement où se situe la logique de chiffrement sans examiner chaque fonction du binaire.

1. Surveillance des interfaces d'E/S

Le chiffrement sert presque toujours à protéger les données sortantes ou persistantes. Il faut donc surveiller les points de sortie :

  • Réseau : Hookez URLSession.dataTask(with:completionHandler:). Si le corps de la requête (HTTP body) est illisible mais possède une structure fixe, le chiffrement a eu lieu juste avant cet appel.
  • Stockage : Surveillez NSUserDefaults.set(_:forKey:) ou les écritures via FileManager.

2. Identification par empreinte de données

Le format du texte chiffré donne des indices sur l'algorithme utilisé :

Structure des données Algorithme probable Méthode de validation Frida
Multiple de 16 octets, entropie élevée AES-CBC / PKCS#7 Inspecter les 16 derniers octets pour le padding.
12 octets de préfixe distincts + données AES-GCM Vérifier si les 12 premiers octets changent à chaque appel (IV).
Blocs de 256 ou 512 octets RSA-2048 / 4096 Rechercher des structures ASN.1 dans les clés en mémoire.

Mise en œuvre technique : Hooking de la couche CommonCrypto

Une fois le flux de données identifié, l'étape suivante consiste à intercepter l'appel bas niveau. Voici un exemple de script Frida pour capturer les paramètres de CCCrypt, l'une des fonctions les plus utilisées sur iOS.


// Ciblage de la fonction CCCrypt dans la bibliothèque système
const cccrypt_ptr = Module.findExportByName('libcommonCrypto.dylib', 'CCCrypt');

if (cccrypt_ptr) {
    Interceptor.attach(cccrypt_ptr, {
        onEnter: function (args) {
            this.operation = args[0].toInt32(); // 0 = Encrypt, 1 = Decrypt
            this.algorithm = args[1].toInt32(); // 0 = AES
            this.keyPtr = args[3];
            this.keyLen = args[4].toInt32();
            this.ivPtr = args[5];

            console.log('--- CCCrypt Intercepté ---');
            console.log('Opération: ' + (this.operation === 0 ? 'Chiffrement' : 'Déchiffrement'));
            console.log('Algorithme: ' + this.algorithm);
            console.log('Longueur Clé: ' + this.keyLen);
            console.log('Clé (Hex): ' + hexdump(this.keyPtr, { length: this.keyLen }));
            
            if (!this.ivPtr.isNull()) {
                console.log('IV (Hex): ' + hexdump(this.ivPtr, { length: 16 }));
            }
        },
        onLeave: function (retval) {
            // Analyse du résultat après exécution
            console.log('Statut de retour: ' + retval);
        }
    });
}

Étude de cas : Audit d'un protocole de messagerie sécurisée

Lors d'une mission d'audit sur une application de chat, l'analyse réseau montrait des messages chiffrés dont la longueur variait étrangement d'un appareil à l'autre pour un même contenu. En utilisant Frida pour remonter la pile d'exécution (backtrace) à partir de CCCrypt, nous avons découvert le problème suivant :


// Extraction de la pile d'appel pour localiser l'origine
onEnter: function(args) {
    console.log('Pile d\'appel :\n' + Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress).join('\n'));
}

La pile a révélé que la clé AES était générée en combinant l'identifiant unique de l'appareil (UDID) et un horodatage. En inspectant l'IV via Frida, il est apparu que les 4 derniers octets étaient systématiquement dérivés des secondes de l'horodatage système. Cette faille permettait une attaque par dictionnaire sur l'IV, réduisant drastiquement l'espace de recherche pour un attaquant connaissant l'heure approximative d'envoi du message.

Adaptation aux architectures arm64e (iOS 16+)

Les processeurs récents utilisent le Pointer Authentication Code (PAC) pour sécuriser les pointeurs de fonction. Un hook classique via Interceptor.attach peut parfois déclencher un crash de type EXC_BAD_ACCESS si le pointeur est mal manipulé.

Pour ces environnements, il est préférable d'utiliser Interceptor.replace en créant un NativeCallback. Cette méthode préserve l'intégrité de la signature PAC lors de l'appel de la fonction originale.


const targetAddr = Module.findExportByName(null, 'CCCrypt');
const originalImplementation = new NativeFunction(targetAddr, 'int', ['int', 'int', 'int', 'pointer', 'int', 'pointer', 'pointer', 'int', 'pointer', 'int', 'pointer']);

Interceptor.replace(targetAddr, new NativeCallback(function (op, alg, opts, key, kLen, iv, dataIn, dInLen, dataOut, dOutLen, outMoved) {
    console.log('[PAC] Appel sécurisé à CCCrypt détecté');
    
    // Logique personnalisée ici...

    // Appel de l'implémentation originale
    return originalImplementation(op, alg, opts, key, kLen, iv, dataIn, dInLen, dataOut, dOutLen, outMoved);
}, 'int', ['int', 'int', 'int', 'pointer', 'int', 'pointer', 'pointer', 'int', 'pointer', 'int', 'pointer']));

Automatisation de l'analyse avec un scanner d'entropie

Pour gagner en efficacité, un ingénieur peut injecter un script capable de scanner les zones mémoires à la recherche de données à haute entropie, typiques des clés cryptographiques, juste avant les appels système critiques.


function checkEntropy(ptr, length) {
    let buf = ptr.readByteArray(length);
    let bytes = new Uint8Array(buf);
    let frequencies = {};
    for (let b of bytes) {
        frequencies[b] = (frequencies[b] || 0) + 1;
    }
    let entropy = 0;
    for (let f in frequencies) {
        let p = frequencies[f] / length;
        entropy -= p * Math.log2(p);
    }
    return entropy;
}

// Utilisation : si entropy > 7.5, il s'agit probablement d'une clé ou d'un flux chiffré

Cette approche permet de distinguer automatiquement un buffer contenant du texte brut (entropie faible) d'un buffer contenant une clé AES ou un IV (entropie proche de 8). En intégrant cette logique dans les hooks de CCCrypt ou CommonHMAC, on élimine les faux positifs générés par les appels système internes.

Étiquettes: Frida ios-security cryptography reverse-engineering ARM64

Publié le 3 juillet à 23h25