HackLu Reverse Challenge
Par Baboon le vendredi, novembre 20 2009, 17:42 - Lien permanent
Lors de la dernière conférence Hack.lu il a été proposé 2 challenges de reverse, le premier étant un challenge de "crypto" par J-B Bedrune le deuxième un challenge de reversing avec moultes exceptions
Ayant réagit "un peu" vivement à une solution proposée par Thierry Zoller je me dois de faire une jolie solution
Tout d'abord et afin de mettre les choses au clair si vous avez lu le post de Thierry Zoller, le crack me n'est pas packé. Le début du code est standard et correspond à une application compilée avec VC (call XXXXXX | jmp long) et le reste du code est parfaitement normal.
Le main se situe en 004014A0, il affiche une message box dont la dialogproc se situe en 004013D0, encore une fois, rien de chiffré ni même d'obfusqué, l'entropie élevée de la fin de la section de data correspond en fait aux tables utilisées pour vérifier notre serial.
Enfin une chose à ne jamais faire est de remplacer un saut conditionnel par son inverse, si vous voulez que la branche soit toujours prise, il faut mettre un jmp, si vous ne voulez pas qu'elle soit prise, un nop. Remplacer un je par un jne reviens à remplacer la phrase "si le code est bon, affiche good boy" par "si le code est mauvais, affiche good boy", si l'on entre le bon sérial avec l'application patchée par Thierry Zoller on aura droit à un joli "bad boy"...
Passons maintenant à l'étude de la fonction de vérification qui se situe en 00401320. On se rend rapidement compte que le sérial doit faire 32 caractères : pour le bruteforce c'est mort :D, Le serial est ensuite converti en hexadécimal en prenant les caractères 2 par 2 ("1234AB" => 12 34 AB) puis ce tableaux de bytes est passé à la moulinette et est comparé à la chaine de caractère "hack.lu-2009-ctf", il va donc nous falloir coder la fonction inverse de la moulinette pour retrouver le code à entrer !
Cette "moulinette" me fait penser à une whitebox dans le sens cryptographique du terme, il n'y a pas de clef courte mais plutôt de grosses tables qui font penser aux tables générées pour chiffrer du DES plus rapidement bref ... Je vais maintenant essayer d'expliquer chacune des étapes de chiffrement et nous verrons ensuite comment les inverser :
Meli-Melo : permutation
La première fonction qui est utilisée sur le serial est une permutation dont la fonction inverse est la suivante :
for (i = 0; i < 4 ; i++) { cpy[i] = chaine[i*4]; cpy[i+4] = chaine[i*4+1]; cpy[i+8] = chaine[i*4+2]; cpy[i+12] = chaine[i*4+3]; }
cette permutation est aussi utilisée juste avant la vérification du sérial, je ne m'attarde pas trop sur cette fonction qui est simplissime
*BOUM*
On passe maintenant à l'analyse de la seconde fonction qui est appliquée au sérial, là ça devient méchant, le crack-me effectue 8 fois une substitution + un ROL sur chacun des DWORD du serial + une "super" substitution, à chaque tour la première substitution est différente et est donnée par le n° de tour.
La substitution
Elle remplace le ième byte b du jème DWORD au nième tour par le tab_408138[(b+n*256)*16+j] comme je ne voulais pas m'embêter à dumper les tables, j'ai décidé d'écrire une DLL que j'injecte dans le crack me, je bruteforce ensuite chaque valeur pour obtenir son inverse par la substitution plutôt que de construire les tables inverses, c'est particulièrement porky mais bon ... Au final ça donne ceci (ce sont les fonction inverses) :
BYTE getInv(int i,BYTE b, int j, int indice) { int k; for (k = 0; k < 0x100 ; k++) if (byte_408138[i+(k+indice)*16+j] == b) return k; MessageBoxA(0,"TAINPUUUUUU",0,0); DebugBreak(); return 0; } void substitution(PBYTE chaine, int indice) { int i; PBYTE v4 = chaine + 1; for (i = 0; i < 16 ; i+=4) { v4[i- 1] = getInv(0,v4[i- 1],i,indice); v4[i] = getInv(1,v4[i],i,indice); v4[i+1] = getInv(2,v4[i+ 1],i,indice); v4[i+2] = getInv(3,v4[i+ 2],i,indice); } }
le ror
La seconde fonction utilisée sur le serial dans la grande boucle effectue un simple ROR sur chacun des DWORD du serial suivant le n° de ce DWORD, la fonction inverse est la suivante :
void rocknrol(PDWORD chaine) { int i; for (i = 3; i >= 0 ; i--) chaine[i] = ROL(chaine[i],8*i); }
je ne m'attarderai pas dessus, je pense que tout le monde a compris :D
la "super" substitution
J'appelle cette fonction la super subtitution parce qu'elle ne se contente pas d'associer à un byte un autre byte mais d'associer à 4 bytes, 4 autres bytes suivant la fonction suivante :
for (i = 0; i < 4 ; i++) { ser0 = serial[i]; ser1 = serial[i+4]; ser2 = serial[i+8]; ser3 = serial[i+16]; serial[i] = 414000[4 * ser0] ^ 414400[4 * ser1] ^ 414800[4 * ser2] ^ 414C00[4 * ser3]; serial[i+4] = 414001[4 * ser0] ^ 414401[4 * ser1] ^ 414801[4 * ser2] ^ 414C01[4 * ser3]; serial[i+8] = 414000[4 * ser0 + 2] ^ 414400[4 * ser1 + 2] ^ 414802[4 * ser2] ^ 414C00[4 * ser3 + 2]; serial[i+16] = 414000[4 * ser0 + 3] ^ 414400[4 * ser1 + 3] ^ 414800[4 * ser2 + 3] ^ 414C00[4 * ser3 + 3]; }
Comme on le voit ici chaque valeur après substitution dépend des 4 valeurs originales on ne peut donc pas construire de table inverse byte par byte (si il fallait construire une table ce serait une correspondance de DWORD à DWORD et on aurait un immense table) la solution est donc de fixer 3 valeurs, d'en déduire la 4eme et de vérifier que cela concorde.
Après étude, on se rend compte que la valeur donnée par 414C00[4 * ser3 + 3] correspond à une substitution identité, c'est à dire qu'elle associe à la valeur ser3 .... ser3 ! On choisit donc de fixer ser0, ser1 et ser2 et de vérifier si la valeur de ser3 donnée par ser3 = serial[i+16] ^ 414000[4 * ser0 + 3] ^ 414400[4 * ser1 + 3] ^ 414800[4 * ser2 + 3] est juste. La fonction inverse à cette substitution est donc :
void bfser(PBYTE chaine) { DWORD i,j,k; for(i = 0; i <= 0xFF ; i++)for(j = 0; j <= 0xFF ; j++)for(k = 0; k <= 0xFF ; k++) { BYTE a,b,c,d,l; a = byte_414000[4 * i] ^ byte_414400[4 * j] ^ byte_414800[4 * k]; b = byte_414001[4 * i] ^ byte_414401[4 * j] ^ byte_414801[4 * k]; c = tab0[4 * i + 2] ^ tab1[4 * j + 2] ^ algn_414802[4 * k]; d = tab0[4 * i + 3] ^ tab1[4 * j + 3] ^ tab2[4 * k + 3]; l = a^chaine[-4]; if (((byte_414C01[4 * l]^b) == chaine[0]) && ((tab3[4 * l+2]^c) == chaine[4]) && ((tab3[4 * l+3]^d) == chaine[8])) { chaine[-4] = i; chaine[0] = j; chaine[4] = k; chaine[8] = l; return; } } MessageBoxA(0,"ERRRROOOOOR",0,0); } /* ouais ouais je sais j'étais pas inspiré ... */ void lolzilolzion(PBYTE chaine) { PBYTE magictab = chaine + 4; int i; for(i = 4; i ; i--) { bfser(magictab); ++magictab; } log("fin bfser :",chaine); }
et la fonction inverse la grosse substitution est donnée par :
void secondfun(PBYTE chaine) { int i; // on oublie pas de faire décroître l'indice et non de le faire croître comme dans la fonction originale for (i = 8; i >= 0 ; i --) { lolzilolzion(chaine); rocknrol((DWORD*)chaine); substitution(chaine,i*0x100); } }
Dernier pas
Les dernières opérations sont une substitution, à nouveau un ror des valeurs et enfin la même substitution que celle du début. Une fois tout ces éléments en main, il suffit d'utiliser nos fonctions inverses dans l'ordre inverse de celui du chiffrement et on obtient la fonction de déchiffrement. Voici le code complet de mon "keygen" :
#include "main.h" #include <windows.h> #define ROL(a,b) ((a << b) | (a >> (32 - b))) #define tab3 ((BYTE*)0x414C00) #define tab2 ((PBYTE)0x414800) #define tab1 ((PBYTE)4277248) #define tab0 ((PBYTE)4276224) #define byte_414000 ((PBYTE)0x414000) #define byte_414400 ((PBYTE)0x414400) #define byte_414800 ((PBYTE)0x414800) #define byte_414001 ((PBYTE)0x414001) #define byte_414401 ((PBYTE)0x414401) #define byte_414801 ((PBYTE)0x414801) #define byte_414C01 ((PBYTE)0x414C01) #define algn_414802 ((PBYTE)0x414802) #define byte_414C01 ((PBYTE)0x414C01) #define byte_408138 ((PBYTE)0x408138) #define byte_408139 ((PBYTE)0x408139) #define byte_40813A ((PBYTE)0x40813A) #define stru_40813B ((PBYTE)0x40813B) #define byte_411138 ((PBYTE)0x411138) #define byte_411139 ((PBYTE)0x411139) #define byte_41113A ((PBYTE)0x41113A) #define stru_41113B ((PBYTE)0x41113B) void log(char* mess, PBYTE chaine) { char resultat[40]; static HANDLE hFile = NULL; int i; DWORD NumberOfBytesWritten; if (! hFile) hFile = CreateFileA("youpi.txt",(GENERIC_READ | GENERIC_WRITE),FILE_SHARE_READ,NULL,CREATE_ALWAYS,0,NULL); for (i = 0; i < 16 ; i++) { wsprintfA(&resultat[2*i],"%02X",chaine[i]); } WriteFile(hFile,mess,strlen(mess),&NumberOfBytesWritten,NULL); WriteFile(hFile,"\x0D\x0A",2,&NumberOfBytesWritten,NULL); WriteFile(hFile,resultat,33,&NumberOfBytesWritten,NULL); WriteFile(hFile,"\x0D\x0A",2,&NumberOfBytesWritten,NULL); } void bfser(PBYTE chaine) { DWORD i,j,k; for(i = 0; i <= 0xFF ; i++)for(j = 0; j <= 0xFF ; j++)for(k = 0; k <= 0xFF ; k++) { BYTE a,b,c,d,l; a = byte_414000[4 * i] ^ byte_414400[4 * j] ^ byte_414800[4 * k]; b = byte_414001[4 * i] ^ byte_414401[4 * j] ^ byte_414801[4 * k]; c = tab0[4 * i + 2] ^ tab1[4 * j + 2] ^ algn_414802[4 * k]; d = tab0[4 * i + 3] ^ tab1[4 * j + 3] ^ tab2[4 * k + 3]; l = a^chaine[-4]; if (((byte_414C01[4 * l]^b) == chaine[0]) && ((tab3[4 * l+2]^c) == chaine[4]) && ((tab3[4 * l+3]^d) == chaine[8])) { chaine[-4] = i; chaine[0] = j; chaine[4] = k; chaine[8] = l; return; } } MessageBoxA(0,"ERRRROOOOOR",0,0); } void lolzilolzion(PBYTE chaine) { PBYTE magictab = chaine + 4; int i; for(i = 4; i ; i--) { bfser(magictab); /* ser0 = magictab[-4]; ser1 = magictab[0]; ser2 = magictab[4]; ser3 = magictab[8]; new0 = byte_414000[4 * ser0] ^ byte_414400[4 * ser1] ^ byte_414800[4 * ser2] ^ byte_414C00[4 * ser3]; new1 = byte_414001[4 * ser0] ^ byte_414401[4 * ser1] ^ byte_414801[4 * ser2] ^ byte_414C01[4 * ser3]; new2 = tab0[4 * ser0 + 2] ^ tab1[4 * ser1 + 2] ^ algn_414802[4 * ser2] ^ tab3[4 * ser3 + 2]; new3 = tab0[4 * ser0 + 3] ^ tab1[4 * ser1 + 3] ^ tab2[4 * ser2 + 3] ^ tab3[4 * ser3 + 3]; magictab[-4] = new0; magictab[0] = new1; magictab[4] = new2; magictab[8] = new3;*/ ++magictab; } log("fin bfser :",chaine); } BYTE getInv(int i,BYTE b, int j, int indice) { int k; for (k = 0; k < 0x100 ; k++) if (byte_408138[i+(k+indice)*16+j] == b) return k; MessageBoxA(0,"ARZOOOOOO",0,0); DebugBreak(); return 0; } void substitution(PBYTE chaine, int indice) { int i; PBYTE v4 = chaine + 1; for (i = 0; i < 16 ; i+=4) { v4[i- 1] = getInv(0,v4[i- 1],i,indice); v4[i] = getInv(1,v4[i],i,indice); v4[i+1] = getInv(2,v4[i+ 1],i,indice); v4[i+2] = getInv(3,v4[i+ 2],i,indice); } } void rocknrol(PDWORD chaine) { int i; for (i = 3; i >= 0 ; i--) chaine[i] = ROL(chaine[i],8*i); } void secondfun(PBYTE chaine) { int i; for (i = 8; i >= 0 ; i --) { lolzilolzion(chaine); rocknrol((DWORD*)chaine); substitution(chaine,i*0x100); log("substitution :",chaine); } } void firstfun(PBYTE chaine) { int i; PBYTE v4 = chaine + 1; for (i = 0; i < 16 ; i+=4) { v4[i- 1] = getInv(36864,v4[i- 1],i,0); v4[i] = getInv(36865,v4[i],i,0); v4[i+1] = getInv(36866,v4[i+ 1],i,0); v4[i+2] = getInv(36867,v4[i+ 2],i,0); } log("fin first fun :",chaine); } void DLL_EXPORT getIt() { BYTE chaine[] = {0x68,0x61,0x63,0x6B,0x2E,0x6C,0x75,0x2D,0x32,0x30,0x30,0x39,0x2D,0x63,0x74,0x66}; BYTE cpy[0x10]; int i; DebugBreak(); for (i = 0; i < 4 ; i++) { cpy[i] = chaine[i*4]; cpy[i+4] = chaine[i*4+1]; cpy[i+8] = chaine[i*4+2]; cpy[i+12] = chaine[i*4+3]; } log("transfo1 :",cpy); rocknrol((DWORD*)cpy); log("rocknrol :",cpy); firstfun(cpy); log("firstfun :",cpy); secondfun(cpy); for (i = 0; i < 4 ; i++) { chaine[i*4] = cpy[i]; chaine[i*4+1] = cpy[i+4]; chaine[i*4+2] = cpy[i+8]; chaine[i*4+3] = cpy[i+12]; } log("résultat :",chaine); MessageBoxA(0,"youpi !","YE SOUIS DIABOULIQUE !!!",0); } // a sample exported function void DLL_EXPORT SomeFunction(const LPCSTR sometext) { MessageBoxA(0, sometext, "DLL Message", MB_OK | MB_ICONINFORMATION); } BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: DebugBreak(); getIt(); break; case DLL_PROCESS_DETACH: // detach from process break; case DLL_THREAD_ATTACH: // attach to thread break; case DLL_THREAD_DETACH: // detach from thread break; } return TRUE; // succesful }
Une fois le code compilé et injecté dans le keygen me, la dll créée un fichier de log contenant les différentes étapes du calcul ainsi que le serial : 192EF9E61164BD289F773E6C9101B89C
On est donc content et on envoie tout ça ! Au final il m'a fallut 5 heures pour venir à bout de ce crack me tout mignon :D merci J-B pour ce keygen-me qui m'a donné l'occasion de faire un peu autre chose que de l'unpacking ;). Un grand merci aussi à l'équipe de Hack.lu pour ce challenge et pour le T-Shirt de la conf et l'abonnement à MISC (que je n'ai pas encore reçu mais bon, je ne désespère pas :D )
Commentaires
Salut Baboon,
Merci pour le descriptif, très intéressant. Respect!
Salut,
Thierry
PS.
"Enfin une chose à ne jamais faire est de remplacer un saut conditionnel par son inverse"
Désolé mais ta conclusion est bizarre, jamais faire? Si on arrive à cracker une protection aussi facilement, je le conseille vivement. D'auilleurs cela me rappelle les années Hexworkshop32.
Si le but est d'arriver à bypasser une protection le plus effectivement (temps vs reussité) possible et que la possibilité existe de le faire avec le bon vieu JN/JNZ, pourquoi pas.
Le style ne joue rarement un role* si on est arrivé au but.
Comme je l'ai dit dans mon post parce que ça n'a aucun sens
Si on veut qu'une branche soit toujours prise, on met un jmp
Dans ton cas tu remplace un je par un jne, ce qui, encore comme dans mon post, équivaut à remplacer la phrase "si le code est bon, affiche good boy" par "si le code est mauvais, affiche good boy"
Autant directement dire : "que le serial soit bon ou mauvais, afficher le message de réussite" ce qui correspond à remplacer le saut conditionnel par un saut inconditionnel
Je ne discute pas l'intérêt de patcher à la tronçonneuse, je suis le premier à le faire. Je dis juste que remplacer un saut conditionnel par son inverse n'a aucun sens car ce que l'on souhaite ici c'est forcer le suivit de la branche
Bravo pour la solution. Tu as été le premier à le résoudre, je crois.
L'algorithme est en fait un AES (pas un DES) en boîte blanche. Il est possible d'extraire la clé facilement, je n'ai pas forcé la difficulté pour que le challenge puisse être résolu pendant la conférence. Si tu as la motivation, essaie de l'algo en entier, voire de sortir la clé. Ca ferait un bon post =)
Encore une fois, félicitations. Je ne m'attendais pas à une solution de ce type.
Au passage, oui, le patch était trivial, mais ce n'était pas le but de ce challenge. Le code est justement en clair pour que les participants se concentrent sur l'algorithme de vérification.
Je me demandais justement combien avait trouvé la solution ...
Merci pour ton commentaire ca fait toujours plaisir :D
Sinon je disais DES pour citer un nom de chiffrement symétrique, je savais que ce n'était pas lui mais je ne savais pas non plus que c'était de l'AES si j'avais été plus assidu à mes cours de crypto j'aurai peut être reconnu l'algo ... Là ca a été un peu la solution brutale
Bref je retient ton idée de post et je ressors mes cours
Salut,
désolé de ce petit hors sujet, mais firefox refuse d'ajouter ce blog à mes RSS (alors qu'il peut le faire avec les comments)
Je pense que c'est un petit bug à corriger ^^
J'utilise FireFox 3.5.5
Merci
Apparemment ca chie avec firefox en effet ...
J'ai aucune idée du pourquoi du comment
J'utilise les flux rss de dotclear donc j'ai pas trop de solution pour toi à part passer à opéra le seul vrai navigateur
Bon en fait y'a bon, les flux étaient des flux atom, maintenant il y a le choix entre flux rss et atom, les flux RSS allant bien avec firefox 3