FR EN

Crackme 1 - Safecrypt

Crackme 4 - Reverseme1 >>

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.

Crackme 4 - Reverseme1 >>

1 comment

  1. FrizN 04/18/14 01:53

    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.