Skip to content

Leçon 8.2 · Encodage, crypto & signatures· 50 min

Identifier les algorithmes cryptographiques

Distinguez chiffrement et encodage, identifiez AES, RC4, ChaCha20, TEA, hashs et CRC32 par constantes, boucles et imports, puis trouvez la clé et l’IV.

Objectifs

  • Distinguer chiffrement et encodage à l’aide de la dépendance à la clé, de l’entropie et de la longueur
  • Reconnaître les algorithmes courants à partir de leurs constantes, de l’organisation de leurs tables et de la structure de leurs boucles
  • Identifier l’usage de la cryptographie à partir des imports CryptoAPI et CNG et des bibliothèques liées statiquement
  • Utiliser capa, FindCrypt, signsrch et les signatures crypto YARA, et expliquer ce que chacun peut manquer
  • Localiser la clé, l’IV et le mode dans le code ou en mémoire, et les rapporter de façon exploitable

Reconnaître XOR, Base64 et les encodages personnalisés traitait de transformations que l’on peut défaire à force d’observation. Les vrais chiffrements sont différents : sans la clé, leur sortie est conçue pour être indiscernable de l’aléatoire, et aucune analyse de fréquence n’y changera rien. Mais le malware (logiciel malveillant) doit transporter ou dériver sa clé quelque part où vous pouvez l’atteindre, et les algorithmes de chiffrement comptent parmi le code le plus facile à identifier, car ils regorgent de constantes fixes.

Votre travail ici n’est pas la cryptanalyse. Il s’agit de répondre rapidement à quatre questions : quel algorithme, dans quel mode, avec quelle clé et quel IV, et d’où viennent-ils ? Ces réponses permettent à quelqu’un d’écrire un déchiffreur, un extracteur de configuration ou une signature réseau.

Encodage ou chiffrement ?

Avant de nommer un algorithme, déterminez à quelle catégorie vous avez affaire. La distinction est pratique, pas académique :

QuestionEncodageChiffrement
La sortie dépend-elle d’une clé secrète ?Non, ou d’une clé trivialement courteOui, généralement de 128 à 256 bits
Entropie du texte encodéProche de celle du texte clair, ou plafonnée (hex 4, Base64 6)Proche de 8 bits par octet, quelle que soit l’entrée
Un texte clair connu révèle-t-il la clé ?Généralement, par simple XOR ou soustractionNon, pour tout chiffrement moderne
Longueur de la sortieIdentique, ou selon un ratio fixe (4/3 pour Base64)Identique (chiffrement par flot) ou arrondie à 8 ou 16 octets (chiffrement par bloc)
Taille du codeQuelques instructions dans une boucleTables, nombreux tours, cadencement de clé

Deux cas limites comptent. RC4 est un vrai chiffrement, mais assez petit pour ressembler à un décodeur. Et une clé XOR répétée de 16 octets reste un encodage, aussi aléatoire qu’elle paraisse : si ciphertext XOR known plaintext se répète, c’est du XOR, pas de l’AES.

Astuce : Une longueur de texte chiffré toujours multiple de 16 indique un chiffrement par bloc de 128 bits (AES), et un multiple de 8 un chiffrement de 64 bits (DES, 3DES, Blowfish, TEA). Des blocs de 16 octets identiques au sein d’un même texte chiffré suggèrent le mode ECB, qui laisse fuir les blocs de texte clair répétés.

Reconnaître les algorithmes à leurs constantes

La plupart des algorithmes ont besoin de nombres fixes : S-boxes, valeurs initiales de hash, constantes de tour ou mots magiques, stockés sous forme de tables dans .rdata ou de valeurs immédiates dans le code. Cherchez dans les deux ordres d’octets, car x86 stocke les mots de 32 bits en little-endian (voir endianness).

AlgorithmeConstante à rechercherRemarques
AESS-box 63 7C 77 7B F2 6B 6F C5 ... ; S-box inverse 52 09 6A D5 ... ; constantes de tour 01 02 04 08 10 20 40 80 1B 36Les implémentations rapides utilisent à la place quatre « T-tables » de 1 Ko ; les instructions AESENC/AESDEC n’ont besoin d’aucune table
MD50x67452301 0xEFCDAB89 0x98BADCFE 0x10325476 ; constantes par étape commençant par 0xD76AA478SHA-1 partage les quatre premiers mots
SHA-1Les mots de MD5 plus 0xC3D2E1F0 ; constantes de tour 0x5A827999 0x6ED9EBA1 0x8F1BBCDC 0xCA62C1D6
SHA-256Valeurs initiales 0x6A09E667 0xBB67AE85 ... ; 64 constantes de tour commençant par 0x428A2F98SHA-224 et SHA-512 ont leurs propres tables
CRC32Polynôme 0xEDB88320 (réfléchi) ou 0x04C11DB7 ; table commençant par 0x00000000 0x77073096 0xEE0E612CUne somme de contrôle, pas de la crypto, mais utilisée pour le hachage d’API et les contrôles d’intégrité
RC4Aucune constanteIdentifié par sa structure (ci-dessous)
ChaCha20 / Salsa20"expand 32-byte k" (mots 0x61707865 0x3320646E 0x79622D32 0x6B206574)"expand 16-byte k" pour les clés de 128 bits
TEA / XTEADelta 0x9E3779B9, ou 0x61C88647 lorsque le compilateur transforme sum -= delta en additionLe déchiffrement TEA part souvent de 0xC6EF3720 (32 tours fois delta)
BlowfishTableau P commençant par 0x243F6A88 0x85A308D3 (décimales de pi)Quatre S-boxes de 1 Ko suivent
RSAExposant public 0x10001 ; magic de blob RSA1/RSA2 ; en-têtes PEM -----BEGINL’algorithme lui-même est de l’arithmétique sur grands nombres

Une constante qui correspond est une piste, pas un verdict : 0x67452301 seul peut désigner MD5, SHA-1 ou RIPEMD-160. Cherchez la table complète ou plusieurs constantes dans une même fonction.

Structure : RC4, grands nombres et compagnie

RC4 n’a pas de constantes, mais il a une forme caractéristique. Le cadencement de clé (KSA) remplit un tableau de 256 octets avec les valeurs 0 à 255, puis exécute une seconde boucle de 256 itérations qui échange des paires d’entrées sous le contrôle de la clé. Le générateur (PRGA) est une courte boucle qui incrémente i, ajoute S[i] à j, échange de nouveau et applique un XOR entre les données et S[(S[i] + S[j]) & 0xFF]. Deux boucles de 256 itérations consécutives, l’une avec une seule écriture d’octet et l’autre avec deux, plus un usage intensif de movzx pour le & 0xFF : c’est du RC4 jusqu’à preuve du contraire.

Le code RSA et à courbes elliptiques n’a pas de S-boxes, seulement des boucles sur des tableaux de limbs (mots) de 32 ou 64 bits utilisant mul, adc et sbb, plus une exponentiation par carré-multiplication ou une réduction de Montgomery. Des constantes de courbe comme le nombre premier de P-256 ou 121665 (Curve25519) nomment la courbe. Les malwares écrivent rarement ce code à la main ; ils utilisent CryptoAPI, CNG ou une bibliothèque liée.

AES avec AES-NI n’a pas de table non plus : cherchez aesenc, aesenclast et aeskeygenassist dans les registres SIMD.

Reconnaître les algorithmes à leurs imports et bibliothèques

Le cas le plus simple est celui du malware qui demande à Windows de faire la crypto : les séquences d’appels sont reconnaissables et les paramètres nomment l’algorithme.

Famille d’APISéquence typiqueOù l’algorithme est nommé
CryptoAPI (advapi32)CryptAcquireContext, CryptImportKey ou CryptCreateHash + CryptHashData + CryptDeriveKey, puis CryptEncrypt / CryptDecryptConstantes ALG_ID : CALG_RC4 0x6801, CALG_AES_128 0x660E, CALG_AES_256 0x6610, CALG_MD5 0x8003, CALG_SHA1 0x8004, CALG_SHA_256 0x800C
CNG (bcrypt)BCryptOpenAlgorithmProvider, BCryptSetProperty, BCryptGenerateSymmetricKey ou BCryptImportKeyPair, BCryptEncrypt / BCryptDecryptChaînes UTF-16 telles que L"AES", L"RSA", L"SHA256" et L"ChainingModeCBC"

Si ces API sont résolues dynamiquement, la table des imports ne les montrera pas. Cherchez les noms de DLL et de fonctions sous forme de chaînes, ou de hashs (voir hachage d’API), et revenez à la leçon Lire les capacités à partir des imports pour la méthode générale.

Les bibliothèques liées statiquement constituent l’autre cas courant. OpenSSL, mbedTLS, Crypto++, libsodium, les paquets crypto/* de Go et les crates Rust apportent leurs propres chaînes d’erreur et de version (en Go, les chemins de paquets survivent au strip). Identifiez la bibliothèque, renommez ses fonctions et ne la rétro-ingéniérez pas ; concentrez-vous sur l’appelant. FLIRT dans IDA, Function ID dans Ghidra et les signatures de bibliothèques de capa automatisent cela.

Outils

OutilFonctionnementPoints fortsAngles morts
capaRègles sur des caractéristiques du désassemblage : constantes, boucles, appels d’API, mnémoniquesNomme l’algorithme et la fonction ; KSA/PRGA de RC4 par la structureDu code optimisé ou inhabituel peut échapper à la structure d’une règle
FindCrypt (versions Ghidra et IDA)Parcourt le programme chargé à la recherche de tables de constantes connues et les étiquetteMontre les références croisées de la table, qui mènent au codeSeulement les constantes, pas la structure
signsrchRecherche par signatures d’octets de milliers de tables de crypto et de compressionCouverture très large, y compris d’algorithmes obscursOffsets bruts uniquement ; bruyant sur les gros fichiers
Signatures crypto YARAChaînes hexadécimales pour les tables et valeurs magiques (par exemple le jeu crypto_signatures.yar de Yara-Rules)Rapide, scriptable, fonctionne sur les dumps mémoireComme signsrch ; c’est vous qui maintenez les règles

Exécutez ensemble un scanner de constantes et capa : une correspondance de table vous dit quoi, et les références croisées ou l’adresse de fonction donnée par capa vous disent où.

Attention : Les constantes peuvent être cachées. Les auteurs calculent les tables à l’exécution, les stockent encodées par XOR ou modifient une constante (un delta TEA modifié, une S-box non standard). Un outil qui ne trouve « rien » a seulement prouvé que les tables standard ne sont pas stockées en clair.

Trouver la clé et l’IV

Une fois la fonction connue, la clé est l’un de ses arguments. Remontez depuis le site d’appel :

  1. Suivez les arguments. Dans l’ABI Windows x64, les quatre premiers arguments sont dans rcx, rdx, r8 et r9. Un lea rcx, [rip+...] pointant dans .rdata ou .data, suivi d’une petite longueur constante dans rdx, correspond souvent à la clé et à sa taille. Pour les appels d’API, la clé est le blob passé à CryptImportKey ou le tampon passé à BCryptGenerateSymmetricKey.
  2. Vérifiez s’il y a dérivation. La valeur stockée peut être un mot de passe haché au préalable (CryptCreateHash + CryptDeriveKey, PBKDF2 ou une routine maison). Rapportez le secret stocké et la dérivation, car un déchiffreur a besoin des deux.
  3. Trouvez l’IV et le mode. Cherchez un tampon de 16 octets passé avec la clé, un appel à BCryptSetProperty nommant le mode de chaînage, ou un CryptSetKeyParam avec KP_IV ou KP_MODE. L’IV est souvent placé en tête de chaque texte chiffré ; vérifiez donc si les 16 premiers octets de chaque message sont consommés séparément.
  4. Passez en dynamique quand le statique est difficile. Dans un débogueur, placez un point d’arrêt sur la fonction de déchiffrement, CryptImportKey ou BCryptGenerateSymmetricKey et lisez la clé dans les arguments. Dans les dumps mémoire, aeskeyfind recherche les cadencements de clé AES étendus.

Confirmez toujours une clé récupérée en déchiffrant des données réelles avec une implémentation indépendante (le paquet Python cryptography ou CyberChef). Une sortie identique est la seule preuve.

Que rapporter

Une découverte crypto doit permettre à un collègue de reproduire le déchiffrement sans ouvrir l’échantillon :

ChampExemple
Algorithme et varianteRC4 ; AES-256 ; ChaCha20 avec nonce de 12 octets ; CRC32 standard
Mode et paddingCBC avec PKCS#7 ; CTR ; sans objet pour les chiffrements par flot
FinalitéDéchiffrement de configuration, trafic C2, déchiffrement de chaînes, chiffrement de fichiers
ImplémentationEn ligne (adresse de la fonction), CryptoAPI, CNG, bibliothèque liée statiquement et sa version
Emplacement de la cléRVA de la clé, site d’appel qui la passe, ou mode de dérivation
Matériel de cléLa clé et l’IV en hexadécimal, s’ils ont été récupérés et peuvent être partagés sans risque
PreuvesConstantes correspondantes, règles capa, sortie des outils, déchiffrement de test vérifié

Si la clé est générée pour chaque victime et enveloppée par une clé publique RSA embarquée, comme dans beaucoup de ransomwares, dites-le : cela détermine ce que les équipes de réponse peuvent récupérer.

Lab : trouver RC4 et CRC32 dans un binaire

Vous allez compiler un programme inoffensif contenant un RC4 écrit à la main et un CRC32 fondé sur une table, puis les retrouver tous les deux avec un scan de constantes YARA, un scanner d’idiomes Capstone et capa. Il vous faut mingw-w64 et un environnement virtuel avec yara-python, capstone, pefile et, éventuellement, flare-capa.

  1. Compilez le programme. Enregistrez sous cryptolab.c. La table CRC32 est précalculée, comme le fait zlib ; le listing ci-dessous la tronque après les premières entrées, générez donc la table complète de 256 entrées avec quelques lignes de Python (l’algorithme réfléchi standard avec le polynôme 0xEDB88320) et collez-la.

    c
    // cryptolab.c - RC4 and a table-driven CRC32 over a demo string (benign lab program)
    #include <stdio.h>
    #include <string.h>
    #include <stdint.h>
    
    /* Precomputed like zlib's crc32.c: reflected polynomial 0xEDB88320 */
    static const uint32_t crc_table[256] = {
        0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
        /* ... 250 more entries ... */
    };
    
    static uint32_t crc32(const unsigned char *p, size_t n) {
        uint32_t c = 0xFFFFFFFFu;
        while (n--)
            c = crc_table[(c ^ *p++) & 0xFF] ^ (c >> 8);
        return c ^ 0xFFFFFFFFu;
    }
    
    static void rc4(const unsigned char *key, size_t klen, unsigned char *buf, size_t n) {
        unsigned char S[256];
        for (int i = 0; i < 256; i++)               /* KSA: identity permutation */
            S[i] = (unsigned char)i;
        for (int i = 0, j = 0; i < 256; i++) {      /* KSA: key-driven swaps */
            j = (j + S[i] + key[i % klen]) & 0xFF;
            unsigned char t = S[i]; S[i] = S[j]; S[j] = t;
        }
        for (size_t k = 0, i = 0, j = 0; k < n; k++) {  /* PRGA */
            i = (i + 1) & 0xFF;
            j = (j + S[i]) & 0xFF;
            unsigned char t = S[i]; S[i] = S[j]; S[j] = t;
            buf[k] ^= S[(S[i] + S[j]) & 0xFF];
        }
    }
    
    int main(void) {
        static const unsigned char key[] = "lab-rc4-key";
        unsigned char msg[] = "server=update.example.com;port=8080;id=lab";
        size_t n = strlen((char *)msg);
    
        printf("crc32(plain) = %08x\n", crc32(msg, n));
        rc4(key, sizeof(key) - 1, msg, n);
        printf("rc4 first bytes = %02x %02x %02x %02x\n", msg[0], msg[1], msg[2], msg[3]);
        rc4(key, sizeof(key) - 1, msg, n);
        printf("round trip: %s\n", msg);
        return 0;
    }
    bash
    x86_64-w64-mingw32-gcc -O1 -s -o cryptolab.exe cryptolab.c
    x86_64-w64-mingw32-gcc -O2 -s -o cryptolab_O2.exe cryptolab.c

    Compilé nativement (clang -O2 cryptolab.c) et exécuté, il affiche :

    text
    crc32(plain) = 65f3938f
    rc4 first bytes = 7b 88 57 44
    round trip: server=update.example.com;port=8080;id=lab
  2. Recherchez les constantes avec YARA. Enregistrez un petit jeu de règles sous crypto_consts.yar. Chaque règle est un fragment d’une table de la section sur les constantes ci-dessus :

    text
    rule CRC32_table_reflected {
        strings:
            $t = { 00 00 00 00 96 30 07 77 2C 61 0E EE BA 51 09 99 }
        condition:
            $t
    }
    rule CRC32_poly_immediate {
        strings:
            $p = { 20 83 B8 ED }            // 0xEDB88320 as a little-endian dword
        condition:
            $p
    }
    rule SHA256_initial_values {
        strings:
            $h = { 67 E6 09 6A 85 AE 67 BB 72 F3 6E 3C 3A F5 4F A5 }
        condition:
            $h
    }
    rule AES_forward_sbox {
        strings:
            $s = { 63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76 }
        condition:
            $s
    }
    rule ChaCha_Salsa_sigma {
        strings:
            $s = "expand 32-byte k"
        condition:
            $s
    }
    rule TEA_delta {
        strings:
            $d1 = { B9 79 37 9E }           // 0x9E3779B9
            $d2 = { 47 86 C8 61 }           // 0x61C88647 (= -0x9E3779B9)
        condition:
            any of them
    }

    Associez ensuite chaque correspondance à une section et à une adresse virtuelle avec scan_consts.py :

    python
    # scan_consts.py - run the crypto-constant YARA rules and map hits to PE sections
    import sys, pefile, yara
    
    rules = yara.compile(filepath="crypto_consts.yar")
    for path in sys.argv[1:]:
        pe = pefile.PE(path)
        data = open(path, "rb").read()
        print(f"== {path}")
        for m in rules.match(data=data):
            for s in m.strings:
                for inst in s.instances:
                    sec = next((x.Name.rstrip(b"\0").decode() for x in pe.sections
                                if x.PointerToRawData <= inst.offset < x.PointerToRawData + x.SizeOfRawData), "?")
                    rva = pe.get_rva_from_offset(inst.offset)
                    print(f"  {m.rule:24} {s.identifier:4} offset=0x{inst.offset:05x} "
                          f"va=0x{pe.OPTIONAL_HEADER.ImageBase + rva:x} {sec}")
    bash
    python scan_consts.py cryptolab.exe cryptolab_O2.exe
    text
    == cryptolab.exe
      CRC32_table_reflected    $t   offset=0x02460 va=0x140004060 .rdata
      CRC32_poly_immediate     $p   offset=0x02660 va=0x140004260 .rdata
    == cryptolab_O2.exe
      CRC32_table_reflected    $t   offset=0x02460 va=0x140004060 .rdata
      CRC32_poly_immediate     $p   offset=0x02660 va=0x140004260 .rdata

    La table est trouvée au même endroit dans les deux builds. Notez que la correspondance « polynôme » n’est pas une valeur immédiate dans le code : elle se trouve 0x200 octets après le début de la table, car l’entrée 128 de la table CRC32 réfléchie est égale au polynôme lui-même. Vérifiez toujours où tombe une correspondance. Aucune règle ne trouve RC4, car RC4 n’a pas de constantes.

  3. Trouvez RC4 par sa structure avec Capstone. Enregistrez sous find_rc4.py. Le script cherche des boucles courtes qui se terminent par un saut conditionnel arrière, comparent à 0xFF ou 0x100 et écrivent des octets isolés :

    python
    # find_rc4.py - flag tight 256-iteration loops that write bytes (RC4 KSA shape)
    import sys, pefile
    from capstone import Cs, CS_ARCH_X86, CS_MODE_64
    from capstone.x86 import X86_OP_IMM, X86_OP_MEM
    
    pe = pefile.PE(sys.argv[1])
    text = next(s for s in pe.sections if s.Name.startswith(b".text"))
    base = pe.OPTIONAL_HEADER.ImageBase + text.VirtualAddress
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True
    insns = list(md.disasm(text.get_data(), base))
    index = {i.address: n for n, i in enumerate(insns)}
    
    def byte_store(i):
        ops = i.operands
        return (i.mnemonic == "mov" and len(ops) == 2 and ops[0].type == X86_OP_MEM
                and ops[0].size == 1)
    
    loops = []
    for n, i in enumerate(insns):
        # a conditional jump backwards = the bottom of a loop
        if not i.mnemonic.startswith("j") or i.mnemonic == "jmp":
            continue
        tgt = i.operands[0].imm if i.operands[0].type == X86_OP_IMM else None
        if tgt is None or tgt >= i.address or tgt not in index:
            continue
        body = insns[index[tgt]:n + 1]
        if len(body) > 25:
            continue
        bound = [x for x in body if x.mnemonic == "cmp" and x.operands[-1].type == X86_OP_IMM
                 and x.operands[-1].imm in (0xFF, 0x100)]
        stores = [x for x in body if byte_store(x)]
        if bound and stores:
            loops.append((tgt, i.address, len(body), len(stores)))
    
    for start, end, size, nst in loops:
        print(f"loop 0x{start:x}-0x{end:x}: {size:2} insns, 256 iterations, {nst} byte store(s)")
    # RC4 KSA: an init loop (1 store) followed closely by a swap loop (>= 2 stores)
    for a, b in zip(loops, loops[1:]):
        if a[3] == 1 and b[3] >= 2 and b[0] - a[1] < 0x40:
            print(f"RC4 KSA candidate: init 0x{a[0]:x}, swap 0x{b[0]:x}")
            for x in insns[index[a[0]]:index[a[1]] + 1]:
                print(f"  {x.address:x}: {x.mnemonic:6} {x.op_str}")
    bash
    python find_rc4.py cryptolab.exe
    text
    loop 0x1400014e0-0x1400014ee:  5 insns, 256 iterations, 1 byte store(s)
    loop 0x140001500-0x140001539: 16 insns, 256 iterations, 2 byte store(s)
    RC4 KSA candidate: init 0x1400014e0, swap 0x140001500
      1400014e0: mov    byte ptr [rdx], al
      1400014e2: add    eax, 1
      1400014e5: add    rdx, 1
      1400014e9: cmp    eax, 0x100
      1400014ee: jne    0x1400014e0

    La boucle de cinq instructions correspond à S[i] = i, et la boucle d’échange suit immédiatement. Exécutez maintenant le même script sur cryptolab_O2.exe : il n’affiche rien. En -O2, GCC vectorise la boucle d’identité en instructions SSE qui remplissent seize octets par itération ; il ne reste donc plus de cmp eax, 0x100 à détecter. Gardez cela en tête chaque fois qu’une heuristique écrite à la main ne renvoie rien.

  4. Exécutez capa. Installé avec pip install flare-capa, capa 9.4 a besoin de ses règles (le dépôt capa-rules) et du répertoire sigs du dépôt capa, au même tag :

    bash
    capa -r capa-rules -s capa/sigs cryptolab.exe

    Réduit aux lignes pertinentes :

    text
    │ CRYPTOGRAPHY         │ Encrypt Data::RC4 [C0027.009]                         │
    │                      │ Encryption Key::RC4 KSA [C0028.002]                   │
    │                      │ Generate Pseudo-random Sequence::RC4 PRGA [C0021.004] │
    │ DATA                 │ Checksum::CRC32 [C0032.001]                           │
    │                      │ Encode Data::XOR [C0026.002]                          │
    ...
    │ hash data with CRC32                    │ data-manipulation/checksum/crc32   │
    │ encode data using XOR (2 matches)       │ data-manipulation/encoding/xor     │
    │ encrypt data using RC4 KSA              │ data-manipulation/encryption/rc4   │
    │ encrypt data using RC4 PRGA             │ data-manipulation/encryption/rc4   │

    Avec -vv, capa montre pourquoi. La règle RC4 KSA a reconnu la fonction 0x1400014C0 grâce à deux boucles serrées bornées par 0x100 (à 0x1400014E9 et 0x140001532, les mêmes boucles que votre script a trouvées) plus le motif movzx du « modulo 256 ». La règle CRC32 a reconnu les 32 premiers octets de la table. Exécutez aussi capa sur cryptolab_O2.exe : il signale toujours CRC32 et RC4 PRGA, mais « RC4 KSA » disparaît, pour la même raison que votre script est resté muet.

  5. Trouvez la clé au site d’appel. capa vous a donné la fonction RC4, 0x1400014c0. Regardez comment elle est appelée :

    bash
    x86_64-w64-mingw32-objdump -d -M intel cryptolab.exe | grep -B6 "call   0x1400014c0"
    text
    14000168b:	48 8d 3d be 29 00 00 	lea    rdi,[rip+0x29be]        # 0x140004050
    140001692:	49 89 d9             	mov    r9,rbx
    140001695:	49 89 f0             	mov    r8,rsi
    140001698:	ba 0b 00 00 00       	mov    edx,0xb
    14000169d:	48 89 f9             	mov    rcx,rdi
    1400016a0:	e8 1b fe ff ff       	call   0x1400014c0

    Le premier argument (rcx) vaut 0x140004050 et le second (rdx) 11 : une clé de 11 octets dans .rdata. Affichez-la avec x86_64-w64-mingw32-objdump -s -j .rdata cryptolab.exe et vous verrez lab-rc4-key à cette adresse, juste avant la table CRC32.

  6. Confirmez avec une implémentation indépendante. Enregistrez sous rc4_check.py :

    python
    # rc4_check.py - confirm the identification: same key, same keystream as the binary
    import pefile, zlib
    
    def rc4(key: bytes, data: bytes) -> bytes:
        S = list(range(256)); j = 0
        for i in range(256):
            j = (j + S[i] + key[i % len(key)]) & 0xFF
            S[i], S[j] = S[j], S[i]
        out, i, j = bytearray(), 0, 0
        for b in data:
            i = (i + 1) & 0xFF; j = (j + S[i]) & 0xFF
            S[i], S[j] = S[j], S[i]
            out.append(b ^ S[(S[i] + S[j]) & 0xFF])
        return bytes(out)
    
    pe = pefile.PE("cryptolab.exe")
    key = pe.get_data(0x4050, 11)            # RVA of the lea rcx target, length from edx=0xb
    msg = b"server=update.example.com;port=8080;id=lab"
    print("key       :", key)
    print("rc4 first :", rc4(key, msg)[:4].hex(" "))
    print("crc32     :", f"{zlib.crc32(msg):08x}")
    text
    key       : b'lab-rc4-key'
    rc4 first : 7b 88 57 44
    crc32     : 65f3938f

    Les deux valeurs correspondent à ce que le programme a affiché à l’étape 1. Vous avez maintenant identifié les deux algorithmes, localisé la clé et vérifié le résultat : tout ce que demande le tableau du rapport.

    Pour comparaison, refaites l’étape 5 sur cryptolab_O2.exe. Les deux appels à 0x1400014c0 ne passent plus que le tampon (lea rcx,[rsp+0x30]) et sa longueur (mov edx,0x2a). GCC a spécialisé la fonction pour son unique clé constante : l’adresse de la clé 0x140004050 et une multiplication par un nombre magique pour i % 11 ont été déplacées à l’intérieur. La clé est toujours là, mais vous la trouvez en lisant le corps de la fonction, pas le site d’appel.

Questions : Pourquoi le scan YARA a-t-il trouvé CRC32 mais pas RC4, et quelle approche a trouvé RC4 à la place ? Quelle règle capa a disparu en -O2, et qu’est-ce que cela vous apprend sur la confiance accordée à un seul outil ? Si la clé à 0x140004050 avait été encodée par XOR et décodée juste avant l’appel, quelles étapes de ce lab fonctionneraient encore et lesquelles remplaceriez-vous par un point d’arrêt dans le débogueur ? Rédigez le tableau « Que rapporter » pour ce binaire.

À retenir

  • Le chiffrement dépend d’une vraie clé et produit une sortie quasi aléatoire ; l’encodage, non. Un texte clair connu qui révèle une clé répétée signifie que vous êtes encore face à un encodage.
  • Les constantes identifient la plupart des algorithmes : S-boxes AES, valeurs initiales MD5/SHA, tables CRC32, la chaîne expand 32-byte k de ChaCha20 et le delta de TEA. Cherchez dans les deux ordres d’octets et vérifiez où tombe chaque correspondance.
  • RC4, RSA et AES-NI n’ont pas de tables révélatrices ; reconnaissez-les à leur structure. CryptoAPI et CNG nomment l’algorithme dans leurs paramètres, et les bibliothèques liées doivent être identifiées plutôt que rétro-ingéniérées.
  • capa, FindCrypt, signsrch et YARA se complètent, et tous peuvent manquer du code optimisé ou modifié. Confirmez chaque identification en déchiffrant avec une implémentation indépendante.
  • Rapportez l’algorithme, le mode, la finalité, l’implémentation, l’emplacement de la clé et le matériel de clé, avec les preuves qui les établissent.