Reverse de Crypt-o-Text v1.21 et v1.24

Par

CASIMIR

Extrait de la documentation de Savard Software: "En utilisant Crypt-o-Text, vous pouvez crypter votre message pour qu'il soit illisible... De plus, vous pouvez utiliser des mots de passe de telle sorte que le message ne puisse être lu que par votre destinataire..."

EN ÊTES-VOUS SÛRS ? ? ?

Note :

Le programme étudié dans ce fichier est COT.EXE v1.21 [349696, 09/10/96]. Le mécanisme de chiffrage est EXACTEMENT LE MÊME dans la dernière version (v1.24), la seule différence est que la v1.21 accepte jusqu'à 16 caractères pour les mots de passe, tandis que la v1.24 peut en accepter jusqu'à 45. MAIS cette différence de longueur ne remet pas en cause le principe de chiffrage si vous employez une "cracking approach" pour le casser au lieu d'une stratégie "Brute-force", comme nous verrons.

Amusez-vous bien... Et UTILISEZ PGP!!!

Commençons!

Lancez Winice à partit du DOS-prompt.
Lancez COT, entrez un message et encryptez le avec par exemple: "CASIMIR" :-)
 

1. Où est mon ENTRÉE ?

D'habitude, pour découvrir ce qu'un programme fait avec mon entrée (pwd, numéro d'enregistrement... Ce que vous voulez), je mets un breakpoint (BPX) aux fonctions suivantes : GetDlgItemText, GetDlgItemInt... et j'ai atterri droit dans la routine de contrôle de mon entrée.

Essayez : vous découvrirez qu'ELLE NE FONCTIONNE PAS dans ce cas. Je n'ai toujours pas trouvé comment COT fait exactement pour saisir les entrés. Aucune importance, car le programme beep stupidement quand l'entrée est fausse, aussi exécutons les actions suivantes :

Winice : BPX MESSAGEBEEP
COT : entrez à pwd faux
! Winice : F11 pour retourner à la fonction appelante

Note :

"Le !" utilisé comme préfixe signifie un break sec dans Winice quand, par exemple, un BPX a eu une touche. Aucun préfixe signifie que nous avons ouvert Winice en utilisant CTRL-D

Notre position actuelle est maintenant (*) :

CODE:OFFSET  0137:00434E22   JZ 00434E55               // 0 => good guy    
        434E24   PUSH 10                              // bad guy, let's beep him
        434E26   CALL USER32!MessageBeep             // code that triggered Winice 
        434E2B * PUSH 00

2. Vérification de la longueur du mot de passe

Jetons un regard dans le code (aux offsets un peu plus haut) et en mettant quelque BPX, nous trouvons ceci :

0137:00434CAA * CMP EAX,[EBP-20]  // EAX=bogus pwd lenght, [EBP-20]=good pwd
       434CAD   JNE 434E24                                          lenght (7)

Nous savons maintenant quelle doit être la longueur du mot de passe (si vous ne me croyez pas, essayez avec des pwd faux de tailles diverses)! Nous découvrirons plus tard à quel endroit il est caché dans le fichier crypté.

3. Damned, mais où est l'ÉCHO ?

Ne serait ce pas agréable si COT exécutait les actions suivantes :

1-récupère le pwd de l'utilisateur
2-encrypte le bon pwd stocké dans fichier crypté
3-les comparent

Et dans ce cas, s'il y a comparaison, on devrait trouver l'écho du bon pwd quelque part en mémoire... Donc trouvons où cette comparaison se fait.

Nous pouvons supposer que la comparaison a lieu entre CS:434CAD et CS:434E22.

En cherchant un peu, la séquence CMP se montre dans toute sa Gloire :
( Entrez "1212121" comme faux password)

 0137:00434E17   MOV EAX,[EBP-34]  -> 31.32.31.32.31.32.31 ("1212121")
        434E1A   MOV EDX,[EBP-38]  -> D2.54.D1.A5.C2.7A.26
        434E1D   CALL 00403700    // trace it 
        434E22   JZ 00434E55      // 0 => good guy      
        0137:00403700   PUSH BPX 
               .               .
        403729   MOV ECX,[ESI]   -> 31.32.31.32.31.32.31 ("1212121")
        40372B   MOV EBX,[EDI]   -> D2.54.D1.A5.C2.7A.26 // funny string
        40372D   CMP ECX,EBX

Oh mon Dieu! Il n'y a AUCUN écho du bon pwd en mémoire. Au lieu de "CASIMIR", nous obtenons cette drôle de série : D2.54.D1.A5.C2.7A.26. Mais si nous entrons un bon pwd, nous obtenons 43.41.53.49.4D.49.52 ("CASIMIR") au lieu de cette funny_string sans signification. Il me semble que l'auteur de COT n'a pas voulu que le bon pwd soit encrypté lors de l'entrée d'un faux pwd, cela aurait été une sacré brèche dans leur système de sécurité (et je ne serai pas en train d'écrire ce texte à trois heures du matin).

4. D'où cette drôle de string (funny_string ) peut elle venir?

C'est la question...

En mettant des douzaines de BPRS, je découvre finalement que cette string surgit par morceaux en mémoire à DS : [68FB41 68FB42]:

0137:00434D78    MOV EAX,100                
                 CALL 00402984
                 XOR EDX,EDX      // clean EDX                 
                 MOV DL,[EBP-3F]
                 XOR EAX,EDX      // EAX=6E  EDX=BC
                 MOV [EBP-3F],AL  // AL=D2 : 1° byte of funny_string 
                 MOV EAX,100                 
                 CALL 00402984
                 XOR EDX,EDX      // clean EDX                
                 MOV DL,[EBP-3E]
                 XOR EAX,EDX      // EAX=4A  EDX=1E
                 MOV [EBP-3E],AL  // AL=54 : 2° byte of funny_string
                 MOV EAX,100 
                 CALL 00402984
                 MOV EBX,EAX                 
                 XOR EAX,EAX      // clean EAX
                 MOV AL,[EBP-3D]
                 XOR EBX,EAX      // EBX=8A  EAX=5B
 0137:00434DB3   MOV [EBP-3D],BL  // BL=D1 : 3° byte de la funny_string

Cette routine est appelée plusieurs fois, avant que la funny_string ne soit crée. Nous obtenons quelques octets supplémentaires si la longueur du pwd n'est pas un multiple de 3 (par exemple : si pwd lengh t= 7, nous obtenons 7 octets valables + 2 octets inutiles). Maintenant essayons avec un pwd correct ("CASIMIR") :

     LEFT                 RIGHT               CHECK
     VECTOR               VECTOR              VECTOR 
     EAX=FF      xor      EDX=BC      =>      AL=43 ("C") 
     EAX=5F      xor      EDX=1E      =>      AL=41 ("A") 
     EBX=08      xor      EAX=5B      =>      BL=53 ("S")  
        .                    .                  .
        .                    .                  .
        .                    .                  .

... Et cetera. Seuls les vecteurs de GAUCHE changent (notre funny_string), ceux de DROITE restent les mêmes. La solution me semble assez évidente : les vecteusr de gauche sont utilisés comme fonctions d'entrée, tandis que les vecteurs de droite servent pour le bon pwd (stocké dans le fichier crypté, où ailleurs ? ? ?).

Alors le check_vector est construit par "XORization" des vecteurs de gauche et simplement comparé avec l'entrée.

               = > BONNE entrée : entrée = check_vector               
               = > MAUVAISE entrée : entrée! = check_vector

5. La Fabrication du vecteur gauche

Ne soyez pas si impatient et arrêtez de vous plaindre! Nous avons presque fini, COT est déjà, humm, mort à 25 % ...

BIEN, quel est le rôle que joue le pwd entré dans la fabrication du vecteur gauche ? Examinons les call mystérieux à 00402984. Voici ce a quoi nous arrivons :


1° call 0137:00402984   IMUL EDX,[0043902C],08088405  // [0043902C]=00000857
                 INC EDX                              // EDX=FF0505B3
                 MOV [0043902C],EDX                   // EDX=FF0505B4
                 MUL EDX                              // EDX=100*EDX  
                 MOV EAX,EDX                          // EDX=FF (higher-weight byte)
 0137:00402999   RET              2° call
 0137:00402984   IMUL EDX,[0043902C],08088405         // [0043902C]=FF0505B4
                 INC EDX                              // EDX=5FA9EC84
                 MOV [0043902C],EDX                   // EDX=5FA9EC85
                 MUL EDX                              // EDX=100*EDX    
                 MOV EAX,EDX                          // EDX=5F
 0137:00402999   RET

Nous traçons un troisième call juste pour le fun (et nous assurer il n'y a aucun blème) :

3° call
 0137:00402984   IMUL EDX,[0043902C],08088405  // [0043902C]=5FA9EC85
                 INC EDX                       // EDX=086E3299
                 MOV [0043902C],EDX            // EDX=086E329A
                 MUL EDX                       // EDX=100*EDX    
                 MOV EAX,EDX                   // EDX=08
 0137:00402999   RET            
 

Pseudo-code:
for(i=0;i<pwd_lenght;i++) { Seed = (08088405*Seed + 1); left_vector[i] = 100*Seed; } 

Question subsidiaire : qu'est ce qui a construit le Seed ?

6. Le Seed

Prêtez attention à l'adresse mémoire en DS:0043902C …

Cette adresse a-t-elle déjà été utilisée dans le code ? Oui Monsieur! Juste après la vérification de la taille du pwd [la partie 2 de ce fichier], nous avons :

 0137:00434CAA   CMP EAX,[EBP-20]                   
                 JNE 434E24                 
                 MOV EAX,[EBP-34]    -> "CASIMIR"
                 CALL 00433978
 0137:00434CBB   MOV [0043902C],EAX  // EAX=857

Le seed semble venir du call en 433978. Comme d'habitude, nous prenons notre mitrailleuse et allons voir cela de plus prêt :

 0137:00433978   PUSH EBP  
                 XOR EBX,EBX  // EBX
        4339A2   MOV EDX,EAX  // pwd_lenght (7)
                 TEST EDX,EDX
                 JLE 004339CC
                 MOV EAX,00000001
 *****> 4339AD   MOV ECX,[EBP-04]
 *               MOVZX ECX,BYTE PTR[ECX+EAX-01] // ECX=43,41,53,49,4D,49,52
 *               IMUL ECX,EAX                               ("CASIMIR")
 *               JNO 004339BF
 *               CALL 00402B10
 *      4339BF   ADD EBX,ECX  // EBX=1*43+2*41+3*53+4*49+5*4D+6*49+7*52=857
 *               JNO 004339C8 
 *               CALL 00402B10
 *      4339C8   INC EAX
 *               DEC EDX
 **************< JNZ 004339AD  // check if pwd'end reached

La boucle est exécutée jusqu'à épuisement des caractères du pwd, additionnant chaque valeur ascii à sa position dans le mot de passe :

Donc des entrées DIFFÉRENTES peuvent produire le MÊME Seed, MAIS le right_vector fera croire à COT qu'il est correct!

7. RÉSUMÉ et Méditation

BIEN, tout le monde est là ? Personne ne s'est perdu dans le code ? Laissons COT de coté (juste un moment) le temps de souffler, une bonne tasse de Thé à la main (ne vous endormez pas, la partie agréable de l'histoire arrive!!!).

Voici les actions exécutées par COT :

1. Obtenir le user'input
2. Construire le Seed (la somme pondérée)
3. Construire le left_vector
4. Récupérer le right_vector du fichier crypté
5. Construire le check_vector
6. Si entrée = check_vector = > GOOD BOY, décryptons le fichier pour lui
    Si entrée! =check_vector = > BAD BOY, signal sonore et coup de pied au cul!

HUUUMMMMM..... (cogitation intense, CA FAIT MAL!!!) nous pouvons trouver la pwd_lenght et le right_vector en employant notre vieil ami Winice... A partir de pwd_lenght, quel seed possible avons nous? En étudiant un peu, avec des entrée du type ALT 0, ALT 1..., ALT 255 et en observant comment COT les traduit, nous apprenons que COT ne traite que 136 valeurs ascii en additionnant les caractères entrés (voir "car_set" dans CRACKCOT.CPP).

Code_MIN=0x20, code_MAX=0xF7:

Seed_MIN = (1+2+...+pwd_lenght)*code_MIN
                        = 0.5*pwd_lenght*(pwd_lenght+1)*code_MIN 
               Seed_MAX = 0.5*pwd_lenght*(pwd_lenght+1)*code_MAX

COT v1.24 accepte jusqu'à 45 caractères entrés, et nous obtenons :

            Seed_MIN=0.5*45*46*32 Seed_MAX=0.5*45*46*247                  =33120 = 255645

Ainsi, avec un pwd_lenght = 45 (le pire cas de figure du point de vue du cracker), nous obtenons SEULEMENT (Seed_MAX-Seed-MIN) 222525 seed possibles!!!

Quel PETIT nombre!!!

La faiblesse de COT vient- principalement de ce ridicule 222525...

Écrivons rapidement un méchant petit programme en C:

  1. Calculer Seed_MIN et Seed_MAX 2.

for(seed=Seed_MIN;seed&lt;=Seed_MAX;seed++) { for(i=1;i&lt;=pwd_lenght;i++)

{ 1. build left_vector[i] 2. build check_vector[i]=left_vector[i] XOR right_vector[i] 3. TEST1 is check_vector[i] a valid character, i.e member of car set? valid=FALSE =&gt; break; let's try next seed valid=TRUE =&gt; OK, goto TEST2 4. TEST2 is check_vector built to this point valid? (we must have: 1*check_vector[1]+...+i*check_vector[i]

seed IF i<pwd_lenght

" " " " == seed IF i==pwd_lenght)

valid=FALSE =>

break; let's try next seed, valid=TRUE =&gt; if i<pwd_lenght : go on, we are on the good way, but pwd_lenght not reached if i==pwd_lenght : PASSWORD FOUND !!! 

FÉLICITATIONS!!! NOUS L'AVONS FAIT!!!

8. Traque du pwd_lenght et du right_vector

Bien, nous pouvons maintenant découvrir comment le pwd a été employé pour chiffrer le fichier, mais dans ce but nous devons d'abord découvrir le pwd_lenght et le right_vector en utilisant Winice... Ennuyant et long, mais finissons le travail.

La découverte du pwd_lenght n'exige pas de reverse, de traçage, ou de stack-fishing

... utilisez COT et chiffrez des fichiers divers en employant des mots de passe de tailles différentes. REGARDEZ et si ne voyez rien, REGARDEZ DE PLUS PRÈS!!!

OUI, ces types l'ont juste noté, entre le 3 ° et le 4 ° slash, sans cryptage ou quoi que ce soit d'autre. Jamais je n'ai vu une telle confiance en soi...

Par exemple :

Il suffit juste d'ouvrir le fichier crypté et de le lire.

La découverte du right_vector est un peu plus compliquée. Tout d'abord, ouvrez COT et chiffrez le message "LONGUE VIE À CASIMIR" en utilisant "CASIMIR" comme pwd. Voici ce que nous obtenons :

********************** START  Crypt-o-Text **********************
 CoT/0121/01/7/20 CLy68KoYUEBAFiqBzkfCTGjGiH-+k-LHaQnpio+Z1 CoT/5225
 **********************  END   Crypt-o-Text **********************
0137:00434D78   MOV EAX,100                     ...
                 MOV DL,[EBP-3F]->SS:0068FB41                     ...
                 MOV DL,[EBP-3E]->SS:0068FB42                     ...
                 MOV AL,[EBP-3D]->SS:0068FB43                     ...
 0137:00434DB3   MOV [EBP-3D],BL

Humm... Ca vaut certainement la peine d'espionner ces adresses mémoire. En faisant ainsi, nous découvrons que quelque chose se passe ici :

 0137:00433D71   MOV DL,[EBP-04]     -> 4C ("L")
                 CALL 0040358C
                 MOV EAX,[EBP-14]    -> 4C ("L")
                 MOV EDX,[004388F8]  -> AaBbCc...
                 CALL 0040387C
                 MOV EBX,EAX         // EAX=17
                 SUB EBX,1           // EBX=16                    ...
      00433D97   IMUL EBX,[004388DC] // EBX=40000*16=580000
                    ...      
      00433DA4   MOV DL,[EBP-03]     -> 79 ("y")
                 CALL 0040358C
                 MOV EAX,[EBP-14]    -> 79 ("y")
                 MOV EDX,[004388F8]  -> AaBbCc... 
                 CALL 0040387C
                 SUB EAX,1           // EAX=31                    ...
      00433DC4   IMUL DWORD PTR [004388E4] // EAX=1000*31=31000
                    ...
      00433DD1   ADD EBX,EAX // EBX=EBX+EAX=580000+31000=5B1000
                    ...      
      00433DDD   MOV DL,[EBP-02]     -> 36 ("6")
                 CALL 0040358C
                 MOV EAX,[EBP-14]    -> 36 ("6")
                 MOV EDX,[004388F8]  -> AaBbCc...
                 CALL 0040387C
                 SUB EAX,1           // EAX=3A                    ...
      00433DFD   IMUL DWORD PTR [004388EC] // EAX=40*3A=E80
                    ...
      00433E0A   ADD EBX,EAX // EBX=EBX+EAX=5B1000+E80=5B1E80
                    ...
      00433E16   MOV DL,[EBP-01]     -> 38 ("8")
                 CALL 0040358C
                 MOV EAX,[EBP-14]    -> 38 ("8")
                 MOV EDX,[004388F8]  -> AaBbCc...
                 CALL 0040387C
                 SUB EAX,1           // EAX=3C                    ...
      00433E36   IMUL DWORD PTR [004388F0] // EAX=1*3C=3C         ...
 0137:00433E43   ADD EBX,EAX // EBX=EBX+EAX=5B1E80+3C=5B1EBC

5B1EBC??? BC,1E,5B!!! Le Right_vector lui-même! Il est extrait du fichier crypté, commençant sur la deuxième ligne juste après "C" : Ly68...

Les valeurs Ascii du fichier crypté sont lu 4 par 4, avant que le right_vector ne soient construit.
Mais une question reste en suspend: comment savons-nous , par exemple, qu'un "8" (38) lu dans le fichier crypté produira un "<" (3C) ?
Pour en apprendre plus, examinons les call à 0040387C. Avant le traçage du call, remarquez qu'EDX pointe sur la structure de DONNÉES suivante :

 DS : 00432E3C   Aa Bb Cc Dd Ee Ff Gg Hh
 DS : 00432E4C   Ii Jj Kk Ll Mm Nn Oo Pp
 DS : 00432E5C   Qq Rr Ss Tt Uu Vv Ww Xx
 DS : 00432E6C   Yy Zz 01 23 45 67 89 +-

BIEN, maintenant nous pouvons tracer le call :

 0137:0040387C   TEST EAX,EAX                    ...
      0040388B   MOV ECX,[EDI-04] // ECX=40 (size of look-up table shown above)
                 PUSH EDI
                 MOV EDX,[ESI-04] // EDX=1 (we are looking for 1 character)
                 DEC EDX
                 JS 004038B0
                 MOV AL,[ESI]     ->38 ("8"): we are looking for "8" in table 
                 INC ESI
                 SUB ECX,EDX
                 JLE 004038B0
                 REPNZ SCASB     ->search 38
                 JNZ 004038B0    ->NOT FOUND (file corrupted?)
                     ...
      004038B8   MOV EAX,EDI     // EDI=432E79=our actual position in table ("9")    
                 SUB EAX,EDX     // EDX=432E3C=first position in table ("A")

Ainsi : EAX=3D (offset de "8" + 1). Après la call 0040387C, nous avons un SUB EAX, 1 pour obtenir l'offset correct.

La formule finale est donc :

         right_vector=40000*offset("L")+1000*offset("y")+40*offset("6")+1*offset("8") 

Ce système permet à COT de transformer indépendamment le right_vector en une série ASCII que n'importe quelle traitement de texte peut manipuler...

Maintenant nous ajoutons cette amélioration à notre prg en C et nous obtenons un agréable CRACKCOT.EXE

9. Benchmark et code Source