En plus d’être narcissique comme le titre le sous-entend, j’ai un côté sombre. En plus d’être un windowsien à mes heures perdues (bouuuh! :P) je suis friand de Reverse Engineering. Est-il besoin de le rappeler ?
Sans m’attarder sur pourquoi j’adore ça / je fais ça (j’en parlerais peut-être si on me pose vraiment la question), je vais rentrer dans le vif du sujet avec quelque chose que j’ai essayé de faire il y a 2 ans et que je vais recommencer aujourd’hui : Faire de la rétro-ingénierie sur Guild Wars 2.
Guild Wars 2 est un MMORPG développé par ArenaNet et sorti en Juillet 2012. Il a reçu plusieurs mises à jour de contenu et ne cesse d’avoir du contenu publié depuis. C’est un jeu en vogue auquel j’ai passé beaucoup d’heures (beaucoup trop !). Il est temps de rentabiliser ça.
Le hic, c’est que j’avais commencé quelque chose sur la version x86 du binaire (compilée sur 32-bit, donc). J’étais dans une petite zone de confort (mais pas trop, car le reverse, c’est long et chiant). Depuis, il y a le binaire x64. Je me dis que c’est l’occasion d’apprendre de nouvelles choses et de me heurter à une difficulté certaine.
Dans cette tribune je vais détailler pas à pas quelques tripatouillages pour comprendre comment fonctionne le client du jeu.
Le débogueur à la rescousse
Ici il est question de s’attaquer à quelque chose tout en évitant de dépasser la ligne rouge. Et puis il faut des outils tranchants pour désosser une partie d’un programme dont on ne dispose pas des sources.
Ceux qui ont connu Ollydbg ou Immunity Debugger pour reverse des binaires 32-bit seront ravis d’apprendre qu’il existe x64dbg avec une expérience utilisateur similaire pour les binaires 64-bit. Evidemment, il y a aussi l’incontournable IDA. Dommage qu’il ne soit pas gratuit ou open source, mais peut-on en vouloir à des gens qui veulent en vivre ?
Pour le moment, parce que je suis une tête brûlée, je vais commencer par utiliser x64dbg. On ouvre le binaire du client du jeu et on atterrit sur une fenêtre comme celle-là :
C’est pas beau, hein ?
Grâce à mes talents sur Paint, je vous ai numéroté les 4 zones principales quand on analyse le code binaire d’un exécutable.
- C’est le code désassemblé, lui-même découpé en quatre colonnes. De gauche à droite, on a les adresses mémoires, les opcodes représentés en hexadécimal (parce que le binaire, c’est bien, mais c’est masochiste à ce niveau), les mnémoniques correspondants et des indications textuelles pour aider à la compréhension de la micro-instruction concernée.
- Il s’agit du contexte du thread courant, notamment la valeur des registres généraux ou encore des flags processeurs.
- Il s’agit d’une zone permettant de faire un dump mémoire. Un peu comme dans un éditeur hexadécimal. Contrairement à la fenêtre 1, dans cette fenêtre, on considère qu’on a des données alors que dans la fenêtre du dessus, c’est supposé être du code exécutable.
- Il s’agit d’une zone un peu comme la numéro 3, sauf qu’on y représente par convention la stack, la pile d’exécution. Par ailleurs, vous remarquerez que l’adresse surlignée en noir,
000000E52F92F4A0
, est la valeur qui est contenue dans le registreRSP
au niveau de la fenêtre 2. Ca nous permet d’avoir un visuel sur l’état de la stack à chaque déroulement d’instructions. Pratique, hein ?
Si vous ne savez pas ce qu’est la stack, je vous renvoie ici : https://zestedesavoir.com/articles/97/introduction-a-la-retroingenierie-de-binaires/
Vous ne comprenez pas tout ? Ce n’est pas grave. Je n’ai pas le temps de passer en revue chaque détail. Vous comprendrez ceux-ci de vous-mêmes au fur et à mesure de ma progression. A vrai dire moi-même je ne sais pas tout ce que je fais. On y va ensemble à tâtons.
Un clic sur l’onglet Memory Map dans la barre juste au-dessus des fenêtres 1 et 2 nous mène sur une nouvelle vue comme vous pouvez le voir ci-dessous :
Les colonnes parlent d’elles-mêmes. Ici on va parler de segments mémoires qui comportent une adresse, une taille, une information sur son nom/son contenu, son type, son niveau de protection actuel et initial.
Par niveau de protection d’un segment mémoire, on désigne les différentes permissions que le processeur possède sur ces segments :
- R: initiale de
Read
, il s’agit de la possibilité de lire la mémoire. - W: initiale de
Write
, il s’agit de la possibilité d’écrire la mémoire. - E: initiale de
Execute
, il s’agit de la possibilité d’exécuter la mémoire (qui contient du code exécutable, très probablement, à 99.99%, donc).
J’ignore ce à quoi fait référence le C
, mais la lecture des sources de x64dbg
est laissée à l’appréciation du lecteur qui a du temps à perdre à lire ma maudite tribune.
On peut voir aussi que dans l’espace mémoire du processus, il y a les sections de gw2-64.exe
mais aussi de usp10.dll
ou encore de gdiplus.dll
. Et il y en a encore d’autres. Les DLL qui sont utilisées par le processus en cours sont chargées dans son espace d’adressage. Et c’est pareil pour les autres processus qui sont en cours d’exécution sur votre machine Windows.
En fait, sur nos systèmes d’exploitations récents, les processus utilisent de la mémoire virtuelle. Ils ont l’illusion d’accéder à toute la mémoire adressable du système d’exploitation. Lorsqu’une zone mémoire est sollicitée via son adresse, celle-ci, qui est virtuelle, va être traduite en adresse mémoire physique. Ce mécanisme est géré par la Memory Management Unit, qu’on abrège souvent en MMU. Elle a pour but d’isoler chaque processus pour éviter qu’un processus ne vienne modifier directement la mémoire d’un autre processus.
Et comme une DLL est partagée entre plusieurs processeurs, son code résidera au même emplacement physique en mémoire mais chaque processus qui l’utilise l’aura dans son espace d’adressage virtuel, et peut-être à des adresses mémoires différentes.
… C’est pas clair, hein ?
Allez, un exemple ! Ici, sur gw2-64.exe, on voit que la DLL usp10.dll
est chargée à partir de l’adresse mémoire 00007FFA993F0000
. Dans un autre processus, cette adresse pourrait tout à fait être (au pif) 00007FFA996F0000
. Ces deux adresses virtuelles peuvent être traduites à la même adresse physique (au pif) DEADBEEF00000000
. Il s’agit de la même zone mémoire physique !
Mais si on arrive à écrire dans la mémoire qui correspond à notre DLL partagée entre les autres processus, on en revient donc à pouvoir écrire dans la mémoire des autres processus qui utilisent cette DLL, non ?
Eh bien… Pas tout à fait ! Vous vous douteriez bien qu’il s’agirait autrement d’une faille de sécurité très sévère pour un système d’exploitation quelconque !
L’intérêt de partager la mémoire d’une DLL entre les processus qui utilisent cette même DLL fait gagner du temps. Cela serait trop lourd de charger et recharger la même DLL pour chaque processus (en plus d’être gourmand en mémoire). Mais si une modification d’une zone partagée intervient, le mécanisme de Copy On Write va permettre de copier seulement la page mémoire modifiée dans une autre zone de mémoire physique
et ainsi éviter que les processus utilisant la DLL soient victime des modifications apportées par un processus voisin.
Si on reprend notre exemple (qui reste très théorique), sur gw2-64.exe on a usp10.dll
est chargée à partir de 00007FFA993F0000
et on a dans un autre processus cette même usp10.dll
en 00007FFA996F0000
. Elles font toujours référence à DEADBEEF00000000
. Mais si l’autre processus se met à réécrire la mémoire de usp10.dll
pour une raison X ou Y, alors la page mémoire modifiée par être copiée à un autre emplacement physique, admettons DEADBEED00000000
.
Mais si les adresses physiques changent, les adresses virtuelles, elles, ne changent pas ! Elle est vachement cool, la MMU, quand même sans parler de l’OS qui s’occupe de tous ces détails sordides (mais ô combien croustillants) pour nous !
Ca fait beaucoup d’un coup et on n’a pas beaucoup parlé de Guild Wars 2. Mais il nous faut un minimum de bases pour savoir ce qu’on fait. Et il m’a semblé important d’expliquer comment un processus sous un système d’exploitation comme Windows est grossièrement situé en mémoire. Vous avez tous les détails techniques de l’agencement de la mémoire virtuelle ici : https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces.
Par exemple, on voit que les adresses virtuelles 0000000000000000
jusqu’à 7FFFFFFFFFFFFFFF
correspondent à l’espace mémoire utilisateur de votre processus, tandis que l’espace d’adressage 8000000000000000
à FFFFFFFFFFFFFFFF
correspond à l’espace d’adressage du noyau. A l’instar des DLL, chaque processus avait le noyau chargé en mémoire entre 8000000000000000
et FFFFFFFFFFFFFFFF
. Maintenant, depuis que les vulnérabilités Meltdown et Spectre ont été soulevées, c’est… Un peu plus compliqué !
Enfin ! On a beaucoup parlé de mémoire virtuelle car c’était l’occasion, et je suis au regret de vous dire qu’on va s’arrêter là pour que cette tribune ne prenne pas trop de proportions démesurées. Mais ça tombe bien, j’ai déjà une idée de ce dont je parlerai dans la tribune suivante.
On a vu grosso-modo comment un processus sous Windows était géré au niveau de sa mémoire. Grâce à la MMU, il a l’illusion d’être le seul processus du système et de disposer de l’intégralité de l’espace d’adresse mémoire (utilisateur, bien entendu ! Puisqu’un accès direct à une adresse résidant dans l’espace noyau se soldera par une erreur).
Dans la prochaine tribune, on parlera du format Portable Executable, c’est-à-dire le format qui structure les fichiers exécutables .exe
, .dll
ou encore .sys
.
Score actuel :
Ge0 0 - 1 Guild Wars