Le crack-me en question est packé avec UPX et commence par un petit layer avec des instructions SSE 2 qui embêtent bien olly mais pas le fourbe que je suis qui n'a même pas pris le temps de les étudier mais s'est contenté de lancer le binaire puis de le mettre en pause.

Pas d'anti-debuggers particuliers (aucun ne s'est manifesté avec HideDebugger).

Passons au vif du sujet, la routine de vérification se trouve à l'adresse 00401342 du crack-me qui commence par récupérer le serial, vérifier que sa taille est bien de 32 caractères et enfin le mettre en majuscule.

Le nom est ensuite récupéré et doit faire au moins 3 lettres (pourquoi 3... je ne sais pas ... surement pour faire chier les jB et autres crackers aux pseudos atrophiés (meuh non s'pas méchant)). Enfin le nom est hashé à l'aide de MD2 et des APIs cryptographiques de windows (CryptAcquireContext / CryptCreateHash / CryptHashData / CryptGetHashParam).

Le serial est transformé en hexadécimal à l'aide de la routine en 00401771, rien de spécial.

Une transformation du hash du nom est créé à l'aide de l'algo suivant :

  1. DWORD hh[4];
  2. DWORD h[4] = MD2(Name);
  3. d = 0x4E4F44
  4. for (i = 0; i < 4; i++)
  5. {
  6. d = ROR(d,0xC);
  7. hh[i] = ((~(0x45534554^d))&0x455353) | h[i];
  8. }

Et là on entre dans le vif du vif du sujet. Le serial est en effet accepté ou non après l'exécution d'une VM et par la comparaison de sa sortie avec le hash du nom.

La VM est exécutée en 004015C4 et son byte code est situé en 0040305C.

Cette VM n'est pas très compliquée à étudier et on trouve rapidement le format de ses instructions :

grp_1 :

*format de l'instruction*
aaa ---- ccc --- bbb

a==0 => retn
a==1 => call if flag(b)
a==2 => InvFlag(b)
a==3 => SetFlag(b)
a==4 => flag(c) = flag(c) & flag(b)
a==5 => flag(c) = flag(b)

grp_2 :

*format de l'instruction*
aaa ddddd ccccc bbb

a==6 => flag(b) = bit(regs[d], c)
a==7 => bit(regs[d], c) = flag(b)

Comme on le voit ici le nombre d'instruction est très réduit, par contre le nombre d'instructions et quand à lui TRES élevé (195,545 instructions, rien que ca).

J'ai donc commencé à coder plusieurs émulateurs en python,

Le premier se contentait de garder en mémoire l'ensemble des opérations faites sur chacun des bits : FAIL python arrivait très vite à bout de mémoire

Le deuxième comportait un simplificateur d'équations booléennes plutôt pas mal foutu mais hélas re FAIL, la complexité des équations explose très vite et mon émulateur n'avance plus.

Pensant que le problème venait de mon simplificateur, je me suis rabattu sur une lib en python implémentant l'algorithme de Quine-McCluskey et ai créé dans la foulée une petite interface de debug mais re-FAIL : la complexité reste trop élevée.

J'ai enfin bridé mon émulateur pour arrêter la simplification des équations à partir d'une certaine complexité et amélioré mon interface de debug et enfin j'ai pu étudier le code exécuté par la VM (tout en sachant que mon émulateur a continué à être modifié / customisé tout au long de l'étude pour effectuer différents tests etc.).

Mon émulateur est disponible à la fin de l'article en pièce jointe et les différentes commandes utilisables sont détaillées ici :

  • d : désassemble la ligne courante
  • d1 : toutes les instructions exécutées seront affichées
  • d0 : annule la commande précédente
  • r : entre et quitte le mode pas à pas
  • c : continue l'exécution
  • g : affiche l'état des différentes variables (ghh affiche le hash modifié, gss le serial modifié, gh le hash etc.)
  • q : termine l'émulation
  • y/n : prend le saut ou non
  • p : permet l'exécution de commandes python (ex : pgetRepr(flags[1]))

Après un certain temps d'analyse, on arrive enfin à comprendre le fonctionnement du code :

  1. s[0] ^= h[1];
  2. s[2] ^= h[3];
  3. ss[1] = s[1]*hh[2];
  4. ss[3] = s[3]*hh[0];
  5. ss[0] = s[0]*hh[3];
  6. ss[2] = s[2]*hh[1];
  7. ss[1] ^= h[0];
  8. ss[3] ^= h[2];

la multiplication est effectuée à l'aide de la technique de multiplication dans l'Égypte antique.

A la fin de l'exécution, le serial modifié (DWORD ss[4]) doit être égal au hash du nom (DWORD h[4]), l'algorithme est donc simplement inversé à l'aide de l'algo suivant :

  1. DWORD findMagic(h,hh)
  2. {
  3. DWORD ss = 0, s = 0;
  4. int i;
  5.  
  6. for (i = 0; i < 32; i++)
  7. {
  8. if (getbit(h,i) != getbit(ss,i))
  9. {
  10. if (!getbit(hh,i))
  11. {
  12. printf("Sorry I can't find any serial for this name :/\n");
  13. exit(EXIT_FAILURE);
  14. }
  15. ss += hh;
  16. s += 1 << i;
  17. }
  18. hh <<= 1;
  19. }
  20. return s;
  21. }
  22.  
  23. s[0] = h[1]^findMagic(h[0], hh[3]);
  24. s[2] = h[3]^findMagic(h[2], hh[1]);
  25. s[1] = findMagic(h[1]^h[0], hh[2]);
  26. s[3] = findMagic(h[3]^h[2], hh[0]);

Et enfin comme je me suis bien fait chier à résoudre ce crackme, je me suis dit que j'allais coder un petit keygen (en pièce jointe à la fin de l'article encore une fois), j'espère que vous apprécierai le graphisme et la musique ;)

Bon, je sais que ce crackme est hyper spécifique et que vous n'aurez pas appris grand chose aujourd'hui si ce n'est comment trouver l'inverse d'un entier modulo 0x100000000 (supaire !) mais j'espère avoir le temps de vous pondre un truc un peu plus intéressant dans les jours / semaines / mois à venir ;)