Il y a quelques mois, j'ai eu un coup de nostalgie et j'ai voulu essayer à nouveau un peu de Lineage 2. Dans le passé, la plupart des serveurs étaient remplis de tonnes de bots qui farmaient pour soit monter de niveau, gagner de l'argent ou récolter des matériaux utiles pour fabriquer de l'équipement.
Les bots disponibles et fonctionnels sont tous payants (à ma connaissance) et la plupart d'entre eux sont détectés par les serveurs distants comme des outils de triche !
J'ai donc décidé de créer mon propre bot car je pensais que le processus serait intéressant. Cet article de blog présente mon approche pour créer mon propre bot pour Lineage II.
Avertissement
Bien que Lineage II soit un MMORPG en déclin, le botting est toléré sur certains serveurs et interdit sur d'autres car il peut être considéré comme un outil de triche. Vous pourriez être banni à vie si vous essayez un tel logiciel sur certains serveurs.
Introduction
Si vous n'êtes pas conscient du concept de botting, décrivons-le avant d'aller plus loin. Vous pouvez vous faire une idée de ce qu'est un bot en lisant la page Wikipedia.
Lineage II est un MMORPG qui demande beaucoup de farming. La principale façon de monter de niveau est de tuer des monstres, la principale façon d'obtenir de l'argent est de tuer des monstres et la principale façon de fabriquer un meilleur équipement est de tuer des monstres. Cela peut sembler être un jeu ennuyeux, mais Lineage II offre, à mon humble avis, un style de gameplay joueur contre joueur (PvP) très unique qui rend le jeu bien plus intéressant que beaucoup d'autres MMORPGs.
Parce que cela nécessite beaucoup de farming, pouvoir automatiser l'action de tuer des monstres aide beaucoup le joueur. On peut laisser son personnage farmer pendant la nuit et jouer pendant la journée et obtenir beaucoup de récompenses.
Le bot que j'ai conçu sera capable de cibler un monstre à proximité, de l'attaquer et d'utiliser des sorts, de ramasser des objets et de se reposer lorsque nécessaire.
Lineage II est protégé par une (probablement ancienne) version de WinLicense, donc certaines fonctions dans le fichier exécutable peuvent être virtualisées, mais cela n'aura pas d'importance pour nous. De plus, les serveurs de jeu utilisent des protections comme GameGuard, SmartGuard ou LameGuard afin de prévenir le botting, et pour une raison quelconque, mon bot n'est pas du tout détecté par aucun serveur auquel je me suis connecté.
Lineage II a beaucoup d'émulateurs de serveurs privés publics et diverses recherches autour du piratage de paquets, donc la tâche globale n'a pas été très difficile à accomplir car de nombreuses ressources sont disponibles en ligne.
Création du bot
Choix de conception
Je vois deux types de bots différents. Les bots complètement Out-of-Game (OOG), et les bots In-Game (IG).
La difficulté de créer un bot OOG est qu'il nécessite de faire de l'ingénierie inverse, de comprendre et d'implémenter tout le protocole client-serveur (y compris le chiffrement réseau). Cependant, cela peut être assez gratifiant car ensuite il est possible de faire tourner le bot sans une copie du jeu. Cela signifie qu'il sera beaucoup plus léger et beaucoup plus facile de faire tourner plusieurs instances du bot.
Dans le cas de Lineage II, créer un bot IG est beaucoup plus facile car Lineage II est implémenté de telle manière que le client ne prédit aucune action, mais réagit seulement à ce que le serveur envoie. Cela signifie que nous pouvons simuler l'envoi d'un paquet en utilisant le client en cours d'exécution, et c'est tout, le client du jeu réagira à ce paquet que nous avons envoyé après que le serveur ait validé (ou non) l'action. Cependant, faire tourner un bot IG peut être fastidieux car il sera un peu intrusif et pourrait déclencher des détections anti-triche.
J'ai décidé d'opter pour le bot IG car Lineage II est un vieux jeu et je soupçonnais que s'il avait un anti-cheat, il ne serait pas trop difficile de le contourner.
Envoyer un paquet spécifique
Trouver la fonction d'envoi de paquet
Comme mon but était d'automatiser des choses, mon premier objectif était d'envoyer un paquet choisi afin que je puisse automatiser des tâches individuelles.
Lineage II fonctionne de manière TCP complète et se connecte d'abord à un serveur de connexion qui centralise plusieurs serveurs de jeu. Les étapes suivantes décrivent l'envoi d'un paquet lorsque l'on est déjà en jeu et connecté à un serveur de jeu.
Pour envoyer un paquet, j'avais besoin de trouver la fonction qui enverrait un paquet. Le jeu utilise la fonction send
de la bibliothèque native Windows ws2_32.dll
. Tout ce que j'avais à faire était de mettre un point d'arrêt là, et de faire une action en jeu pour le déclencher. Les paramètres donnés à la fonction send
contiennent le paquet chiffré, donc nous devons vérifier d'où la fonction send
est appelée grâce à la pile d'appels. À partir de là, il est possible de faire un peu d'ingénierie inverse et de comprendre ce qui se passe.
Point d'arrêt à la fonction d'envoi d'un paquet
La capture d'écran ci-dessus montre un appel à la fonction d'envoi de paquet lorsque je tape un message à envoyer dans le chat du jeu. La première valeur sur la pile d'appels est l'adresse de retour, et les suivantes sont les arguments de la fonction. Le premier argument est un pointeur fixe vers une structure de données qui contient la socket TCP Windows. Le deuxième argument est une chaîne de caractères terminée par un zéro pour laquelle chaque caractère spécifie le type du champ du paquet. Dans notre cas (envoi d'un message de chat), le paquet contient les arguments suivants :
- "c" (
char
) :0x49
qui correspond à l'ID de paquet "envoyer un message", - "S" (
String
) :L"Hello"
qui est un pointeur vers une chaîne de caractères large terminée par un zéro, - "d" (
dword
) :0x00000000
qui correspond à l'ID du canal.
Rejouer un paquet
Pour appeler la fonction "envoyer un paquet", j'ai décidé d'injecter une DLL dans le processus du jeu qui créerait un thread et appellerait cette fonction.
Le code de la DLL est plutôt petit :
DWORD SendPacketFP; DWORD(_stdcall* sendPacket)(DWORD FP, const char* format, ...); #define DATA_SEND_ADDR 0x3E3B80 // Offset de la fonction "envoyer un paquet" dans engine.dll #define DATA_SEND_SOCKET_INFO 0xFD890000 // Offset de la structure réseau void ProcessAttach() { DWORD EngineDLLBase = (DWORD) GetModuleHandle(L"engine.dll"); sendPacket = (DWORD(_stdcall *)(DWORD, const char*, ...)) EngineDLLBase + DATA_SEND_ADDR; SendPacketFP = DATA_SEND_SOCKET_INFO; // Pointeur vers la structure réseau const char format[] = "cSd"; const WCHAR* message = L"Hello"; DWORD parameters[3] = { 0 }; parameters[0] = 0x49; // ID de paquet parameters[1] = (DWORD) message; // Pointeur vers notre message parameters[2] = 0x00000000; // ID de canal général sendPacket(SendPacketFP, format, parameters[0], parameters[1], parameters[2]); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ProcessAttach, 0, 0, NULL); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
Il y a plusieurs façons d'injecter la DLL dans le processus du jeu. J'ai décidé de créer mon propre injecteur qui est très simple et qui va :
OpenProcess
le processus du jeul2.exe
- Allouer de la mémoire dans l'espace d'adressage du processus du jeu avec
VirtualAllocEx
- Écrire le chemin complet du
.dll
avecWriteProcessMemory
dans cet espace - Charger la bibliothèque avec
CreateRemoteThread
qui appelleraLoadLibraryW
Maintenant, lorsque je charge la bibliothèque, un paquet est envoyé au serveur et nous pouvons observer le résultat directement dans le jeu.
Envoi de paquet réussi depuis une bibliothèque partagée
C'est cool, maintenant je peux faire des actions dans le jeu, mais j'ai aussi besoin d'obtenir des informations de l'environnement environnant.
Obtenir des informations du jeu
Pour que le bot fonctionne, il est nécessaire de recueillir des informations du jeu telles que la position actuelle du joueur, la santé, l'inventaire, les compétences, mais aussi les entités à proximité telles que les Personnages Non Joueurs (PNJ) ou les joueurs ou monstres à proximité.
Pour ce faire, je vois seulement deux façons : soit inspecter la mémoire du jeu, ce qui implique de trouver la position de toutes ces structures dans l'espace d'adressage virtuel du jeu, soit simplement analyser les paquets reçus du serveur comme le fait le client du jeu. J'ai décidé d'opter pour la deuxième méthode, car je pensais qu'il serait difficile de recueillir de manière fiable toutes les informations dont j'avais besoin avec seulement l'inspection de la mémoire. Cependant, à long terme, cela aurait peut-être été plus facile car il n'y aurait pas eu besoin de comprendre le protocole client-serveur.
De manière similaire à ce que j'ai fait précédemment, il est possible de mettre un point d'arrêt sur la fonction recv
de ws2_32.dll
et vérifier la pile d'appels et essayer de trouver quelle fonction appelle recv
. Après cela, nous pouvons essayer de passer chaque fonction jusqu'à ce que le paquet soit déchiffré et que nous puissions commencer à l'analyser.
Pour notifier mon bot qu'un paquet est reçu, il est nécessaire de hooker la fonction "recevoir paquet" afin que je puisse dumper les paquets et ensuite redonner le flux d'exécution au jeu.
LPVOID Hook(LPVOID functionToHook, LPVOID myFunction, size_t size) { DWORD old; DWORD old2; // En x86, l'instruction 'jmp addr' fait 5 octets de long et commence par 0xE9 const JMP_INSTR_SIZE = 5; // Allouer de la mémoire et copier les anciens octets (instructions originales) là oldInstructions = malloc(JMP_INSTR_SIZE + size); VirtualProtect(oldInstructions, size + 5, PAGE_EXECUTE_READWRITE, &old); memcpy(oldInstructions, functionToHook, size); // Ajouter un saut après les octets copiés de la fonction hookée // pour sauter en arrière vers le reste de cette fonction hookée. // Cela permet que lorsque quelqu'un appelle oldInstructions, cela agit comme la fonction originale functionToHook *(BYTE*)((DWORD)oldInstructions + size) = 0xE9; *(DWORD*)((DWORD)oldInstructions + size + 1) = (DWORD)((DWORD)functionToHook + size) - (DWORD)((DWORD)oldInstructions + size) - JMP_INSTR_SIZE; // Patcher la fonction à hooker afin de sauter vers notre propre fonction VirtualProtect(functionToHook, JMP_INSTR_SIZE, PAGE_EXECUTE_READWRITE, &old); *(BYTE*)functionToHook = 0xE9; *(DWORD*)((DWORD)functionToHook + 1) = (DWORD)myFunction - (DWORD)functionToHook - JMP_INSTR_SIZE; VirtualProtect(functionToHook, JMP_INSTR_SIZE, old, &old2); return oldInstructions; }
Pour comprendre les paquets et leur signification, il y a des tonnes de ressources en ligne décrivant les paquets pour presque toutes les versions de Lineage II, donc la tâche d'ingénierie inverse ici est presque inexistante.
Architecture du bot
Conception logicielle
Maintenant que je suis capable de recevoir un paquet et d'en envoyer un au jeu, j'ai décidé de venir avec l'architecture suivante :
Architecture logicielle du L2Bot
Comme on peut le voir sur l'image, j'injecte d'abord la DLL qui créera automatiquement un pipe nommé. Le pipe sera utilisé de sorte que lorsque le jeu reçoit un paquet, le bot est notifié qu'un paquet a été reçu, et de manière similaire lorsque le bot a besoin d'interagir et de faire une action dans le jeu, il peut l'envoyer à travers ce socket.
Grâce à cette architecture, je peux injecter et retirer le bot du processus de jeu de manière transparente ainsi que gérer plusieurs instances de jeu.
Pour l'interface graphique, j'ai décidé d'utiliser Qt pour aucune raison spécifique autre que le fait que j'étais déjà familier avec.
Automate du bot
Pour que le bot soit "intelligent", j'ai décidé d'opter pour une sorte d'automatisme qui peut réagir à des événements externes. L'automatisme actuel peut être décrit comme suit :
Automate du bot
En réalité, c'est beaucoup plus complexe car beaucoup de choses peuvent arriver à tout moment! Par exemple, vous pouvez atteindre l'état Start
avec une santé très basse, et vous ne voulez pas que votre personnage commence à frapper des monstres avec des Points de Vie (PV) bas, donc il est préférable de sauter à l'état Rest
. De plus, vous devez garder à l'esprit que rien n'est instantané, donc beaucoup de vérifications doivent être faites très souvent. Lorsque vous choisissez un monstre à attaquer et que vous l'attaquez, généralement lorsque vous jouez pour de vrai, vous ne voulez pas attaquer un monstre qui est déjà attaqué par un joueur. Pour imiter ce comportement, il est important de vérifier à plusieurs états si la cible actuelle est toujours valide. De plus, il est important de penser à chaque cas de coin. Le bot pourrait sélectionner un monstre qui a déjà été tué d'un coup par un autre joueur au moment où le ciblage a été fait, et vous ne voulez pas que le bot soit bloqué dans un tel cas, d'où mon choix d'avoir Choosing
, Target
, Targeted
, Engage
et Engaged
.
Donc chaque état aura son propre ensemble de vérifications et fera l'action seulement si chaque vérification est passée avec succès, sinon il retournera à Start
.
Lorsqu'un événement est reçu, par exemple des messages système tels que "Ne peut pas voir la cible" ou "Cible invalide", j'ai décidé de simplement vérifier dans quel état se trouve l'automate et de réagir en conséquence.
Résultats
En fin de compte, le bot fonctionne bien et n'est détecté par aucun serveur que j'ai essayé, même après des heures de farming. J'ai réussi à faire tourner 4 instances simultanément sur le même ordinateur et sur le même serveur pendant des heures sans aucun problème, tandis que je sais que d'autres outils de botting sont détectés immédiatement. Cela me laisse penser que tous les systèmes "anti-bot" qui sont achetables en ligne sont en fait seulement vérifiant les bots connus ou les signatures et n'essayent même pas de faire une analyse "avancée".
Je suis conscient que certains serveurs sont plus sophistiqués et demandent un captcha après un certain temps, mais je pense que ceux-ci sont des protections "faites maison" personnalisées qui peuvent être un peu ennuyeuses pour les joueurs, mais qui rendent plus difficile pour quelqu'un qui utilise un bot de les vaincre (bien que j'aie trouvé certaines implémentations de casseurs de captcha in-game fonctionnels en ligne).
C'est à peu près tout, en fin de compte le bot était assez simple à construire et je suis satisfait des résultats même s'il reste encore beaucoup à faire. Cependant, j'ai peur que ma nostalgie se soit envolée et que je ne travaillerai pas beaucoup sur ce petit projet.
Vous pouvez obtenir le code source sur mon dépôt GitHub : https://github.com/xarkes/L2Bot.
Interface graphique du bot Lineage 2
Il reste encore beaucoup de choses à améliorer pour le bot et si vous êtes intéressé, vous pouvez les vérifier dans la section suivante.
Aller plus loin
Le bot actuel permet d'avoir quelque chose qui fonctionne, mais personne n'aime les bots stupides, donc ci-dessous se trouvent quelques fonctionnalités que je pense qu'il serait bon d'avoir afin que le bot soit utilisable dans toutes les situations :
- Ajouter une détection automatique de la version de Lineage 2 et un support approprié pour plusieurs versions de protocole
- Supporter la fonctionnalité automatique de spoil et de sweep
- Supporter le buff automatique
- Supporter le buff de groupe
- Supporter l'utilisation d'objets (potions, parchemins, ...)
- Ajouter des conditions pour l'utilisation d'objets/buffs/compétences
- Analyser automatiquement les données du jeu afin d'obtenir et d'afficher des icônes pour les objets, compétences, noms de PNJ, etc.
- Fait amusant : les fichiers
.dat
de Lineage 2 sont chiffrés avec RSA, ce qui signifie que le client peut les déchiffrer avec la clé publique, et seul l'éditeur du jeu peut créer un fichier.dat
valide (à condition que la clé RSA ne soit pas trop faible). - En gros c'est une signature, cependant c'est
saledrôle qu'ils vérifient simplement si le résultat du déchiffrement est un format de fichier valide
- Fait amusant : les fichiers
- Ajouter des actions en cas de mort (aller en ville, se déconnecter, ...)
- Améliorations de l'UI (meilleur redimensionnement de la carte, ...)
- Meilleures interactions avec le groupe (permettre à un personnage d'assister un autre, de buff, de soigner, etc. lorsque nécessaire)
- Ajouter un moteur de script
- Cela pourrait être génial de simplement enregistrer des actions pour automatiser certaines quêtes, ou d'autres choses variées.
- Ajouter une fonctionnalité de "zone interdit" pour éviter les obstacles qui obstruent la ligne de vue du personnage lorsqu'il essaie d'attaquer un monstre
C'est tout, n'hésitez pas à me contacter si vous avez des questions, j'espère que vous avez apprécié !