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!!!
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).
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:
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 :
Pwd_lenght=10 : CoT/0121/01/10/7Pwd_lenght=45 : CoT/0124/03/45/3
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