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.

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