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.