FR EN

Crackme 1 - rce 100

Crackme 2 - rce 200 >>

Ce premier crackme rce100 est un exécutable PE qui affiche une simple interface graphique demandant un mot de passe. Lors d'une tentative, une image Access Denied est affichée. Son point d'entrée est à 0x466d90.

L'exécutable commence sur une fonction d'unpacking assez compliquée. Le but dans ce genre de packing fait maison est de trouver le point de sortie. Puisqu'il jumpe souvent sur une fonction qui avant était cryptée et contenait des données a priori aléatoire, on cherche donc une "feuille" sur le control-flow. Pour ça, les désassembleurs offrant une vue en graphe comme IDA sont d'une grande aide.

Ceci nous permet de repérer la seule feuille à 0x466f1b (qui de plus était la fin du segment) qui effectue un jump vers 0x41e74b. On place donc un hardware breakpoint à cette adresse et on lance l'exécutable. Lors de l'arrivée au breakpoint, on trouve un call 0x41eb44, jmp 0x41e48a.

On observe donc 0x41eb44 et on voit qu'elle effectue plusieurs subcalls à des adresses autour de 0x41F000. On y découvre une sorte d'IAT avec les différentes fonctions dynamiques de l'application. Parmi les subcalls de cette fonction, on voit notamment des appels à QueryPerformanceCounter et GetTickCount qui font penser à de l'anti-debug. Puisque l'on semble avoir trouvé les fonctions importées entre 0x40F1000 et 0x40F21C, on essaye de trouver les autres méthodes d'anti-debug et on voit notamment IsDebuggerPresent et GetParent. On active donc les différentes options d'IDAStealth qui vont bien.

Puisque nous avons à faire à une fenêtre graphique, on peut directement essayer de chercher les fonctions typiques des dispatchers graphiques qui ne sont appellés qu'une fois, comme DispatchMessage. On trouve bien une référence à 0x4010C6 qui fait partie de la traditionnelle boucle de fond Get/Translate/DispatchMessage. On essaye donc de s'y insérer en posant un breakpoint et en avancant.

Et là, c'est le drame. Notre fenêtre IDA disparaît, même si elle semble tout de même être active. On recommence donc en consultant les autres fonctions graphiques utilisées et on voit notamment ShowWindow qui semble bien correspondre à ce qui nous arrive.. On cherche donc dans les bouts de code utilisant ShowWindow ceux qui sont susceptibles de faire un ShowWindow(0) et on en trouve en premier lieu un seul à 0x403597. On pose un breakpoint dessus mais le problème demeure.. On n'a donc pas d'autre choix que de poser un breakpoint à l'intérieur de ShowWindow. Le premier appel que l'on trouve a bien comme deuxième argument un 0 et l'adresse de retour est à 0x41ef1e. En analysant le code alentour on obtient la procédure suivante :

sub esp, 404h
mov eax, dword_4228B4
xor eax, esp
mov [esp+404h+var_4], eax
push esi
mov esi, [esp+408h+arg_0]
push edi
push 400h
lea eax, [esp+410h+var_404]
push eax
push esi
call RealGetWindowClass
lea edi, [esp+40Ch+var_404]
call sub_41EC50
cmp eax, 3675E71Ah
jz short loc_41EF15
cmp eax, 0E764ED66h
jnz short loc_41EF1E

loc_41EF15:
push 0
push esi
call NTUSerShowWindow

On se trouve donc bien en présence d'un ShowWindow(0) conditionnel sur la valeur de retour de la fonction 0x41EC50 que voici :

push ecx
push ebx
mov ecx, edi
push esi
mov esi, 0DEADBEEFh
xor edx, edx
lea ebx, [ecx+1]
nop

loc_41EC60:
mov al, [ecx]
add ecx, 1
test al, al
jnz short loc_41EC60
sub ecx, ebx
jz short loc_41EC9F
lea ecx, [ecx+0]

loc_41EC70:
movsx eax, byte ptr [edx+edi]
imul esi, 38271606h
imul eax, 5B86AFFEh
sub eax, esi
mov ecx, edi
mov esi, eax
add edx, 1
lea eax, [ecx+1]
lea esp, [esp+0]

loc_41EC90:
mov bl, [ecx]
add ecx, 1
test bl, bl
jnz short loc_41EC90
sub ecx, eax
cmp edx, ecx
jb short loc_41EC70

loc_41EC9F:
mov eax, esi
pop esi
pop ebx
pop ecx
retn

Cette fonction semble simplement calculer une sorte de hash par rapport au edx qui lui est passé. On consulte donc cet edx et on découvre qu'il sagit du nom de notre fenêtre "TIdaWindow". C'est joli.

On remplace donc le push 0 par un push 1 à 0x41EF15 et on replace notre breakpoint sur DispatchMessage à 0x4010C6. A partir de ce DispatchMessage, on peut poser un hardware breakpoint en lecture sur le deuxième paramètre et avancer jusqu'à ce qu'il soit pushé sur la pile avant un UserCallWinProcCheck. On y rentre et on avance jusqu'à MapKernelClientFnToClientFn. Cette fonction retourne la valeur de la fonction WndProc associée avec notre fenêtre : 0x4015E0 (qui sera appellée un peu plus tard dans InternalWndProcCall). On y cherche l'action associée aux messages 0x20? (clic) :

loc_401611:
mov eax, [ebp+arg_4]
cmp eax, 201h
jz loc_4017F4

loc_4017F4:
push 0
push 2
push 0A1h
push ebx
call sendMessage
jmp short loc_401818

loc_401818:
mov eax, [ebp+arg_C]
push eax
mov eax, [ebp+arg_8]
push eax
mov eax, [ebp+arg_4]
push eax
push ebx
call [ebp+var_58]

On voit donc qu'il y a tout d'abord le renvoi d'un message 0xA1 à la fenêtre qui n'a pas l'air de mener à quelque chose de spécial. Ensuite, il y a ce call ebp+0x58. Cette variable a l'air d'être constante pour les messages à la fenêtre, on pose donc un breakpoint après sa définition à 0x401611 pour en obtenir la valeur : 0x41ED20. Cette fonction commence par un call à EnumWindows qui passe une fonction à *toutes* les fenêtres top-level. La fonction en question est 0x41eed0 qui contient notamment le code qui effectuait le ShowWindow(0). Ensuite, selon le wParam qui identifie le bouton en question qui a reçu le clic, différentes actions sont prises : defWinProc, PostQuitMessage, NTUserDestroyWindow et 0x41ecb0. Nous semblons donc bien être dans la fonction qui nous intéresse puisqu'elle gère également le bouton quitter. Intéressons-nous à la seule fonction inconnue :

sub esp, 208h
mov eax, dword_4228B4
xor eax, esp
mov [esp+208h+var_4], eax
push edi
push 201h
lea eax, [esp+210h+var_208]
push eax
push 2
call sub_403F10
add esp, 0Ch
lea edi, [esp+20Ch+var_208]
call sub_41EC50
cmp eax, 0C4B1801Ch
pop edi
jnz short loc_41ECEE
push 3
jmp short loc_41ECF0

loc_41ECEE:
push 4

loc_41ECF0:
call sub_403550
push 2
call sub_403580
push 64h 
call sub_403580
mov ecx, [esp+214h+var_4]
add esp, 0Ch
xor ecx, esp
xor eax, eax
call near ptr off_41E430
add esp, 208h
retn

La première chose que l'on reconnait est la fonction 0x41ec50 qui est la même fonction de hash rencontrée précedemment. On voit ensuite que l'exécution diffère selon le résultat de cette fonction. On y pose un breakpoint, on toggle le ZF pour voir l'incidence de cette comparaison et on voit apparaître l'image "Access Granted". On revient et on essaye de voir ce qui a été hashé, donc ce qui est pointé par edi. Surprise, c'est notre mot de passe.. Par conséquent, on n'a plus qu'à implémenter un brute-force sur la fonction de hash pour trouver une chaîne qui donne 0C4B1801Ch :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LEN 6

#define CUSTHASH "\x1c\x80\xb1\xc4"
#define HASHLEN 4

char BRUTECHARS[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

int customHash(char * inBuf) {
	int seed = 0xDEADBEEF;
	char * end;
	int i=0;

	for (end=inBuf;*end++;);

	if (end != inBuf + 1)
		for (i=0;i<strlen(inBuf);i++)
			seed = 0x5B86AFFE * inBuf[i] - 0x38271606 * seed;
	return seed;
}

void do_recurs_brute(char * buf, int idx, int len) {
	int i;
	static int x;

	if (idx == len) {
		buf[len] = 0;
		x = customHash(buf);
		if (!memcmp((char*)&x, CUSTHASH, HASHLEN)) {
			printf("Found valid hash value %s\n", buf);
			exit(0);
		}
		return;
	}

	for (i=0;i<strlen(BRUTECHARS);i++) {
		buf[idx] = BRUTECHARS[i];
		do_recurs_brute(buf, idx+1, len);
	}

	if (!idx)
		printf("Not found for len: %d\n", len);
}

int main() {
	int i;
	char buf[MAX_LEN+1];

	for (i=0;i<MAX_LEN;i++)
		do_recurs_brute(buf, 0, i+1);
	return 1;
}
$ gcc -O2 -m32 bruterce100.c -o bruterce100 && ./bruterce100
Not found for len: 1
Not found for len: 2
Not found for len: 3
Not found for len: 4
Found valid hash value pWn3D

Et voilà, le mot de passe est trouvé en quelques secondes. Beau crackme de la part des organisateurs, qui couvre une belle portion des techniques de cracking et d'anti-debug.

Crackme 2 - rce 200 >>