Nuit du Hack Public Wargame 2011 > Crackme 1 - Safecrypt
Premier des deux crackme proposés au public lors de la Nuit du Hack 2011. Nous étaient fournis un exécutable SafeCrypt.exe ainsi qu'une fichier vraisemblablement encrypté Challenge.png. En essayant d'exécuter, on voit qu'on a un exécutable console qui prend trois arguments : une opération (décrypter ou encrypter), un nom de fichier en entrée et un nom de fichier en sortie. Lors de l'exécution, que ce soit pour crypter ou pour décrypter, un mot de passe est demandé et un fichier généré même si le mot de passe ne convient pas. SafeCrypt applique donc un algorithme d'encryption bête et méchant à partir du mot de passe entré.
L'exécutable était un PE Windows, point d'entrée à 0x406000. Le point d'entrée décrypte l'exécutable en unxorant trois petits segments de données, en répetant trois fois la boucle xor suivante :
mov eax, offset 401000h mov ecx, offset 401E00h loc_40600A: xor dword ptr [eax], 2Dh add eax, 1 cmp eax, ecx jnz short loc_40600A
On voit donc facilement que la boucle unxor byte à byte entre 0x401000 et 0x401E00 par 0x2d. La même chose est faite entre 0x402000 et 0x402200 puis entre 0x403000 et 0x403600, avant de jumper dans le code unxoré avec un push 0x401220, ret. On peut donc poser un breakpoint sur le ret et avancer dans la fonction 0x401220.
On y reconnait des initialisation de msvcrt.dll via _set_app_type, puis un appel à 0x401100, vraisemblablement notre fonction de lancement du main. On y retrouve en effet tous les éléments attendus : le SetUnhandledExceptionFilter, et les différents getminargs, p_environ, etc. Avant les exit on trouve l'appel de la vraie fonction main à 0x401290.
Si la fonction a bien trois arguments, on a d'abord un sscanf("%256s") qui récupère le mot de passe. Ensuite, on a l'appel d'une fonction à 0x401637 avec le mot de passe et sa longueur en paramètre :
push ebp mov ebp, esp sub esp, 0Ch mov eax, [ebp+arg_0] not eax mov [ebp+var_4], eax mov eax, [ebp+arg_4] mov [ebp+var_8], eax mov [ebp+var_C], 0 loc_401652: mov eax, [ebp+var_C] cmp eax, [ebp+arg_8] jnb short loc_401686 mov eax, [ebp+var_4] mov edx, eax shr edx, 8 mov eax, [ebp+var_8] add eax, [ebp+var_C] movzx eax, byte ptr [eax] xor eax, [ebp+var_4] and eax, 0FFh xor edx, ds:dword_403080[eax*4] mov eax, edx mov [ebp+var_4], eax lea eax, [ebp+var_C] inc dword ptr [eax] jmp short loc_401652 loc_401686: mov eax, [ebp+var_4] not eax leave ret
Cette fonction a l'air de calculer une sorte de hash, en fonction des bytes du deuxième paramètre et des bytes stockés à 0x403080. Puisque l'on a observé que l'encryption se faisait en fonction du mot de passe, il est probable que ce hash soit une seed pour l'algorithme de cryptage à venir. Dans la fonction main, le résultat est placé à ebp + 0xC
Ensuite, les deux fichiers passés en paramètre sont ouverts en lecture et en écriture puis deux bouts de code différents sont ouverts si argv[1] est "1" (encrypte) ou "2" (décrypte). Nous avons donc nos fonctions d'encryption et on se dirige donc vers la première :
mov [ebp+var_1C], 0 loc_40148D: mov eax, [ebp+var_20] sar eax, 2 cmp eax, [ebp+var_1C] jle loc_4015ED mov eax, [ebp+var_1C] lea edx, ds:0[eax*4] mov eax, [ebp+var_12C] mov eax, [edx+eax] mov [ebp+var_130], eax mov eax, [ebp+var_1C] lea ebx, ds:0[eax*4] mov esi, [ebp+var_12C] mov eax, [ebp+var_1C] lea ecx, ds:0[eax*4] mov edx, [ebp+var_12C] mov eax, [ebp+var_C] xor eax, [ecx+edx] mov [ebx+esi], eax mov [esp+158h+var_150], 4 lea eax, [ebp+var_130] mov [esp+158h+var_154], eax mov eax, [ebp+var_C] mov [esp+158h+var_158], eax call sub_401637 mov [ebp+var_C], eax lea eax, [ebp+var_1C] inc dword ptr [eax] jmp short loc_40148D
ebp + 0x12c étant le buffer contenant les données à crypter, ebp + 0x20 étant la longueur du fichier et ebp + 0xc étant la valeur du hash du mot de passe, cette fonction ne fait rien d'autre qu'appliquer des xor 4 bytes par 4 bytes à notre buffer d'entrée par la valeur du hash. Ce hash est mis à jour à chaque tour de boucle.
Nous savons donc que la seed est simplement sur 32 bits. Puisque nous savons le format initial du fichier à décrypter (.png), il nous est possible d'effectuer un bruteforce sur toutes les seeds possibles pour voir celles qui renvoient les 4 premiers bytes d'un header PNG :
#include <stdio.h> int main() { unsigned int i, encrypted=0xc7807161; // encrypted = beginning of Challenge.png for (i=0;encrypted != (0x474e5089 ^ i);i++); //89 50 4E 47 = PNG Header printf("Found seed: 0x%x\n", i); return 0; }
$ gcc -O2 /tmp/ndh11pw-cm1.c -o /tmp/ndh11pw-cm1 && /tmp/ndh11pw-cm1 Found seed: 0x80ce21e8 $
En ayant la seed, il suffit donc de redémarrer le programme en demandant le décryptage de Challenge.png. Arrivé après le premier appel de 0x401637 (hashage du mot de passe), on remplace eax par 0x80ce21e8 et on laisse le programme se terminer correctement. On a donc pu décrypter l'image sans le mot de passe. Elle contient le flag : 31cb891dbb83e16aa9ea1c54bd0bf1d1.
Le programme original était probablement en C. Dans cet article, on se sert du programme tel quel après génération du programme, donc en assembleur Intel x86.