Le 2 avril 2011 a fait place aux pré-qualifications de la NDH 2011. Un CTF très attendu qui est malheureusement tombé un week-end de beau temps. Et qui dit temps magnifique, dit promenades, sport et très peu de « geekage » !
Mais je ne pouvais pas résister à un sympathique challenge de Reverse-Engineering win32.
C’est pour cette raison que je me suis retrouvé à me triturer les neurones sur le rce100!
Voici le programme de ce write-up :
I/ – Découverte du CrackMe. (Lu33y)
II/ – Unpacking « presque » perso. (Lu33y)
III/ – Elle est où cette routine de vérification de mot de passe ? (Lu33y)
IV/ – Le côté obscure du bruteforcing. (int_0x80)
I/ Découverte du CrackMe :
On a affaire à un CrackMe qui nous demande d’entrer un code afin de valider l’épreuve.
On commence par lancer notre bon vieux « PEiD » :
Celui-ci ne trouve aucune signature de « packer » connu. On se dit alors que pour une épreuve qui rapporte 100 points, on a peut-être affaire à un CrackMe tout simple …
On ouvre donc le CrackMe sous OllyDbg puis on enchaine avec un « ctrl+n » :
Entre le pushAD, le warning d’OllyDbg et l’appel à « VirtualAlloc » on comprend vite que l’on a affaire à un programme « packé » !!!
II/ Unpacking « presque » perso :
Le « packer » ressemble beaucoup à UPX. Je dis ressemble car il ne m’a pas été possible d’utiliser un « unpacker » UPX dessus … Donc on y va à « la mano » !!!
Comme pour UPX, notre programme commence avec un « PUSHAD » qui sauvegarde tous les registres dans la pile.
Il faut que l’on trouve le code assembleur « POPAD » qui va restaurer tous les registres avant de sauter vers un nouvel OEP.
Voila à quoi ressemble cette routine en général :
1 2 3 4 5 6 7 | PUSHAD Du code Encore du code … … POPAD JMP vers OEP |
Dans le cas du rce100, on a ceci :
Dans un premier temps, on va chercher à avoir notre programme sous sa forme décompressée en mémoire. Pour cela, on pose un breakpoint (F2) sur l’instruction assembleur « JMP 0041E74B » (vous aurez probablement une adresse différente), on lance notre programme (F9) et on avance d’une instruction (F8).
Au passage, on note le nouvel OEP. Maintenant que notre programme est décompressé en mémoire, on va chercher à obtenir une copie sur notre disque dur, qu’on appelle plus communément : un dump. Pour cela on peut utiliser LordPE ou le plugin Ollydump (ce qui revient au même !).
Utilisons LordPE :
On sélectionne notre programme et on note au passage l’IMAGEBASE, qui est ici de 400000. On clique droit et on sélectionne Dump Full comme ci-dessous :
On finit par enregistrer le « dump » sur notre disque dur avec le nom « dump.exe ».
Le dump nouvellement crée contient toutes les instructions nécessaires au bon fonctionnement du CrackMe, pourtant si on essaie de l’exécuter il ne fonctionnera pas.
En effet, le CrackMe utilise des fonctions qui ne sont pas propre à lui-même.
Toutes ces informations sont contenues dans l’IAT du CrackMe (Import Address Table). « Dumper » un programme ne reconstruit pas un IAT exploitable, c’est pour cette raison que notre « dump.exe » ne fonctionne pas. Il va donc falloir reconstruire cette table.
Pour nous aider dans cette tâche, nous allons utiliser ImpRec [d’ou Import Reconstructor]. Nous commençons par sélectionner notre CrackMe : rce100.exe
Dans la fenêtre de log on peut noter encore l’ImageBase 400000.
Nous avons vu plus haut que notre nouvel OEP était 0041E74B donc pour ImportRec nous faisons OEP=Nouvel OEP – imagebase de Olly : 0041E74B – 400000 = 1E74B. On modifie cette donnée manuellement si besoin dans la case “OEP”, puis on clique sur IAT Auto Search.
Tout fonctionne jusqu’à présent, puisque ImpRec trouve quelque chose et nous le dit. Il ne nous reste plus qu’à cliquer sur « Get Imports », et ceux-ci doivent être valides comme ci-dessous :
Maintenant, on va réparer le dump endommagé en cliquant sur Fix Dump, puis en sélectionnant notre fichier dump.exe.
ImpRec sauvegarde une copie de notre dump appelé “dump_.exe”. Si vous l’essayez, le fichier nouvellement crée fonctionne !
On se retrouve finalement avec un CrackMe lisible sur lequel on va pouvoir travailler.
III/ Elle est où cette routine de vérification de mot de passe?
On commence par effectuer une recherche sur les chaînes de caractère qui ne nous ne retournent pas trop d’infos.
On poursuit avec un « CTRL + n » et on se met à chercher une API intéressante.
On pose un breakpoint sur un « strcmp », et on debug en mode pas à pas jusqu’à tomber sur une fonction qui retourne un résultat nous permettant de rediriger le flux de notre programme vers un BadBoy ou un GoodBoy.
On comprend très rapidement que l’on est face à la routine de vérification du mot de passe.
On traduit cette portion de code assembleur en pseudo-code, telle que :
Fonction de calcul d’un hash perso :
1 2 3 4 5 6 7 8 9 10 | [ ESI = 0xDEADBEEF 1/ Pour chaque caractère composant notre « serial » : ESI = ESI x 0x38271606 EAX = EAX x 0x5B86AFFE EAX = EAX - ESI ESI = EAX End 1/ EAX = ESI ] |
Pour obtenir le GoodBoy, EAX doit être égal à 0xC4B1801C.
IV/ Le côté obscure du bruteforcing :
C’est bientôt finit, mais il faut encore se triturer les méninges … (pour 100 points).
Le seul moyen de trouver la solution qui nous permettrait d’afficher un GoodBoy est de tester toutes les solutions possibles en brute forçant !
On commence donc par croiser les doigts en espérant que le staff nous ai épargné plusieurs heures de calculs avec un serial de petite taille.
Il n’y avait donc plus qu’à coder ce brute-forcer. C’est à l’aide d’aXs qu’on arriva à nos fins.
Donc voici notre chef d’oeuvre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #include <stdio.h> #include <stdlib.h> #include <string.h> int sub_41EC50(const char *a1) { unsigned int v1; // edx@1 const char *v2; // ecx@1 int v3; // esi@1 char v4; // al@2 v2 = a1; v3 = -559038737; v1 = 0; do v4 = *v2++; while ( v4 ); if ( v2 != a1 + 1 ) { do v3 = 1535553534 * a1[v1++] - 942085638 * v3; while ( v1 < strlen(a1) ); } return v3; } int main(int argc, char **argv) { char key[32]; char *nl; FILE *f; int c; int hash; //f = fopen(argv[1], "r"); f = stdin; while(!feof(f)) { fgets(key, 32, f); nl = strrchr(key, '\r'); if (nl) *nl = '\0'; nl = strrchr(key, '\n'); if (nl) *nl = '\0'; if (!strlen(key)) continue; hash = sub_41EC50(key); if (c % 100000 == 0) { printf("hash: %X\n", hash); } if (hash == 0xC4B1801C) { printf("OK for hash: %s\n", key); exit(0); } c++; } } |
Une fois compilé, j’utilise un « tool » qu’aXs m’a conseillé, celui-ci se nomme : crunch.
Il va me permettre de générer un fichier (dictionnaire) qui contiendra toutes les permutations possibles pour un serial qui contiendrait des chiffres, des lettres majuscules et des lettres minuscules. On définit notre keyspace à 5 chars.
Ci-dessous, la commande utilisée pour générer le dictionnaire de brute force qui enchaîne en exécutant le programme :
1 2 3 4 5 6 7 8 9 10 11 12 | int_0x80@int0x80:~/Bureau/crunch2.9$ ./crunch 5 5 -f charset.lst mixalpha-numeric | ./bf [ ... ] hash: 961CD642 hash: B924CB5A hash: E3CBE2F2 hash: C357B796 hash: A14BF876 hash: D2BA93F6 hash: F5C2890E hash: 46DEC4E8 OK for hash: pWn3D int_0x80@int0x80:~/Bureau/crunch2.9$ |
On obtient le « serial » qui permet de valider l’épreuve : pWn3D
Merci à aXs pour son aide au bruteforcing et à Overclok[] qui nous a aidé à valider cette foutue épreuve 🙂