Ce week end s’est tenu l’édition 2013 du CSAW-CTF. Bien ! J’y ai pas fait grand chose (as usual), mon week end dans la vie réelle étant chargé. J’ai eu cependant ce petit reverse de 300Pts pour BigDaddy ;) au final GG les gars, 6800 Pts, 18eme ex aequo sur 1387.
Voici la petite histoire;
Ce même vendredi Yodzeb n’était plus tenable, il a commencé a désosser tout seul le chall reverse 300 au GDB (oui, c’est un genre de geek reverveur sado maso). Il a passé tout l’après midi à tenter de trouver le password de façons détournée. Celui-ci était caché dans un crackme accessible à distance qui quand on lui donne le bon password, va lire et affiche un fichier. Dès lors il est apparu qu’a part brute forcer le mot de passe on n’arriverai pas a grand choses car le mot de passe passe dans une moulinette du diable qui shift et add les octets.
En clair on commence avec un magic number 0x539h (1337), on le multiplie par 32 et on additionne le caractère de notre password. On rajoute au résultat le résultat de l’opération du dernier tour. et on recommence pour le caractère suivant. Le tout étant potentiellement écrété naturellement par les registre 32bits.
Au final il faut tomber sur 0xEF2E3558h sinon on perd. Il était possible de trouver rapidement cette fonction en reverse statique, le programme ne cachant pas grand choses. Sous IDA il suffit d’afficher les strings, de double cliquer sur le message “Invalide registration code” puis X et on y est. Apres le code de hashing vu plus haut, si EDX est égal au chiffre attendu on gagne.
Voici la version ‘perl’ du dit algorithme (c) YodZeb.
$start=0x539; foreach $c (split //) { if ($c!~/\n/) { $old=$start; $start=( ($old << 5) + $old + ord($c) ) ; $start &= 0xFFFFFFFF; } }
Avec ce genre de moulinette tôt ou tard on tombe sur une collision, la version ‘Perl’ de cet algo n’ayant pas fait mouche avec une juteuse wordlist, et n’ayant pas les compétences mathématique pour le faire élégamment (Si tenté que l’on puisse); Allons y pour un brute forçage plus académique. Avec un peu d’optimisation je suis arrivé à 19 millions de hashs/sec sur un seul CPU de mon petit AMD Phenom II et je monte a 28 avec un peut de nettoyage de code C pour le write-up.
C’est l’occasion de monter comment on appelle une routine ‘pure assembleur’ en C. Ca pourra toujours re-servir. Première étape ré-encoder la fonction en ASM. Pour que cela dépote plus,. L’idée est de virer le plus possible les accès mémoire dans la boucle interne et de garder toute la mamaille dans des registres. Voici ma version du hash, plus ‘concise’ :
;File hash.asm section .text GLOBAL mybrute mybrute: ; Normalement il faut un appel ; qui sauve ebp et esp ; Mais ici on touchera pas a la pile ; on s'en passe et on libere un registre ;[esp] = retour addr ;[esp+4] = mystring mov esi, dword [esp+4] ; mystring xor eax,eax ; pour nettoyer la partie haute d'eax lodsb ; On increment ESI, on met le 1er char dans AL mov ebx, 0x539 ; Ebx sera le checksum, on y met le magicnumber .myloop xor ecx,ecx ; xor 32 suivis de mov 8... mov cl,al ; equivalent a movzx mais en moins de cycles mov edx,ebx ; [currentsum] est ebx shl edx,5 ; Multiplication add ecx,edx ; Addition avec char add ebx,ecx ; Addition avec checksum lodsb ; pointer+1, next char dans al test al,al ; est t'on a la fin de la stringZ ? jnz .myloop ; Ici EAX est a O, en C, 1 c'est true cmp ebx, 0xEF2E3558 ; TEST pour la string mystère je .good ret .good: inc eax ret
Tout ce qu’il faut dans le fichier ASM c’est déclarer la fonction pour l’exportation avec le mot clef GLOBAL (voir ligne 4), la convention d’appel en C pour 32bits passera les paramètres de la fonction sur la pile. ici je vais appeller ma fonction avec comme paramètre une string. Pour les fans qui veulent se renseigner sur le grand bordel que sont les convention d’appel, il y a une page Wikipedia pour cela. bref il nous suffira de faire.
mybrute(‘toto’);
Sur la pile j’aurai donc dans l’ordre l’adresse de retour puis l’offset de cette string. au retour le C vérifie eax qui est a 0 false ou 1 pour true. EBP et ESP doivent être préservés.
voici le code C autour;
/* brute.c (c) Thanat0s.trollprod.org / 2013 // jusque 6 char printable 742.912.017.120 742 Milliards de combinaisons... */ // ************ LIB *************** #include <stdio.h> #include <string.h> #include <stdlib.h> extern mybrute () asm ("mybrute"); // ********* Variables ************* int g_bf_len, g_char_max,pass_len, g_char_min; // ************ CODE *************** // Hexadecimal print void hexprint(unsigned char buf[255],int x) { int i; for (i = 0; i < x; i++) { if (i > 0) printf(":"); printf("%02X", buf[i]); } } // BruteForce loop from AAA to ZZZ void brute_force(int max_len,int char_max,int min_char) { unsigned char tmp_buff[255]; int last_char, count, idx = 0; // remplis le buffer de AAAA for (idx = 0; idx <= max_len; idx++) { tmp_buff[idx] = min_char; } tmp_buff[0]--; // Fixup 1er Char // Big Loop.. tant que le dernier char est <... while (tmp_buff[max_len] <= min_char ) { tmp_buff[0]++; // inc char // Scan et inc/dec char autour for (idx = 0; idx < max_len; idx++ ) { if (tmp_buff[idx] > char_max ) { // inc char suivant tmp_buff[idx] = min_char; tmp_buff[idx+1]++; } } last_char = tmp_buff[max_len]; // Create Stringz tmp_buff[max_len]=0; // TEST print si ok if (mybrute(tmp_buff)) { printf("\nGot a Winner ---->"); hexprint(tmp_buff,idx); printf ("<->%s<---- WIN\n",tmp_buff); exit(0); // Break on WIN } tmp_buff[max_len] = last_char; } } // Main programm int main() { for (pass_len = 1; pass_len <= 25 ; ++pass_len) { // Bruteforcons de 1 a 25 Char (mec patient) g_char_max = 126; // Maximum tilda g_char_min =32; // min char commence au espace brute_force(pass_len,g_char_max,g_char_min); } printf("Welcome in 2019, i didn't find it\n"); return 0; }
Et le makefile qui va bien;
all: brute force: clean brute clean: rm brute *.o brute: brute.c hash.o gcc -c brute.c gcc hash.o brute.o -o brute chmod +x brute hash.o: hash.asm yasm -f elf -m x86 hash.asm -o hash.o
Avec ça, et la combinaison juteuse de tous les caractères printables de 0x20h ‘espace’ à 0x7Eh ’tilda’ je m’en suis sortis avant que je n’eu a me lancer dans une laborieuse version multi-threadé. Le mot de passe est apparu en pleine nuit en moins de 4h. La version ‘propre’ le fait en un peu plus de 2h.
$ time ./brute Got a Winner ---->62:33:20:36:3E:36<->b3 6>6<---- WIN real 124m36.048s user 124m1.713s sys 0m0.000s
Ce genre de truc donne envie de mettre le doigt dans Opencl la prochaine fois !
Au flag, on sent que quelque part un soft utilise cela… mais lequel !
Enter registration code: b3 6>6 Thank you, valued customer! Your key is: day 145: they still do not realize this software sucks