« Game Trainer » : différence entre les versions
(Page créée avec « ... ») |
Aucun résumé des modifications |
||
Ligne 1 : | Ligne 1 : | ||
<i>Auteur : Lukas AUGER</i> |
|||
... |
|||
<div style="text-align: justify;">Les « Trainers » sont des programmes ayant pour objectif d’accéder aux adresses en mémoire d’un logiciel afin de lire et de modifier une valeur ou un mécanisme spécifique dans le but d’engendrer un comportement inattendu. On parle souvent de « Game Trainers » car ces derniers sont généralement développés afin de tricher aux jeux vidéo. Le niveau de difficulté quant au développement d’un tel programme varie énormément en fonction du logiciel auquel il s’attaque : plus ce dernier est complet et complexe, plus il est difficile de trouver l’information recherchée. L’impact d’un trainer varie également entre un logiciel en ligne (disposant d’une partie serveur avec laquelle un certain nombre de données sont synchronisées avec le client) et hors ligne (client seul). De surcroît, les méthodes permettant de lutter contre ces attaques ne sont pas les mêmes. |
|||
Parmi quelques exemples applicatifs de ces programmes, on retrouve notamment la possibilité de geler la valeur d’une variable (points de vie, nombres de munitions, et plus globalement les « compteurs » de n’importe quelle nature qu’ils soient) ou de lui faire atteindre des valeurs disproportionnées, mais aussi de provoquer ou d’observer le déclenchement d’événements particuliers. |
|||
Les game trainers diffèrent des codes de triches dans la mesure où ces derniers ont été pensés à cet effet par les développeurs dans le but de faciliter la progression du joueur, ou de lui offrir une nouvelle expérience de jeu. |
|||
= Fonctionnement théorique = |
|||
== L’accès à la mémoire == |
|||
Une des méthodes les plus employées pour la réalisation d’un trainer consiste, dans un premier temps, à identifier l’adresse mémoire liée à la valeur que l’utilisateur souhaite modifier. Pour ce faire, nous utiliserons un scanneur de mémoire du nom de Cheat Engine. Ce logiciel permet de lister toutes les adresses mémoires rattachées à un logiciel précis, de rechercher une adresse en particulier en fonction de différents critères (comme son type) et à l’aide d’opérateurs de comparaison (appliqués lorsque la valeur change), et enfin de modifier la valeur stockée dans cet espace. Il offre de nombreuses fonctionnalités comme la recherche de pointeurs, l’injection de code ou encore le contrôle de la souris notamment utilisé pour la création d’aimbots; certaines d’entre-elles seront abordées dans ce wiki. |
|||
Un problème que nous risquons rapidement de rencontrer réside dans le fait qu’une simple adresse mémoire a de fortes chances d’être différente lorsque le jeu sera relancé, ou plus tard pendant l’exécution du jeu. En connaissant l’adresse d’un pointeur, nous pourrons de ce fait accéder à l’adresse mémoire dans laquelle est stockée la valeur que nous souhaitons lire ou modifier. |
|||
<div style="text-align:center;">[[Fichier:PtrAdrVal.png]]</div> |
|||
Une fois le pointeur déterminé, deux cas de figures peuvent se présenter : l’adresse du pointeur est conservée de manière statique dans la mémoire, ou alors elle l’est de manière dynamique. Pour faire simple, dans le cas de l’allocation de mémoire statique, il est possible de connaître l’emplacement de la ressource dès lors que le programme est compilé (et donc d’y accéder de manière déterministe à chaque lancement du programme), tandis que lors d’une allocation dynamique, il n’est pas possible de prédire quel emplacement mémoire sera utilisé. |
|||
En d’autres termes, l’objectif est d’obtenir une adresse de pointeur de la forme : |
|||
<div style="text-align:center;"><math>'MonProgramme.exe'+Offset=AdresseValeur</math></div> |
|||
Ici, ‘MonProgramme.exe’ est ce qu’on appelle un module principal. Dans certains cas, une adresse statique peut être accédée à partir de l’adresse d’une ressource de la forme ‘MaRessource.dll’ : on parle alors de module tout court, mais le principe reste le même. L’adresse derrière ce module correspond à l’adresse minimale à partir de laquelle le logiciel va pouvoir allouer de la mémoire. D’une certaine manière, il s’agit d’un référentiel qu’on peut facilement connaître à l’aide d’un langage de programmation, et qui constituer le point de départ à partir duquel nous allons accéder à notre valeur. |
|||
La notion d’offset, ou décalage, est généralement exprimé en hexadécimal et correspond on nombre de « sauts » à effectuer dans la mémoire à partir d’une certaine adresse afin d’accéder à une adresse mémoire cible. Le code C suivant illustre simplement cette notion en partant d’un tableau de 200 entiers : |
|||
<pre> |
|||
// ptr = ptr[0] = ptr + 0 |
|||
int *ptr = (int*) malloc(200 * sizeof(int)); |
|||
// ptr8 = ptr[8] = ptr + 8 |
|||
int *ptr8 = ptr + 8; |
|||
// ptr199 = ptr[199] = ptr + 199 |
|||
int *ptr199 = ptr + 199; |
|||
</pre> |
|||
Pour résumer, une adresse statique se compose de l’adresse d’un module que l’on va pouvoir facilement connaître, à laquelle on additionne un certain nombre (offset) afin d’accéder à une adresse en mémoire donc la valeur contiendra ce qui nous intéresse : ici, une autre adresse que nous permettra d’accéder à la valeur recherchée. |
|||
<div style="text-align:center;">[[Fichier:TabStaticPtr.png]]</div> |
|||
Dans le cas où l’adresse du pointeur n’est pas statique, il est nécessaire de remonter plus haut dans la mémoire jusqu’à trouver un pointeur statique : on parle alors de multilevel pointer, ou encore de level-n pointer. |
|||
Enfin, il est possible qu’une adresse statique mène directement à la valeur recherchée, sans passer par un pointeur intermédiaire. Cela n’arrive cependant presque jamais car les programmes pour lesquels des trainers sont développés atteignent rarement un tel niveau de simplicité. |
|||
Nous avons abordé dans les grandes lignes ce que nous souhaitions faire : trouver une adresse statique nous menant jusqu’à notre valeur. Détaillons un peu comment nous y prendre. |
|||
Dans un premier temps, comme vu précédemment, expliquons plus en détail comment trouver l’adresse d’un pointeur. Tout d’abord, nous devons trouver l’adresse mémoire où se trouve la valeur qui nous intéresse. |
|||
<div style="text-align:center;"><math>Adresse=Valeur</math></div> |
|||
Afin de déterminer l’adresse du pointeur, il est d’abord nécessaire de connaître l’adresse de base de celle que nous avons trouvé. Pour cela, il « suffit » de soustraire l’offset de l’adresse trouvée à cette dernière. |
|||
<div style="text-align:center;"><math>AdresseBase=Adresse-Offset_n</math></div> |
|||
Cheat Engine permet de visualiser les instructions réalisées par l’assembleur afin de déterminer l’offset d’une adresse. Avoir quelques notions en assembleur peut aider, notamment lorsqu’on souhaite réaliser une injection de code. Ainsi, en observant les instructions réalisées précisément lorsqu’on modifie la valeur que nous observons en interagissant avec le logiciel, il devient possible de retrouver l’offset de l’adresse mémoire. Evidemment, il peut arriver que celui-ci soit égale à 0 : dans ce cas, l’adresse trouvée est donc déjà une adresse de base. |
|||
<div style="text-align:center;">[[Fichier:AccessAssembly.png]]</div> |
|||
Enfin, il ne reste plus qu’à trouver l’adresse du pointeur (allouée de manière statique ou dynamique) en recherchant simplement quelle adresse a pour valeur l’adresse de base que nous venons de calculer. |
|||
<div style="text-align:center;"><math>Pointeur \rightarrow AdresseBase</math></div> |
|||
Nous avons ainsi calculé l’adresse de pointeur pour une adresse donnée. Si cette adresse n’est pas allouée statiquement, il est nécessaire de réitérer cette démarche jusqu’à trouver une adresse statique (level-n pointer). En résumé, la démarche est la suivante : |
|||
<div style="text-align:center;"><math>Pointeur Statique \rightarrow AdresseBase_1+Offset_1=Pointeur_2</math> |
|||
<div><math>...</math></div> |
|||
<math>Pointeur_(n-1) \rightarrow AdresseBase_(n-1)+Offset_(n-1)=Pointeur_n</math> |
|||
<math>Pointeur_n \rightarrow AdresseBase_n+Offset_n=Adresse</math></div> |
|||
Maintenant que nous possédons une adresse statique, il faut trouver un moyen de redescendre jusqu’à la valeur initialement observée. Pour cela, rien de plus simple. Supposons que nous disposons d’une fonction getAddress() calculant l’adresse passée en paramètre ; accéder à la valeur observée revient à se déplacer par « sauts » successifs correspondant aux offsets précédemment déterminés. |
|||
<div style="text-align:center;"><math>(Rappel) Pointeur Statique=getAddress('MonProgramme.exe'+Offset)</math> |
|||
<br /> |
|||
<math>Adresse=Valeur</math> |
|||
<div><math>\Leftrightarrow</math></div> |
|||
<math>getAddress(getAddress(Pointeur Statique)+Offset_i )=Valeur</math></div> |
|||
Jusqu’à présent, nous avons supposé que l’offset était une information qui ne variait pas dans le programme : elle dépend en réalité de la manière dont est écrit le code, et n’évolue pas tant que le code garde la même structure. Si le logiciel subit une mise à jour importante, il est possible que ces valeurs changent, et qu’il faille recalculer une nouvelle adresse statique. Ici, il faut vraiment voir le pointeur statique comme un référentiel connu qui nous permet de descendre dans la mémoire jusqu’à la valeur. |
|||
Nous avons ainsi vu <strong>une manière</strong> d’accéder à la mémoire d’un logiciel. Par ailleurs, Cheat Engine introduit le concept de Cheat Tables consistant à importer des pointeurs statiques (mais aussi des scripts), déjà calculées par d’autres personnes afin de profiter de certains hacks sans avoir à chercher soi-même ces adresses. |
|||
== L’injection de code == |
|||
Injecter du code dans un logiciel peut rapidement s’avérer complexe, et demande de bonnes connaissances en assembleur. L’idée, ici, est juste de survoler le principe d’injection avec quelques exemples concrets, sans trop entrer dans les détails. |
|||
A chaque fois que la valeur observée sera modifiée, des instructions seront réalisées en assembleur. Par exemple, le code suivant : |
|||
<pre>int i = 1, j = 1; |
|||
i -= j;</pre> |
|||
Pourra donner un code assembleur à peu près équivalent à ce qui suit : |
|||
<div style="text-align:center;">[[Fichier:Assembly.png]]</div> |
|||
Note sur l’instruction lea : |
|||
<div style="text-align:center;"><math>lea reg1,[reg2+offset]</math> |
|||
<div><math>\Leftrightarrow</math></div> |
|||
<math>mov reg1,adresse(reg2+offset)</math></div> |
|||
A la différence de mov où c’est la valeur qui est stockée dans le registre, et non l’adresse. |
|||
L’injection de code consiste simplement à ajouter une ou plusieurs instructions à l’endroit souhaité dans la pile d’exécution afin de modifier, par exemple, le résultat d’une affectation. CheatEngine permet aussi de « commenter » certaines instructions afin de ne pas les exécuter (il réalise probablement un jump de son côté afin de sauter les instructions initialement exécutées). |
|||
Voici un exemple d’injection que l’on pourrait effectuer sur le code assembleur vu au-dessus : |
|||
<pre>alloc(newmem,2048,"Tutorial-x86_64.exe"+2B233) |
|||
newmem: |
|||
mov edx,1E |
|||
add [rbx+00000790],edx |
|||
originalcode: |
|||
;lea edx,[rax+01] |
|||
;sub [rbx+00000790],edx |
|||
exit: |
|||
jmp returnhere</pre> |
|||
Ici, l’injection a lieu à un emplacement mémoire précis. Sans entrer plus en détail sur la manière dont l’insertion est gérée et exploitée, il est fort probable que si le code du logiciel attaqué vient à être modifié, les instructions injectées ne fassent plus ce qui est attendu (au point de potentiellement déclencher des erreurs). |
|||
Dans l’exemple ci-dessus, on insère la valeur brute « 30 » (0x1E) dans le registre edx, puis on l’additionne à la valeur de l’adresse rbx+00000790 (pointant vers la variable i). Le code assembleur exécuté sera ainsi : |
|||
<div style="text-align:center;">[[Fichier:AssemblyInjection.png]]</div> |
|||
Il ne fait aucun doute que la complexité d’une injection dépend de ce que l’on souhaite faire, et des valeurs manipulées (une division de nombres flottants fera appel à d’autres instructions de l’assembleur, par exemple). Il est aussi parfaitement envisageable de récupérer la valeur d’un pointeur qu’on aurait calculé dans la partie précédente afin d’effectuer un certain nombre d’opérations spécifiques. |
|||
= Exemples applicatifs = |
|||
== Récupération de données simples d’un jeu == |
|||
Afin de mettre en pratique ce que nous avons abordé précédemment et découvrir comment fonctionne Cheat Engine, je vais vous présenter comment récupérer un accès vers une valeur simple dans un jeu. Pour des raisons évidentes de sécurité, nous jouerons sur un serveur local. |
|||
L’idée va donc consister à récupérer la vitalité (simple, mais toujours aussi efficace) de notre « joueur » afin de tenter de devenir invulnérable sur le jeu The Isle, qui est en cours de développement (ce qui rendra la recherche de pointeurs plus simple à réaliser). |
|||
Avant de commencer, nous partons du principe que nous disposons à tout moment de la vitalité courante du joueur (disponible dans des fichiers de données du jeu au format JSON). Dans le cas où la vitalité du joueur n’est pas explicitement affichée sous forme de valeur numérique, il est possible de retrouver une adresse par comparaison, en observant par exemple si la valeur observée (initialement inconnue) est augmentée ou réduite suite à un certain événement (comme le fait de recevoir des dégâts) : cette recherche reste cependant laborieuse et demande beaucoup de temps. |
|||
De plus, nous savons que la valeur désignant les points de vie du joueur est codée sur un entier de 4 bytes. Là aussi, retrouver le type exact de la valeur que nous recherchons peut prendre un certain temps, d’autant plus que les types utilisés dépendent fortement des jeux et des langages dans lesquels ils sont développés). |
|||
La première étape, comme expliqué dans la partie précédente, consiste à trouver l’adresse mémoire où est stockée la valeur des points de vie du joueur. Pour ce faire, Cheat Engine permet facilement de rechercher les adresses par valeur. |
|||
<div style="display: flex; align-items: center;"> |
|||
<pre style="flex: 1; margin: 10px;">{ |
|||
"Growth": "1.0", |
|||
"Hunger": "200", |
|||
"Thirst": "86", |
|||
"Stamina": "106", |
|||
"Health": "6500", |
|||
"Oxygen": "40", |
|||
"bGender": true |
|||
}</pre> |
|||
[[Fichier:TheIsle01.png]] |
|||
</div> |
|||
Plus de 104 adresses différentes ont été trouvées, mais seulement quelques-unes d’entre elles correspondent à ce que nous recherchons vraiment. Afin de trouver la bonne adresse, nous allons modifier la vitalité du joueur en subissant des dégâts en jeu ; les adresses des valeurs qui ont été modifiées seront mises en évidence. |
|||
<div style="text-align:center;">[[Fichier:TheIsle02.png]]</div> |
|||
Deux valeurs ont été modifiées suite aux dégâts subis. L’une d’elles correspond à un résultat cohérent : le joueur disposant initialement de 6500 pdv, a de fortes chances d’être tombé à 4196 pdv. Il est ainsi quasiment certain que l’adresse <strong>21E33D4BD2C</strong> contienne la valeur des points de vie du joueur. |
|||
Il arrive souvent que plusieurs adresses contenant la nouvelle valeur soient trouvées. Dans ce cas, il est nécessaire de pousser la recherche un cran plus loin en modifiant chacune d’elles jusqu’à trouver celle qui impacte véritablement la vitalité du joueur. |
|||
Nous devons maintenant remonter la mémoire jusqu’à trouver l’adresse d’un pointeur statique. Cependant, nous allons procéder d’une manière différente : rechercher des pointeurs à la main comme expliqué précédemment serait bien trop long, notamment lorsqu’on s’attaque à un jeu pouvant allouer plusieurs millions d’adresses en mémoire. Fort heureusement, Cheat Engine permet de réaliser des « pointer scans » afin de calculer automatiquement tous les pointeurs accédant à l’adresse que nous venons de trouver. |
|||
<div style="text-align:center;">[[Fichier:TheIsle03.png]]</div> |
|||
Bien que le logiciel soit capable de lister une liste de pointeurs sans effort, il est impossible de prédire l’aspect du pointeur que nous recherchons, et notamment quelle sera la taille maximale d’un de ses offsets, ou même le nombre d’offsets nécessaires à appliquer à partir de l’adresse statique afin de retomber sur notre valeur. |
|||
Pour rappel, le pointeur que nous recherchons doit ressembler à ceci : |
|||
<div style="text-align:center;"><math>getAddress(getAddress(Pointeur Statique)+Offset_i )=AdresseValeur</math></div> |
|||
Ainsi, il est parfois nécessaire d’effectuer plusieurs scans avec des paramètres de recherche différents jusqu’à tomber sur le bon pointeur. Dans les cas les plus extrêmes, un scan peut mettre plusieurs heures et nécessiter plusieurs GO (voire TO) d’espace disque. De manière plus globale, on favorisera des recherches avec peu d’offsets (3 à 5) mais un maximal élevé (rarement au-delà de 8192), ou beaucoup d’offset (rarement au-delà de 8) mais un maximal faible (1024 ou 2048). |
|||
Pour notre exemple, un seul scan avec ces paramètres suffira à mettre la main sur le pointeur. |
|||
<div style="text-align:center;">[[Fichier:TheIsle04.png]]</div> |
|||
Le résultat de l’analyse retourne 1431 adresses : certaines d’entre elles sont des adresses statiques, d’autres des pointeurs. Pour rappel, seule les adresses statiques permettent de retrouver notre valeur une fois le jeu relancé ; les pointeurs seront alloués différemment et ne présentent donc aucun intérêt pour la création d’un trainer. |
|||
Afin donc de focaliser notre recherche sur les adresses statiques retournées par le scan, nous allons relancer le jeu, et ne conserver que les adresses qui mènent aux points de vie du joueur (qui a pu se soigner entre temps). |
|||
<div style="display: flex; align-items: center;text-align: center; margin: 0 auto;"> |
|||
<div style="margin: 10px;">[[Fichier:TheIsle05.png]]</div> |
|||
<div style="margin: 10px;">[[Fichier:TheIsle06.png]]</div> |
|||
</div> |
|||
Nous avons presque trouvé l’adresse statique rattachée aux points de vie du joueur. A ce stade-là, il ne reste que 27 adresses à tester. Et pour ce faire, nous pouvons relancer plusieurs fois le jeu, et modifier les valeurs manuellement jusqu’à ce qu’il y ait un impact visible en jeu. Il se peut évidemment que plusieurs adresses statiques soient valides : il ne reste alors plus qu’à choisir celle que l’on souhaite conserver. |
|||
<div style="text-align:center;">[[Fichier:TheIsle07.png]]</div> |
|||
Notons que nous <strong>ne pouvons pas</strong> simplifier l’écriture de l’adresse statique de la manière suivante : |
|||
<div style="text-align:center;"><math>'TheIsle-Win64-Shipping.exe'+02F8F3D0 +30 +3B0+9AC= AdresseValeur</math> |
|||
<div><math>\Leftrightarrow</math></div> |
|||
<math>'TheIsle-Win64-Shipping.exe' + 2F9015C= AdresseValeur</math></div> |
|||
La première équation s’écrit en réalité de la manière suivante (dans le cas où plusieurs offsets sont nécessaires) : |
|||
<pre>getAddress( |
|||
getAddress( |
|||
getAddress( |
|||
getAddress( |
|||
getModuleAddress("TheIsle-Win64-Shipping.exe") + 0x02F8F3D0 |
|||
) + 0x30 |
|||
) + 0x3B0 |
|||
) + 0x9AC |
|||
)</pre> |
|||
Où la valeur retournée par getAddress() peut évoluer au cours de l’exécution du jeu. C’est pour cette raison qu’il ne faut surtout pas additionner les offsets en pensant que la mémoire reste statique pour chacune des adresses calculées. |
|||
Pour nous amuser, nous pouvons geler la variable afin que le joueur ne perde plus de vitalité. Il devient ainsi invulnérable à toute sorte de dégâts. |
|||
== Création d’un trainer == |
|||
Le fait d’avoir passé du temps à trouver une adresse statique va nous permettre de réaliser un véritable trainer (simple, mais fonctionnel) afin d’accéder à la vitalité du joueur en lecture et en écriture à partir d’une interface dédiée. Les trainers sont généralement développés en C++ ou en C# avec une sous-couche en Lua notamment pour réaliser des injections de code. |
|||
Pour cet exemple, nous réaliserons une interface en C# afin de gagner un peu de temps. De plus, nous utiliserons la librairie ProcessMemoryReaderLib.cs qui nous permettra d’utiliser simplement des fonctions de lecture et d’écriture dans la mémoire. |
|||
Voici une interface réalisable en quelques clics sur Visual Studio : |
|||
<div style="text-align:center;">[[Fichier:Trainer.png]]</div> |
|||
Dans un premier temps, préparons le squelette du code. L’exemple qui suit est minimaliste, mais sera suffisant pour ce que nous souhaitons faire. |
|||
<pre>private static readonly string PROCESS_NAME = "TheIsle-Win64-Shipping"; |
|||
private System.Diagnostics.Process TheIsleProcess; |
|||
private ProcessMemoryReader ReadMemory; |
|||
public Trainer() { InitializeComponent(); } |
|||
// Exécuté au chargement du trainer |
|||
private void Trainer_Load(object o, EventArgs e) { |
|||
ReadMemory = new ProcessMemoryReader(); |
|||
DetectGame(); |
|||
} |
|||
// Exécuté toutes les 100 ms |
|||
private void TimerDetectGame_Tick(object o, EventArgs e) { |
|||
DetectGame(); |
|||
} |
|||
// Exécuté toutes les 50 ms |
|||
private void TimerShowHealth_Tick(object o, EventArgs e) { |
|||
ShowHealth(); |
|||
} |
|||
// Exécuté lorsqu'on écrit dans le textbox |
|||
private void VitalityTextbox_TextChanged(object o, EventArgs e) { |
|||
UpdateHealth(); |
|||
}</pre> |
|||
Tout d’abord, penchons-nous sur la détection du jeu. L’idée consiste simplement à afficher un message d’avertissement si notre trainer parvient à détecter au non le jeu auquel nous tentons de tricher. De plus, dans l’idéal, il faudrait décider d’afficher le bloc « Vitalité » de l’exemple de fenêtre vu plus haut uniquement si le jeu a bien été détecté. |
|||
Pour détecter un logiciel en C#, nous utilisons la classe System qui va nous permettre de rechercher tous les processus d’un certain nom. Il peut évidemment y en avoir plusieurs, comme par exemple lorsqu’on ouvre plusieurs fois un même navigateur. Ici, nous travaillons sur une seule instance du processus « TheIsle-Win64-Shipping.exe ». |
|||
<pre>private void DetectGame() { |
|||
System.Diagnostics.Process[] TheIsleProcesses = |
|||
System.Diagnostics.Process.GetProcessesByName(Trainer.PROCESS_NAME); |
|||
if (TheIsleProcesses.Length != 0) { |
|||
TheIsleProcess = TheIsleProcesses[0]; |
|||
/* |
|||
On active les événements TimerShowHealth_Tick |
|||
et VitalityTextbox_TextChanged |
|||
*/ |
|||
} |
|||
else { |
|||
TheIsleProcess = null; |
|||
/* |
|||
On désactive les événements TimerShowHealth_Tick |
|||
et VitalityTextbox_TextChanged |
|||
*/ |
|||
} |
|||
}</pre> |
|||
Il ne nous reste plus qu’à trouver un moyen d’afficher la vitalité du joueur, puis un moyen pour la modifier. En réalité, qu’il s’agisse de lire ou d’écrire dans la mémoire, la manière de procéder est exactement la même. Nous allons simplement lire les octets des emplacements mémoire spécifiques alloués par le processus, que nous devrons ensuite convertir en entier : ce nouvel entier correspond simplement à l’adresse en mémoire (sous forme décimale) que nous avons lue. Enfin, une fois que nous avons obtenu notre <i>AdresseValeur</i> par succession de sauts dans la mémoire, il ne restera plus qu’à lire l’entier en valeur (codé sur 4 octets dans notre cas). |
|||
Il y a cependant une petite subtilité à prendre en compte. Lorsqu’on réalise un trainer, il faut bien avoir en tête que ce dernier s’exécutera sur un processeur 32 bits ou 64 bits. Pour un processeur 32 bits, les adresses sont stockées sur 4 octets, tandis que pour du 64 bits, elles seront stockées sur 8 octets. L’exemple ci-dessous a été adapté seulement pour les systèmes 64 bits. |
|||
Voici la fonction qui permettra d’afficher la vitalité du joueur : |
|||
<pre>private void ShowHealth() { |
|||
// Lecture de la mémoire du jeu |
|||
ReadMemory.ReadProcess = TheIsleProcess; |
|||
ReadMemory.OpenProcess(); |
|||
int NumberOfReadBytes; |
|||
uint ByteSize = sizeof(byte); |
|||
IntPtr Address; |
|||
byte[] ReadBytes; |
|||
Int32 Health = 0; |
|||
// Adresse de base : "TheIsle-Win64-Shipping.exe" + 02F8F3D0 |
|||
ReadBytes = ReadMemory.ReadProcessMemory( |
|||
TheIsleProcess.MainModule.BaseAddress + 0x02F8F3D0, |
|||
8 * ByteSize, |
|||
out NumberOfReadBytes |
|||
); |
|||
Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); |
|||
// Pointeur de niveau 1 : {Adresse de base} + 0x30 |
|||
ReadBytes = ReadMemory.ReadProcessMemory( |
|||
Address + 0x30, |
|||
8 * ByteSize, |
|||
out NumberOfReadBytes |
|||
); |
|||
Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); |
|||
// Pointeur de niveau 2 : {Pointeur Lvl 1} + 0x3B0 |
|||
ReadBytes = ReadMemory.ReadProcessMemory( |
|||
Address + 0x3B0, |
|||
8 * ByteSize, |
|||
out NumberOfReadBytes |
|||
); |
|||
Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); |
|||
// Adresse contenant la valeur : {Pointer Lvl 2} + 0x9AC |
|||
ReadBytes = ReadMemory.ReadProcessMemory( |
|||
Address + 0x9AC, |
|||
4 * ByteSize, |
|||
out NumberOfReadBytes |
|||
); |
|||
Health = BitConverter.ToInt32(ReadBytes, 0); |
|||
// Affichage de la vitalité |
|||
VitalityLabel.Text = Health.ToString(); |
|||
}</pre> |
|||
Pour un système 32 bits, on aurait juste eu à modifier le nombre d’octets lus, et le type de conversion. |
|||
<pre>ReadBytes = ReadMemory.ReadProcessMemory( |
|||
Address + 0x30, |
|||
4 * ByteSize, |
|||
out NumberOfReadBytes |
|||
); |
|||
Address = (IntPtr)BitConverter.ToInt32(ReadBytes, 0);</pre> |
|||
Enfin, il ne reste plus qu’à coder la fonction modifiant la valeur de l’adresse en mémoire à chaque interaction avec le champ texte. |
|||
<pre>private void UpdateHealth() { |
|||
// Début identique à ShowHealth() |
|||
int NumberOfWrittenBytes; |
|||
byte[] WrittenBytes = |
|||
BitConverter.GetBytes(Int32.Parse(VitalityTextbox.Text)); |
|||
// Parcours identique à ShowHealth() |
|||
// Address = {Pointeur Lvl 1} + 0x3B0 |
|||
// Adresse contenant la valeur : {Pointer Lvl 2} + 0x9AC |
|||
ReadMemory.WriteProcessMemory( |
|||
Address + 0x9AC, |
|||
WrittenBytes, |
|||
out NumberOfWrittenBytes |
|||
); |
|||
}</pre> |
|||
Voici donc une manière de réaliser un trainer simple en quelques minutes. Vous pouvez évidemment utiliser d’autres langages de programmation, comme C, C++, ou Lua, et multiplier les variables en jeu que vous souhaitez contrôler. |
|||
= Détecter et lutter contre l’utilisation d’un trainer = |
|||
Les manières de détecter l’utilisation d’un trainer diffèrent du fait qu’il s’agisse d’un logiciel s’exécutant côté client uniquement, ou pouvant inclure des scripts côté serveur. |
|||
== Côté client == |
|||
Les « anti-cheats » côté client sont des logiciels (qu’il faut donc installer, ou au moins exécuter sur la machine) consistant à scanner de manière régulière les instructions exécutées par le jeu, et capables de détecter d’éventuelles instructions ou lignes de code suspicieuses en les comparant à des instructions blacklistées dans une base de donnes, ou encore d’écouter les erreurs de mémoire reportées par le système qui sont généralement issues d’une injection qui s’est mal déroulée. Ces anti-cheats fonctionnent sur un principe relativement proche de celui des anti-virus. |
|||
Cependant, certains hacks plus performants parviennent à passer outre cette protection en changeant en permanence et de manière parfaitement autonome la nature des injections (syntaxe, emplacement, registres utilisés, opérations réalisées, etc) : dans ce cas, il faudrait que l’anti-cheat connaisse autant d’instructions à blacklister que le logiciel de triche en invente, ce qui n’est physiquement pas possible. |
|||
== Côté serveur == |
|||
Disposer d’un serveur connecté au client permet généralement de freiner l’exploitation de logiciels de triche sur ce dernier : pour peu que les données des utilisateurs soient conservées (dans une base de données, par exemple), il est alors possible de vérifier l’authenticité d’une information reçue de la part du client. Lorsque cette vérification n’est pas effectuée ou mal réalisée, on parle généralement d’exploitation de faille. |
|||
Voici un exemple d’exploitation de faille : |
|||
<div style="border-left: 2px solid black; padding-left: 5px; margin-left: 5px;"> |
|||
- L’utilisateur modifie une caractéristique A avec son logiciel de triche, puis attaque un ennemi. |
|||
- Le serveur reçoit les caractéristiques du joueur A et l’ennemi ciblé. Il calcule les dommages effectués, modifie les points de vie restant du monstre, et retourne le résultat de l’attaque.</div> |
|||
Voici un exemple où cette exploitation de faille ne serait pas possible : |
|||
<div style="border-left: 2px solid black; padding-left: 5px; margin-left: 5px;"> |
|||
- L’utilisateur modifie une caractéristique A avec son logiciel de triche, puis attaque un ennemi. |
|||
- Le serveur reçoit l’identifiant du joueur A et celui de l’ennemi ciblé. Il récupère ses caractéristiques à partir d’une base de données, calcule les dommages effectués, modifie les points de vie restant du monstre, et retourne le résultat de l’attaque. |
|||
</div> |
|||
Ces erreurs sont fréquentes sur des logiciels fraîchement créés ou récemment mis à jour, et, bien qu’elles soient relativement faciles à détecter et à corriger, elles peuvent avoir des impacts importants sur le logiciel en question, surtout si la faille est exploitée de manière modéré et discrète sur la durée. |
|||
Détecter une différence entre la valeur client et la valeur récupérée côté serveur n’assure toutefois pas qu’il s’agit d’un acte de tricherie : suivant la nature du logiciel, des désynchronisations peuvent avoir lieu entre client et serveur. Il peut donc parfois être difficile de déterminer précisément si une tentative de triche a été réalisée ou non. |
|||
Ici aussi, il existe des anti-cheats dont le fonctionnement peut varier. Exécutés côté serveur (et ne nécessitant donc aucune installation côté client), ils observent les statistiques relatives à l’utilisation du logiciel (comme le résultat d’une partie et le ratio de tués/morts sur un jeu de tir), et détermine de manière probabiliste si l’utilisateur a eu recours à un logiciel de triche (et comparant ses performances à celles d’autres joueurs de même niveau, par exemple). Après un certain nombre d’alertes, l’activité de l’utilisateur est vérifiée par un administrateur qui jugera si un trainer a bel et bien utilisé, notamment en relisant les logs d’une partie et en mettant en évidence des exploits qui ne sont normalement pas possible sans tricher. Cela peut toutefois engendrer la sanction de joueurs particulièrement bons, détectés par le système comme étant des tricheurs. |
|||
Parmi les anti-cheats les plus utilisés, nous retrouvons par exemple battlEye ou FaceIT. Leur fonctionnement reste cependant obscur, et il est difficile de déterminer sur quelles statistiques ces derniers s’appuient, et leur « seuil de tolérance ». |
|||
= Conclusion = |
|||
Ce wiki avait pour but de présenter les bases sur lesquelles se fondent les entraîneurs de jeux, et de comprendre du mieux possible comment de tels logiciels peuvent fonctionner, mais aussi d’être averti sur la manière dont ces derniers peuvent être repérés. Il est tout à fait possible d’approfondir le sujet notamment au niveau des injections de code, qui ont été très vaguement abordées, ou des injections AOB, qui vont se fonder sur une signature plutôt qu’une adresse mémoire. Cheat Engine est un logiciel complet qui permet d’exploiter la mémoire d’un jeu d’une multitude de façons différentes, et seules deux approches ont été présentées ici. |
|||
</div> |
Version du 15 novembre 2018 à 18:37
Auteur : Lukas AUGER
Parmi quelques exemples applicatifs de ces programmes, on retrouve notamment la possibilité de geler la valeur d’une variable (points de vie, nombres de munitions, et plus globalement les « compteurs » de n’importe quelle nature qu’ils soient) ou de lui faire atteindre des valeurs disproportionnées, mais aussi de provoquer ou d’observer le déclenchement d’événements particuliers. Les game trainers diffèrent des codes de triches dans la mesure où ces derniers ont été pensés à cet effet par les développeurs dans le but de faciliter la progression du joueur, ou de lui offrir une nouvelle expérience de jeu.
Fonctionnement théorique
L’accès à la mémoire
Une des méthodes les plus employées pour la réalisation d’un trainer consiste, dans un premier temps, à identifier l’adresse mémoire liée à la valeur que l’utilisateur souhaite modifier. Pour ce faire, nous utiliserons un scanneur de mémoire du nom de Cheat Engine. Ce logiciel permet de lister toutes les adresses mémoires rattachées à un logiciel précis, de rechercher une adresse en particulier en fonction de différents critères (comme son type) et à l’aide d’opérateurs de comparaison (appliqués lorsque la valeur change), et enfin de modifier la valeur stockée dans cet espace. Il offre de nombreuses fonctionnalités comme la recherche de pointeurs, l’injection de code ou encore le contrôle de la souris notamment utilisé pour la création d’aimbots; certaines d’entre-elles seront abordées dans ce wiki.
Un problème que nous risquons rapidement de rencontrer réside dans le fait qu’une simple adresse mémoire a de fortes chances d’être différente lorsque le jeu sera relancé, ou plus tard pendant l’exécution du jeu. En connaissant l’adresse d’un pointeur, nous pourrons de ce fait accéder à l’adresse mémoire dans laquelle est stockée la valeur que nous souhaitons lire ou modifier.
Une fois le pointeur déterminé, deux cas de figures peuvent se présenter : l’adresse du pointeur est conservée de manière statique dans la mémoire, ou alors elle l’est de manière dynamique. Pour faire simple, dans le cas de l’allocation de mémoire statique, il est possible de connaître l’emplacement de la ressource dès lors que le programme est compilé (et donc d’y accéder de manière déterministe à chaque lancement du programme), tandis que lors d’une allocation dynamique, il n’est pas possible de prédire quel emplacement mémoire sera utilisé. En d’autres termes, l’objectif est d’obtenir une adresse de pointeur de la forme :
Ici, ‘MonProgramme.exe’ est ce qu’on appelle un module principal. Dans certains cas, une adresse statique peut être accédée à partir de l’adresse d’une ressource de la forme ‘MaRessource.dll’ : on parle alors de module tout court, mais le principe reste le même. L’adresse derrière ce module correspond à l’adresse minimale à partir de laquelle le logiciel va pouvoir allouer de la mémoire. D’une certaine manière, il s’agit d’un référentiel qu’on peut facilement connaître à l’aide d’un langage de programmation, et qui constituer le point de départ à partir duquel nous allons accéder à notre valeur.
La notion d’offset, ou décalage, est généralement exprimé en hexadécimal et correspond on nombre de « sauts » à effectuer dans la mémoire à partir d’une certaine adresse afin d’accéder à une adresse mémoire cible. Le code C suivant illustre simplement cette notion en partant d’un tableau de 200 entiers :
// ptr = ptr[0] = ptr + 0 int *ptr = (int*) malloc(200 * sizeof(int)); // ptr8 = ptr[8] = ptr + 8 int *ptr8 = ptr + 8; // ptr199 = ptr[199] = ptr + 199 int *ptr199 = ptr + 199;
Pour résumer, une adresse statique se compose de l’adresse d’un module que l’on va pouvoir facilement connaître, à laquelle on additionne un certain nombre (offset) afin d’accéder à une adresse en mémoire donc la valeur contiendra ce qui nous intéresse : ici, une autre adresse que nous permettra d’accéder à la valeur recherchée.
Dans le cas où l’adresse du pointeur n’est pas statique, il est nécessaire de remonter plus haut dans la mémoire jusqu’à trouver un pointeur statique : on parle alors de multilevel pointer, ou encore de level-n pointer.
Enfin, il est possible qu’une adresse statique mène directement à la valeur recherchée, sans passer par un pointeur intermédiaire. Cela n’arrive cependant presque jamais car les programmes pour lesquels des trainers sont développés atteignent rarement un tel niveau de simplicité.
Nous avons abordé dans les grandes lignes ce que nous souhaitions faire : trouver une adresse statique nous menant jusqu’à notre valeur. Détaillons un peu comment nous y prendre. Dans un premier temps, comme vu précédemment, expliquons plus en détail comment trouver l’adresse d’un pointeur. Tout d’abord, nous devons trouver l’adresse mémoire où se trouve la valeur qui nous intéresse.
Afin de déterminer l’adresse du pointeur, il est d’abord nécessaire de connaître l’adresse de base de celle que nous avons trouvé. Pour cela, il « suffit » de soustraire l’offset de l’adresse trouvée à cette dernière.
Cheat Engine permet de visualiser les instructions réalisées par l’assembleur afin de déterminer l’offset d’une adresse. Avoir quelques notions en assembleur peut aider, notamment lorsqu’on souhaite réaliser une injection de code. Ainsi, en observant les instructions réalisées précisément lorsqu’on modifie la valeur que nous observons en interagissant avec le logiciel, il devient possible de retrouver l’offset de l’adresse mémoire. Evidemment, il peut arriver que celui-ci soit égale à 0 : dans ce cas, l’adresse trouvée est donc déjà une adresse de base.
Enfin, il ne reste plus qu’à trouver l’adresse du pointeur (allouée de manière statique ou dynamique) en recherchant simplement quelle adresse a pour valeur l’adresse de base que nous venons de calculer.
Nous avons ainsi calculé l’adresse de pointeur pour une adresse donnée. Si cette adresse n’est pas allouée statiquement, il est nécessaire de réitérer cette démarche jusqu’à trouver une adresse statique (level-n pointer). En résumé, la démarche est la suivante :
Maintenant que nous possédons une adresse statique, il faut trouver un moyen de redescendre jusqu’à la valeur initialement observée. Pour cela, rien de plus simple. Supposons que nous disposons d’une fonction getAddress() calculant l’adresse passée en paramètre ; accéder à la valeur observée revient à se déplacer par « sauts » successifs correspondant aux offsets précédemment déterminés.
Jusqu’à présent, nous avons supposé que l’offset était une information qui ne variait pas dans le programme : elle dépend en réalité de la manière dont est écrit le code, et n’évolue pas tant que le code garde la même structure. Si le logiciel subit une mise à jour importante, il est possible que ces valeurs changent, et qu’il faille recalculer une nouvelle adresse statique. Ici, il faut vraiment voir le pointeur statique comme un référentiel connu qui nous permet de descendre dans la mémoire jusqu’à la valeur.
Nous avons ainsi vu une manière d’accéder à la mémoire d’un logiciel. Par ailleurs, Cheat Engine introduit le concept de Cheat Tables consistant à importer des pointeurs statiques (mais aussi des scripts), déjà calculées par d’autres personnes afin de profiter de certains hacks sans avoir à chercher soi-même ces adresses.
L’injection de code
Injecter du code dans un logiciel peut rapidement s’avérer complexe, et demande de bonnes connaissances en assembleur. L’idée, ici, est juste de survoler le principe d’injection avec quelques exemples concrets, sans trop entrer dans les détails.
A chaque fois que la valeur observée sera modifiée, des instructions seront réalisées en assembleur. Par exemple, le code suivant :
int i = 1, j = 1; i -= j;
Pourra donner un code assembleur à peu près équivalent à ce qui suit :
Note sur l’instruction lea :
A la différence de mov où c’est la valeur qui est stockée dans le registre, et non l’adresse.
L’injection de code consiste simplement à ajouter une ou plusieurs instructions à l’endroit souhaité dans la pile d’exécution afin de modifier, par exemple, le résultat d’une affectation. CheatEngine permet aussi de « commenter » certaines instructions afin de ne pas les exécuter (il réalise probablement un jump de son côté afin de sauter les instructions initialement exécutées).
Voici un exemple d’injection que l’on pourrait effectuer sur le code assembleur vu au-dessus :
alloc(newmem,2048,"Tutorial-x86_64.exe"+2B233) newmem: mov edx,1E add [rbx+00000790],edx originalcode: ;lea edx,[rax+01] ;sub [rbx+00000790],edx exit: jmp returnhere
Ici, l’injection a lieu à un emplacement mémoire précis. Sans entrer plus en détail sur la manière dont l’insertion est gérée et exploitée, il est fort probable que si le code du logiciel attaqué vient à être modifié, les instructions injectées ne fassent plus ce qui est attendu (au point de potentiellement déclencher des erreurs).
Dans l’exemple ci-dessus, on insère la valeur brute « 30 » (0x1E) dans le registre edx, puis on l’additionne à la valeur de l’adresse rbx+00000790 (pointant vers la variable i). Le code assembleur exécuté sera ainsi :
Il ne fait aucun doute que la complexité d’une injection dépend de ce que l’on souhaite faire, et des valeurs manipulées (une division de nombres flottants fera appel à d’autres instructions de l’assembleur, par exemple). Il est aussi parfaitement envisageable de récupérer la valeur d’un pointeur qu’on aurait calculé dans la partie précédente afin d’effectuer un certain nombre d’opérations spécifiques.
Exemples applicatifs
Récupération de données simples d’un jeu
Afin de mettre en pratique ce que nous avons abordé précédemment et découvrir comment fonctionne Cheat Engine, je vais vous présenter comment récupérer un accès vers une valeur simple dans un jeu. Pour des raisons évidentes de sécurité, nous jouerons sur un serveur local. L’idée va donc consister à récupérer la vitalité (simple, mais toujours aussi efficace) de notre « joueur » afin de tenter de devenir invulnérable sur le jeu The Isle, qui est en cours de développement (ce qui rendra la recherche de pointeurs plus simple à réaliser).
Avant de commencer, nous partons du principe que nous disposons à tout moment de la vitalité courante du joueur (disponible dans des fichiers de données du jeu au format JSON). Dans le cas où la vitalité du joueur n’est pas explicitement affichée sous forme de valeur numérique, il est possible de retrouver une adresse par comparaison, en observant par exemple si la valeur observée (initialement inconnue) est augmentée ou réduite suite à un certain événement (comme le fait de recevoir des dégâts) : cette recherche reste cependant laborieuse et demande beaucoup de temps. De plus, nous savons que la valeur désignant les points de vie du joueur est codée sur un entier de 4 bytes. Là aussi, retrouver le type exact de la valeur que nous recherchons peut prendre un certain temps, d’autant plus que les types utilisés dépendent fortement des jeux et des langages dans lesquels ils sont développés).
La première étape, comme expliqué dans la partie précédente, consiste à trouver l’adresse mémoire où est stockée la valeur des points de vie du joueur. Pour ce faire, Cheat Engine permet facilement de rechercher les adresses par valeur.
{ "Growth": "1.0", "Hunger": "200", "Thirst": "86", "Stamina": "106", "Health": "6500", "Oxygen": "40", "bGender": true }
Plus de 104 adresses différentes ont été trouvées, mais seulement quelques-unes d’entre elles correspondent à ce que nous recherchons vraiment. Afin de trouver la bonne adresse, nous allons modifier la vitalité du joueur en subissant des dégâts en jeu ; les adresses des valeurs qui ont été modifiées seront mises en évidence.
Deux valeurs ont été modifiées suite aux dégâts subis. L’une d’elles correspond à un résultat cohérent : le joueur disposant initialement de 6500 pdv, a de fortes chances d’être tombé à 4196 pdv. Il est ainsi quasiment certain que l’adresse 21E33D4BD2C contienne la valeur des points de vie du joueur. Il arrive souvent que plusieurs adresses contenant la nouvelle valeur soient trouvées. Dans ce cas, il est nécessaire de pousser la recherche un cran plus loin en modifiant chacune d’elles jusqu’à trouver celle qui impacte véritablement la vitalité du joueur.
Nous devons maintenant remonter la mémoire jusqu’à trouver l’adresse d’un pointeur statique. Cependant, nous allons procéder d’une manière différente : rechercher des pointeurs à la main comme expliqué précédemment serait bien trop long, notamment lorsqu’on s’attaque à un jeu pouvant allouer plusieurs millions d’adresses en mémoire. Fort heureusement, Cheat Engine permet de réaliser des « pointer scans » afin de calculer automatiquement tous les pointeurs accédant à l’adresse que nous venons de trouver.
Bien que le logiciel soit capable de lister une liste de pointeurs sans effort, il est impossible de prédire l’aspect du pointeur que nous recherchons, et notamment quelle sera la taille maximale d’un de ses offsets, ou même le nombre d’offsets nécessaires à appliquer à partir de l’adresse statique afin de retomber sur notre valeur.
Pour rappel, le pointeur que nous recherchons doit ressembler à ceci :
Ainsi, il est parfois nécessaire d’effectuer plusieurs scans avec des paramètres de recherche différents jusqu’à tomber sur le bon pointeur. Dans les cas les plus extrêmes, un scan peut mettre plusieurs heures et nécessiter plusieurs GO (voire TO) d’espace disque. De manière plus globale, on favorisera des recherches avec peu d’offsets (3 à 5) mais un maximal élevé (rarement au-delà de 8192), ou beaucoup d’offset (rarement au-delà de 8) mais un maximal faible (1024 ou 2048). Pour notre exemple, un seul scan avec ces paramètres suffira à mettre la main sur le pointeur.
Le résultat de l’analyse retourne 1431 adresses : certaines d’entre elles sont des adresses statiques, d’autres des pointeurs. Pour rappel, seule les adresses statiques permettent de retrouver notre valeur une fois le jeu relancé ; les pointeurs seront alloués différemment et ne présentent donc aucun intérêt pour la création d’un trainer. Afin donc de focaliser notre recherche sur les adresses statiques retournées par le scan, nous allons relancer le jeu, et ne conserver que les adresses qui mènent aux points de vie du joueur (qui a pu se soigner entre temps).
Nous avons presque trouvé l’adresse statique rattachée aux points de vie du joueur. A ce stade-là, il ne reste que 27 adresses à tester. Et pour ce faire, nous pouvons relancer plusieurs fois le jeu, et modifier les valeurs manuellement jusqu’à ce qu’il y ait un impact visible en jeu. Il se peut évidemment que plusieurs adresses statiques soient valides : il ne reste alors plus qu’à choisir celle que l’on souhaite conserver.
Notons que nous ne pouvons pas simplifier l’écriture de l’adresse statique de la manière suivante :
La première équation s’écrit en réalité de la manière suivante (dans le cas où plusieurs offsets sont nécessaires) :
getAddress( getAddress( getAddress( getAddress( getModuleAddress("TheIsle-Win64-Shipping.exe") + 0x02F8F3D0 ) + 0x30 ) + 0x3B0 ) + 0x9AC )
Où la valeur retournée par getAddress() peut évoluer au cours de l’exécution du jeu. C’est pour cette raison qu’il ne faut surtout pas additionner les offsets en pensant que la mémoire reste statique pour chacune des adresses calculées.
Pour nous amuser, nous pouvons geler la variable afin que le joueur ne perde plus de vitalité. Il devient ainsi invulnérable à toute sorte de dégâts.
Création d’un trainer
Le fait d’avoir passé du temps à trouver une adresse statique va nous permettre de réaliser un véritable trainer (simple, mais fonctionnel) afin d’accéder à la vitalité du joueur en lecture et en écriture à partir d’une interface dédiée. Les trainers sont généralement développés en C++ ou en C# avec une sous-couche en Lua notamment pour réaliser des injections de code. Pour cet exemple, nous réaliserons une interface en C# afin de gagner un peu de temps. De plus, nous utiliserons la librairie ProcessMemoryReaderLib.cs qui nous permettra d’utiliser simplement des fonctions de lecture et d’écriture dans la mémoire.
Voici une interface réalisable en quelques clics sur Visual Studio :
Dans un premier temps, préparons le squelette du code. L’exemple qui suit est minimaliste, mais sera suffisant pour ce que nous souhaitons faire.
private static readonly string PROCESS_NAME = "TheIsle-Win64-Shipping"; private System.Diagnostics.Process TheIsleProcess; private ProcessMemoryReader ReadMemory; public Trainer() { InitializeComponent(); } // Exécuté au chargement du trainer private void Trainer_Load(object o, EventArgs e) { ReadMemory = new ProcessMemoryReader(); DetectGame(); } // Exécuté toutes les 100 ms private void TimerDetectGame_Tick(object o, EventArgs e) { DetectGame(); } // Exécuté toutes les 50 ms private void TimerShowHealth_Tick(object o, EventArgs e) { ShowHealth(); } // Exécuté lorsqu'on écrit dans le textbox private void VitalityTextbox_TextChanged(object o, EventArgs e) { UpdateHealth(); }
Tout d’abord, penchons-nous sur la détection du jeu. L’idée consiste simplement à afficher un message d’avertissement si notre trainer parvient à détecter au non le jeu auquel nous tentons de tricher. De plus, dans l’idéal, il faudrait décider d’afficher le bloc « Vitalité » de l’exemple de fenêtre vu plus haut uniquement si le jeu a bien été détecté. Pour détecter un logiciel en C#, nous utilisons la classe System qui va nous permettre de rechercher tous les processus d’un certain nom. Il peut évidemment y en avoir plusieurs, comme par exemple lorsqu’on ouvre plusieurs fois un même navigateur. Ici, nous travaillons sur une seule instance du processus « TheIsle-Win64-Shipping.exe ».
private void DetectGame() { System.Diagnostics.Process[] TheIsleProcesses = System.Diagnostics.Process.GetProcessesByName(Trainer.PROCESS_NAME); if (TheIsleProcesses.Length != 0) { TheIsleProcess = TheIsleProcesses[0]; /* On active les événements TimerShowHealth_Tick et VitalityTextbox_TextChanged */ } else { TheIsleProcess = null; /* On désactive les événements TimerShowHealth_Tick et VitalityTextbox_TextChanged */ } }
Il ne nous reste plus qu’à trouver un moyen d’afficher la vitalité du joueur, puis un moyen pour la modifier. En réalité, qu’il s’agisse de lire ou d’écrire dans la mémoire, la manière de procéder est exactement la même. Nous allons simplement lire les octets des emplacements mémoire spécifiques alloués par le processus, que nous devrons ensuite convertir en entier : ce nouvel entier correspond simplement à l’adresse en mémoire (sous forme décimale) que nous avons lue. Enfin, une fois que nous avons obtenu notre AdresseValeur par succession de sauts dans la mémoire, il ne restera plus qu’à lire l’entier en valeur (codé sur 4 octets dans notre cas).
Il y a cependant une petite subtilité à prendre en compte. Lorsqu’on réalise un trainer, il faut bien avoir en tête que ce dernier s’exécutera sur un processeur 32 bits ou 64 bits. Pour un processeur 32 bits, les adresses sont stockées sur 4 octets, tandis que pour du 64 bits, elles seront stockées sur 8 octets. L’exemple ci-dessous a été adapté seulement pour les systèmes 64 bits.
Voici la fonction qui permettra d’afficher la vitalité du joueur :
private void ShowHealth() { // Lecture de la mémoire du jeu ReadMemory.ReadProcess = TheIsleProcess; ReadMemory.OpenProcess(); int NumberOfReadBytes; uint ByteSize = sizeof(byte); IntPtr Address; byte[] ReadBytes; Int32 Health = 0; // Adresse de base : "TheIsle-Win64-Shipping.exe" + 02F8F3D0 ReadBytes = ReadMemory.ReadProcessMemory( TheIsleProcess.MainModule.BaseAddress + 0x02F8F3D0, 8 * ByteSize, out NumberOfReadBytes ); Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); // Pointeur de niveau 1 : {Adresse de base} + 0x30 ReadBytes = ReadMemory.ReadProcessMemory( Address + 0x30, 8 * ByteSize, out NumberOfReadBytes ); Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); // Pointeur de niveau 2 : {Pointeur Lvl 1} + 0x3B0 ReadBytes = ReadMemory.ReadProcessMemory( Address + 0x3B0, 8 * ByteSize, out NumberOfReadBytes ); Address = (IntPtr)BitConverter.ToInt64(ReadBytes, 0); // Adresse contenant la valeur : {Pointer Lvl 2} + 0x9AC ReadBytes = ReadMemory.ReadProcessMemory( Address + 0x9AC, 4 * ByteSize, out NumberOfReadBytes ); Health = BitConverter.ToInt32(ReadBytes, 0); // Affichage de la vitalité VitalityLabel.Text = Health.ToString(); }
Pour un système 32 bits, on aurait juste eu à modifier le nombre d’octets lus, et le type de conversion.
ReadBytes = ReadMemory.ReadProcessMemory( Address + 0x30, 4 * ByteSize, out NumberOfReadBytes ); Address = (IntPtr)BitConverter.ToInt32(ReadBytes, 0);
Enfin, il ne reste plus qu’à coder la fonction modifiant la valeur de l’adresse en mémoire à chaque interaction avec le champ texte.
private void UpdateHealth() { // Début identique à ShowHealth() int NumberOfWrittenBytes; byte[] WrittenBytes = BitConverter.GetBytes(Int32.Parse(VitalityTextbox.Text)); // Parcours identique à ShowHealth() // Address = {Pointeur Lvl 1} + 0x3B0 // Adresse contenant la valeur : {Pointer Lvl 2} + 0x9AC ReadMemory.WriteProcessMemory( Address + 0x9AC, WrittenBytes, out NumberOfWrittenBytes ); }
Voici donc une manière de réaliser un trainer simple en quelques minutes. Vous pouvez évidemment utiliser d’autres langages de programmation, comme C, C++, ou Lua, et multiplier les variables en jeu que vous souhaitez contrôler.
Détecter et lutter contre l’utilisation d’un trainer
Les manières de détecter l’utilisation d’un trainer diffèrent du fait qu’il s’agisse d’un logiciel s’exécutant côté client uniquement, ou pouvant inclure des scripts côté serveur.
Côté client
Les « anti-cheats » côté client sont des logiciels (qu’il faut donc installer, ou au moins exécuter sur la machine) consistant à scanner de manière régulière les instructions exécutées par le jeu, et capables de détecter d’éventuelles instructions ou lignes de code suspicieuses en les comparant à des instructions blacklistées dans une base de donnes, ou encore d’écouter les erreurs de mémoire reportées par le système qui sont généralement issues d’une injection qui s’est mal déroulée. Ces anti-cheats fonctionnent sur un principe relativement proche de celui des anti-virus. Cependant, certains hacks plus performants parviennent à passer outre cette protection en changeant en permanence et de manière parfaitement autonome la nature des injections (syntaxe, emplacement, registres utilisés, opérations réalisées, etc) : dans ce cas, il faudrait que l’anti-cheat connaisse autant d’instructions à blacklister que le logiciel de triche en invente, ce qui n’est physiquement pas possible.
Côté serveur
Disposer d’un serveur connecté au client permet généralement de freiner l’exploitation de logiciels de triche sur ce dernier : pour peu que les données des utilisateurs soient conservées (dans une base de données, par exemple), il est alors possible de vérifier l’authenticité d’une information reçue de la part du client. Lorsque cette vérification n’est pas effectuée ou mal réalisée, on parle généralement d’exploitation de faille.
Voici un exemple d’exploitation de faille :
- L’utilisateur modifie une caractéristique A avec son logiciel de triche, puis attaque un ennemi.
- Le serveur reçoit les caractéristiques du joueur A et l’ennemi ciblé. Il calcule les dommages effectués, modifie les points de vie restant du monstre, et retourne le résultat de l’attaque.Voici un exemple où cette exploitation de faille ne serait pas possible :
- L’utilisateur modifie une caractéristique A avec son logiciel de triche, puis attaque un ennemi. - Le serveur reçoit l’identifiant du joueur A et celui de l’ennemi ciblé. Il récupère ses caractéristiques à partir d’une base de données, calcule les dommages effectués, modifie les points de vie restant du monstre, et retourne le résultat de l’attaque.
Ces erreurs sont fréquentes sur des logiciels fraîchement créés ou récemment mis à jour, et, bien qu’elles soient relativement faciles à détecter et à corriger, elles peuvent avoir des impacts importants sur le logiciel en question, surtout si la faille est exploitée de manière modéré et discrète sur la durée. Détecter une différence entre la valeur client et la valeur récupérée côté serveur n’assure toutefois pas qu’il s’agit d’un acte de tricherie : suivant la nature du logiciel, des désynchronisations peuvent avoir lieu entre client et serveur. Il peut donc parfois être difficile de déterminer précisément si une tentative de triche a été réalisée ou non.
Ici aussi, il existe des anti-cheats dont le fonctionnement peut varier. Exécutés côté serveur (et ne nécessitant donc aucune installation côté client), ils observent les statistiques relatives à l’utilisation du logiciel (comme le résultat d’une partie et le ratio de tués/morts sur un jeu de tir), et détermine de manière probabiliste si l’utilisateur a eu recours à un logiciel de triche (et comparant ses performances à celles d’autres joueurs de même niveau, par exemple). Après un certain nombre d’alertes, l’activité de l’utilisateur est vérifiée par un administrateur qui jugera si un trainer a bel et bien utilisé, notamment en relisant les logs d’une partie et en mettant en évidence des exploits qui ne sont normalement pas possible sans tricher. Cela peut toutefois engendrer la sanction de joueurs particulièrement bons, détectés par le système comme étant des tricheurs. Parmi les anti-cheats les plus utilisés, nous retrouvons par exemple battlEye ou FaceIT. Leur fonctionnement reste cependant obscur, et il est difficile de déterminer sur quelles statistiques ces derniers s’appuient, et leur « seuil de tolérance ».
Conclusion
Ce wiki avait pour but de présenter les bases sur lesquelles se fondent les entraîneurs de jeux, et de comprendre du mieux possible comment de tels logiciels peuvent fonctionner, mais aussi d’être averti sur la manière dont ces derniers peuvent être repérés. Il est tout à fait possible d’approfondir le sujet notamment au niveau des injections de code, qui ont été très vaguement abordées, ou des injections AOB, qui vont se fonder sur une signature plutôt qu’une adresse mémoire. Cheat Engine est un logiciel complet qui permet d’exploiter la mémoire d’un jeu d’une multitude de façons différentes, et seules deux approches ont été présentées ici.