TL;DR

Le jeu "démineur" est un classique de l'entrainement du reverse engineer. Le principe est simple, comprendre comment le code fonctionne afin de pouvoir afficher la position des mines.

Init

Première étape: récupérer le binaire.

2 solutions pour ça

  • soit vous avez un vieux windows XP sous la main et le fichier se trouve  ici (%systemroot%\system32\winmine.exe)
  • soit vous le téléchargé depuis ce lien

Dis moi Jammy, c'est quoi qu'on veux ?

Comme souvent avant de se lancer tête la première dans le binaire on va essayer de définir ce qu'on cherche:
Ce qu'on veut c'est trouver le moyen de savoir ce que chaque case contient afin de pouvoir déterminer celle qui contiennent une mine.
On cherche donc une structure de données en mémoire. Intuitivement on pense à un tableau où chaque indice représente une case et la valeur représentent son contenu.
Cette structure à de fortes chances d'être remplie de manière aléatoire pour chaque partie.

Dis moi Jammy, c'est quoi qu'on sait ?

On va d'abord vérifier que certaines protections sont désactivés (comme l'ASLR à tout hasard) (ASLR et hasard, c'est bon t'as la vanne ?)

Pour ce faire j'utilise le script PowerShell "PESecurity".

Comme on peut le voir, il n’y a aucune protection sur ce binaire, c'est une bonne chose, ça nous facilitera la vie.

Trouver le générateur de champ de mines

Maintenant intéressons nous au DLL que le programme utilise.

NB: On peut remarquer l'utilisation d'une DLL appellée msvcrt. Cette DLL fait partie des DLL par défaut de "Microsoft Visual Studio 6.0". Donc il y a de forte chance que ce programme ait été compilé avec Visual Studio.

Parmi la liste des fonctions importées on peut voir à la toute fin, les fonctions rand et srand.  Une hypothèse séduisante est de dire que c'est fonction servent à fabriquer le champs de mine aléatoire de chaque partie.
Explorons cette possibilité.

En lissant la documentation de ces 2 fonctions on s'aperçoit que seule la fonction rand va vraiment nous servir.

Il ne reste plus qu'a regarder où est utilisé cette fonction. Avec un peu de chance on va tomber sur le code qui est lancé à chaque partie pour générer le champ de mine.

Comprendre comment est généré le champs de mines

Si l'on s'attarde sur la fonction rand(), on peut déduire les choses suivantes:

Pour résumer, rand_ "randomise" un nombre et effectue ensuite un modulo, qui s'assure que le résultat ne dépasse pas la valeur dans arg_0.

Maintenant il faut trouver où cette fonction est utilisée et comment.

Trouver l'appel à rand

Pour ça c'est pas dur, il suffit de remonter les cross references jusqu'à la fonction qui a appellée rand.

Il est temps de faire une analyse dynamique, afin de savoir l'utilité des 2 valeurs poussées sur la stack avant l'appel à rand().

Comme on peut le voir, mon hypothèse était juste,  puisqu'ici la valeur du dword_1005334 et du dword_1005338 représente la largeur et la longueur de mon champ de mines (dans le cas d'une partie classique).

On peut aussi voir l'adresse du tableau qui contient le champ de mines.

Si on regarde ce tableau après sa génération, on voit qu'il commence et qu'il finit avec les mêmes valeurs.

De l'observation de ce tableau généré suivant plusieurs tailles de champs de mines on peut déduire beaucoup de choses:

  • Le tableau déclaré en mémoire à une taille fixe
  • La tableau commence à 0x1005340 et finit à 0x10056A0
  • La partie du tableau utilisée est délimitée par une série de 10h au début et à la fin
  • Le nombre de valeur 10h est proportionnel à la taille du tableau; ce qui pourrait indiquer les bordures du tableau.
  • Il y a autant de valeur 8F que de mines: c'est certainement la valeur qui indique l'emplacement d'une mine

Comme souvent en reverse, il y a une part de supposition... On va donc vérifier nous déductions en écrivant un petit programme qui va lire le tableau en mémoire.

Pour ce programme j'ai choisi de l'écrire en C sous visual studio afin d'avoir facilement accès a l'API windows.

Tout d'abord, nous devons ouvrir le processus du jeu. Pour ce faire, nous utiliserons FindWindow et pour obtenir le handle de notre programme, puis nous la passerons à GetWindowThreadProcessId afin d'obtenir le dwProcessId, enfin nous pourrons passer cette valeur à l'API OpenProcess.

HWND window = FindWindow(NULL, "Minesweeper");
if (window == NULL)
    return wprintf("[-] Failed to find Minesweeper process");
 
GetWindowThreadProcessId(window, &dwProcessId);
HANDLE process = OpenProcess(PROCESS_VM_READ, FALSE, dwProcessId);

Ensuite, nous allons réserver de la mémoire afin de stocker les informations du champ de mines.

LPBYTE buffer = (LPBYTE)malloc(size);
if (buffer == NULL)
    return wprintf("[-] Failed to allocated memory");

Et enfin, on va mettre en place une boucle infinie pour lire la mémoire du démineur, comme ça on pourra reporter tous les changements dans notre affichage.

while (true) {
        BOOL ret = ReadProcessMemory(process, (LPVOID)start, buffer, size,
            &dwRead);
 
        if (ret == NULL) return wprintf("[-] Failed to read memory");
        
        BYTE field = NULL;

        for (size_t i = 0, j = 0; i < size; i++, j++) {
            if (j == 0x20) {
                puts("");
                j = 0;
            }
            printf(%c;", buffer[i]);
        }
        Sleep(1500);
 
        system("cls");
    }

En bidouillant un peu le code, on peut arriver à trouver les réponses qui nous manquent, comme par exemple: à quelle valeur correspond un drapeau ?

Rassurez-vous si vos premiers affichages ne sont pas exceptionnels, les miens non plus.

La preuve !

L'astuce c'est de faire afficher le code hexa de chaque case du tableau et de jouer avec le démineur. A force de "try and guess" on retrouve les informations utiles

Et voilà le résultat final !

Sources

Le code source de ce petit cheat est bien sur disponible sur mon git.

"Feel free to use it !" comme disent les américains

Social et Media

Comme toujours je suis disponible sur Twitter et cie, si vous avez des questions, des remarques, des suggestions etc. N’hésitez pas !

Twitter: @GhostAgs

Discord: hackraw