Reverse de Crypto v3.5 de Grégoire Braun
Par
CASIMIR
Partie A Sommaire: Introduction De quoi ai-je besoin ? Vue de l'extérieure PART A. REVERSING OF BRAUN'S CRYPTO V3.5 A1. Input handling A2. Hash tables and the making of KEY_0 A3. The making of KEY_1 A4. The making of KEY_2 A5. The check routine A6. The decryption processPART B. ANALYSIS AND BREAKING OF BRAUN'S CRYPTO V3.5 B1. The recovery of KEY_1 B11. Method 1 B12. Method 2 B13. Method 2 vs Method 1 B2. The recovery of KEY_0 and PASSWORD B21. Feasibility B22. Unicity B23. KEY_0's range B24. Reduced ASCII Character Set B25. Choosing suitable Password's length(s) B26. Building a Password B3. Benchmark PART C. 'C' SOURCE FOR CRACKER
Braun prétend employer l'algorithme BlowFish pour produire un cryptage sûr
de Crypto v3.5. En réalité l'algorithme employé est assez faible.
Pour décrypter un fichier, Crypto v3.5 utilise un Mot de passe et, en fonction d'un hash fait maison, produit
une clef 32 bits : KEY_1. KEY_1 qui est manipulée (des manipulations ANDs) pour produire une deuxième
clef 32 bits : KEY_2. KEY_2 est comparée contre une troisième clef 32 bits : KEY_CHK, qui est stockée
dans le header du fichier chiffré. Si KEY_2 = KEY_CHK, alors Crypto v3.5 décrypte le fichier, employant
KEY_1 comme clef de décryptage.
La faiblesse de Crypto v3.5 vient du fait que :
- Donnant KEY_CHK, KEY_1 peut être facilement et rapidement deviné (vous n'avez pas besoin d'essayer
2^32 KEY_1 possibilités, approximativement 500000 essais sont suffisant dans chaque cas);
- Donnant KEY_1, un Mot de passe peut être facilement et rapidement contrefait (ce n'est même pas obligatoire,
puisque crypto v3.5 emploie KEY_1 comme clef de décryptage).
De quoi ai-je besoin ?
Cible : Crypto v3.5 crypto.exe : 188416 octets Crypto 3.5
Outils nécessaires :
Winice par Nu-Mega, un éditeur hexadécimal, une table d'ASCII avec les valeurs hexadécimales
des caractères et une bonne calculatrice : Excalibur 32 bits par David Bernazzani (Cette calculatrice a
une quantité étonnante de fonctions incorporées, comme ROL et ROR.
Vue de l'extérieur
BIEN, avant de commencer le reverse de ce programme, commençons par jouer
un peu avec lui. Chiffrez des fichiers divers avec des tailles et de contenus différents, des longueurs
de noms et de mots de passe variables. Examinez-les maintenant avec Hex Workshop : vous remarquerz que Crypto ajoute
276 octets à chaque fichier chiffré, quel que soit le mot de passe utilisé. Crypto ne semble
pas exécuter de compression avant le cryptage, il n'ajoute probablement pas d'éléments aléatoires
au fichier (la taille des fichiers varierait beaucoup plus). Donc nous pouvons assumer que :
encryption 1 caractère du plaintext ----------------> 1 caractère du ciphertext Ces 276 bytes constituent le header, qui a la structure suivante: CryptoHdrBlk????pathfilename.............encryptedfile <------------- 276 bytes --------------->< original > size
Huuummmm... Ces 4 caractères mystérieux entre l'emplacement original
et le nom de fichier ont titillé mon intérêt. Leurs valeurs semblent être liées
à la fonction du mot de passe, mais ils ne dépendent pas du contenu du plaintext. Ce serait une place
agréable pour un mot de passe chiffré (en fait ce ne sera pas le cas…).
Puisque Braun a employé un algorithme fait maison (et a oublié de nous en donner la recette {:),
nous devrons changer complètement son programme. Préparez un fichier d'essai : braun.txt avec un
contenu : ABCDEF (6 octets, MAJUSCULE). Chiffrez-le avec le pwd : CASIMIR (MAJUSCULE), vous obtiendrez un fichier
chiffré braun. (= txt =)
Dump de braun.txt : 00000000 43 72 79 70 74 6F 48 64 72 42 6C 6B 92 F4 F6 6B CryptoHdrBlk... K 00000010 43 3A 5C 43 52 59 50 54 4F 32 5C 64 65 66 69 5C C:\CRYPTO2\defi\ 00000020 62 72 61 75 6E 2E 74 78 74 00 00 00 00 00 00 00 braun.txt.. ...... 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000110 00 00 00 00 B1 3E 3E 18 12 A7..... > >...
Excellent! Ce fichier de 282 octets sera notre base de référence pendant le reverse du processus, donc sauvegardez-le.
PARTIE A. REVERSE DE BRAUN'S CRYPTO V3.5
======================
A1. Traitement des entrées
======================
Comme d'habitude, je prendrai une approche Live : j'emploierai Winice pour découvrir ce qui arrive à
mon entrée. Choisissez le fichier braun. (= txt =) pour le décryptage, entrez un faux mot de passe
: 13579 (n'employez pas 012345... cet ordre est souvent présent en mémoire). Avant la vérification
du pwd, mettez des BPXS sous Softice aux fonctions de Windows qui sont d'habitude employées pour la saisie
d'info. Cette fois la fonction GetDlgItemTextA fonctione et voici ce que nous voyons :
... CS:405EB2 mov ebx,[USER32!GetDlgItemTextA] CS:405EB8 push 100 CS:405EBD push eax <-- Watch out! Input is copied to DS:EAX CS:405EBE push 65 CS:405EC0 push esi CS:405EC1 call ebx <-- get input and copy it to DS:EAX ...
====================================
A2. Tables de hash et la fabrication de KEY_0
====================================
Mettez un BPR DS:EAX EAX+5 RW pour savoir où et quand votre entrée est employée. L'entrée est lue dans CS:405EE0 (ne me demandez pas pourquoi), et nous arrivons à la partie intéressante :
* Référence : KERNEL32.lstrlenA, Ord:029Ch :0040CD68 xor edi,edi <-- edi=0 :0040CD6A push esi :0040CD6B Call dword ptr [0042D48C] <-- return input's length in eax :0040CD71 test esi, esi :0040CD73 je 0040CDA7 :0040CD75 test eax, eax :0040CD77 je 0040CDA7 :0040CD79 mov ecx, 00000000 :0040CD7E jle 0040CDA7
Avant d'aller plus loin, remarquez que Crypto emploi 2 tables de hash :
- HashTable_1 starting at DS:41DDF8
23 73 65 72 42 26 6E 7A 7C 6D 66 4D 31 2F 35 28 21 73 64 24 4D 71 2E 7B 73 5D 2B 73 46 6A 74 4B 70 7A 53 64 74 7A 6F 58 71 6D 62 5E 41 6C 40 64 76 3A 73 3F 78 2F 00 00 7C 62 21 70 7A 2A 6C 73 3B 72 6E 7C 6C 66 24 76 69 5E 41 78 70 65 29 72 78 35 61 69 63 26 39 2F 32 6D 35 6C 73 69 34 40 30 64 6D 5A 77 39 34 63 6D 71 70 66 68 77 00 00
- HashTable_2 starting at DS:41DE30
7C 62 21 70 7A 2A 6C 73 3B 72 6E 7C 6C 66 24 76 69 5E 41 78 70 65 29 72 78 35 61 69 63 26 39 2F 32 6D 35 6C 73 69 34 40 30 64 6D 5A 77 39 34 63 6D 71 70 66 68 77 00 00
Ces tables sont toujours les mêmes, et ne dépendent pas de l'entrée ou plaintext.
***>:0040CD80 movsx ebx, byte ptr [eax+ecx+0041DDF8] <-- read 1 character * from HashTable_1 * :0040CD88 movsx ebp, byte ptr [esi+ecx] <-- read 1 character from input * :0040CD8C lea edx, dword ptr [ecx+01] <-- edx=ecx+1 * L :0040CD8F imul ebx, ebp * O :0040CD92 movsx ecx, byte ptr [ecx+0041DE30] <-- read 1 character from * O HashTable_2 * P :0040CD99 imul ebx, ecx * :0040CD9C imul ebx, edx * :0040CD9F add edi, ebx <-- edi=accumulator * :0040CDA1 mov ecx, edx * :0040CDA3 cmp eax, edx ***<:0040CDA5 jg 0040CD80 <-- loop eax (5) times :0040CDA7 mov eax, edi :0040CDA9 pop ebp :0040CDAA pop edi :0040CDAB pop esi :0040CDAC pop ebx :0040CDAD ret
En résumé, nous avons length=len de l'entrée :
edi = HashTable_1[len+1] * HashTable_2[1] * 1 * Input[1] + HashTable_1[len+2] * HashTable_2[2] * 2 * Input[2] + ... +
HashTable_1[len+len] * HashTable_2[len] * len * Input[len]
let Coef[i] = HashTable_1[len+i] * HashTable_2[i] * i ,
nous avons:
edi = Coef[1]*Input[1] + Coef[2]*Input[2] + ... + Coef[len]*Input[len]
par exemple, avec l'entrée: 13579, on obtient: edi=0x00868500
Il y a quelque chose d'intéressant du point de vue du cracker : les coefficients dépendent seulement
de la longueur de l'entrée. Cela signifie que, pour la longueur d'une entrée donnée, nous
pouvons pré calculer chaque coefficient. Il nous épargnera beaucoup de temps de calcul pendant le
reverse du processus.
Nous appellerons la valeur d'edi : KEY_0 dorénavant.
======================
A3. La fabrication de KEY_1
======================
Ensuite KEY_0 est copié dans eax (CS:40CDA7) et eax est copié dans DS:42A0F0. Comme d'habitude, nous examinons cet emplacement mémoire et découvrons que la valeur est copiée dans CS:405D4C et CS:402282. Mais le traitement le plus important arrive dans CS:402AC4 :
CS:402AC4 or [esi],06A30DE8 // DS:[esi] contains KEY_0 06A30DE8 is a constant
Nous obtenons une nouvelle clef de 4 octets, KEY_1 :
KEY_1 = KEY_0 ou 06A30DE8 = 00868500 ou 06A30DE8 = 06A78DE8
======================
A4. La fabrication de KEY_2
======================
KEY_1 est utilisé dans le call suivant :
:0040B2F0 push esi:0040B2F1 push edi :0040B2F2 mov esi, dword ptr [esp+0C] // copy KEY_1 to esi :0040B2F6 push esi :0040B2F7 call 00411450 // copy KEY_1 to DS:4205F4 :0040B2FC add esp, 00000004
:0040B2FF call 00411460 // first_call :0040B304 mov edi, eax // eax=00001799 (see first_call walk-thru) :0040B306 call 00411460 // second_call :0040B30B shl eax, 00000010 // eax=00004411 (see second_call walk-thru) :0040B30E add eax, edi // eax = 44110000+00001799 = 44111799 :0040B310 pop edi :0040B311 add eax, esi // add KEY_1 to eax, giving KEY_2: // KEY_2 = 06A78DE8 + 44111799 = 4AB8A581 :0040B313 pop esi:0040B314 ret
Voyons ce qui se passe dans ce premier call en CS:411460
First_call ---------- :00411460 mov eax, [004205F4] // copy KEY_1 to eax :00411465 lea ecx, dword ptr [eax+4*eax] // ecx = 5*eax :00411468 lea ecx, dword ptr [ecx+4*ecx] // ecx = 5*ecx = 19*eax :0041146B add ecx, eax // ecx = 1A*eax :0041146D lea ecx, dword ptr [eax+8*ecx] // ecx = eax+8*1A*eax = D1*eax :00411470 shl ecx, 00000008 // ecx = 100*ecx = D100*eax :00411473 sub ecx, eax // ecx = ecx-eax = D0FF*eax :00411475 lea eax, dword ptr [eax+4*ecx+269EC3] // eax = 343FD*eax+269EC3 :0041147C mov [004205F4], eax // copy KEY_TMP_1 = eax to DS:4205F4 :00411481 and eax, 7FFF0000 // mask eax :00411486 shr eax, 00000010 // right shift : 12345678 becomes 00001234 :00411489 ret
Donc avec KEY_1 = 06A78DE8 nous obtenons : KEY_TMP_1 = 343FD*KEY_1+269EC3 = 1799950B
et eax (la valeur de retour) = (KEY_TMP_1 et 7FFF0000) / 10000 = 00001799
Cette partie de code est appelée une deuxième fois et la valeur de KEY_TMP_1 est employée
:
Second_call ----------- :00411460 mov eax, [004205F4] // copy KEY_TMP_1 to eax :00411465 lea ecx, dword ptr [eax+4*eax] // ecx = 5*eax :00411468 lea ecx, dword ptr [ecx+4*ecx] // ecx = 5*ecx = 19*eax :0041146B add ecx, eax // ecx = 1A*eax :0041146D lea ecx, dword ptr [eax+8*ecx] // ecx = eax+8*1A*eax = D1*eax :00411470 shl ecx, 00000008 // ecx = 100*ecx = D100*eax :00411473 sub ecx, eax // ecx = ecx-eax = D0FF*eax :00411475 lea eax, dword ptr [eax+4*ecx+269EC3] // eax =343FD*eax+269EC3 :0041147C mov [004205F4], eax // copy KEY_TMP_2 = eax to DS:4205F4 :00411481 and eax, 7FFF0000 // mask eax :00411486 shr eax, 00000010 // right shift : 12345678 becomes 00001234 :00411489 ret
Ainsi avec KEY_TMP_1 = 1799950B nous obtenons :
KEY_TMP_2 = 343FD*KEY_TMP_1+269EC3 = 4411CBA2Et
Eax (valeur retournée) = (KEY_TMP_2 et 7FFF0000) / 10000 à 00004411
=====================
A5. La routine de contrôle
=====================
Comme nous le verrons bientôt, KEY_2 est employé immédiatement après sa génération. Le retour se fait en CS:40B314, et voici ce que vous voyez :
:00402D73 call 0040B2F0 <--- the place we return from :00402D78 add esp, 00000004 :00402D7B cmp eax, dword ptr [esp+258] // compare KEY_2 against KEY_CHK :00402D82 je 00402DC3 // jump only if KEY_2 = KEY_CHK :00402D84 mov eax, dword ptr [esp+14] // bad guy, let's kick his ass :00402D88 push eax * Reference To: KERNEL32.CloseHandle, Ord:0017h :00402D89 Call dword ptr [0042D4E0] :00402D8F mov eax, dword ptr [esp + 10] :00402D93 push eax * Possible Reference to String Resource ID=02007: "File: %sThis file was not encrypted with the current key" :00402D94 push 000007D7 :00402D99 jmp 004033BF
Nous avons: KEY_2 = 4AB8A581 et KEY_CHK = 6BF6F492
Les clefs ne sont pas les identiques, donc Braun refuse de décrypter
le fichier.
Question : d'où vient KEY_CHK ?
Vous vous rappelez les 276-bytes du header, n'est-ce pas ?
Il y avait 4 octets mystérieux dans ce header :
00000000 43 72 79 70 74 6F 48 64 72 42 6C 6B 92 F4 F6 6B CryptoHdrBlk... K <-KEY_CHK->
Cette très petite clef est employée par Braun pour savoir si le mot de passe est bon ou non.
=========================
A6. Le processus de décryptage
=========================
Si le mot de passe est correct, Braun décrypte le fichier. Le processus de décryptage ne présente
aucun intérêt, donc je ne le décrirai pas ici. La seule chose importante est que Braun n'emploie
pas de mot de passe pour décrypter le fichier, mais KEY_1.
Copyright septembre 1999 par Casimir.
Mail: Casimir