- Dans Magic: The Gathering Arena, il était possible de faire concéder l’adversaire au moment voulu, créant ainsi une situation où l’on ne pouvait pas perdre dans les parties en matchmaking
- Les jeux de cartes se prêtent généralement bien à une architecture à autorité serveur, où le serveur gère l’état complet, mais l’adversaire bot Sparky de MTGA était implémenté via une logique de client local
- Le client MTGA basé sur C# permettait d’accéder aux objets à l’exécution et aux champs privés via la réflexion, et le code décompilé révélait des noms comme
JoinMatch,ConnectAndJoinMatchetHeadlessClient - Dans les matchs contre un bot, la structure permettait de se connecter aux deux sièges avec le même
PersonaIDde compte et le même JWT ; en l’appliquant à un match normal, il était possible d’attacher un client headless au siège adverse puis d’appelerConcedeGame() - Le serveur a ensuite été patché pour empêcher, dans les parties en matchmaking, que les deux sièges utilisent le même compte et le même JWT ; c’est un exemple où la frontière entre une implémentation de bot côté client et la vérification d’authentification des sièges a eu un impact direct sur la sécurité en conditions réelles
Pourquoi il est difficile de pirater un jeu de cartes
- Les jeux de cartes sont au tour par tour et échangent relativement peu d’informations entre le client et le serveur, ce qui convient bien à une architecture à autorité serveur
- Le serveur peut gérer l’état complet de la partie et ne transmettre au client que les informations nécessaires
- Les informations non publiques, comme la main ou le deck de l’adversaire, n’existent pas localement
- Les tentatives consistant à modifier l’ordre du deck ou à manipuler la carte piochée sont difficiles, car le serveur exécute l’action et n’en communique que le résultat
- Contrairement aux jeux de tir à la première personne, les actions des joueurs dans un jeu de cartes sont limitées et se produisent à des moments définis
- Dans un FPS, des informations comme la position des modèles ennemis peuvent être mises en cache à l’avance côté client, ce qui rend possibles des formes de wallhack
- Le billet de Riot sur l’anti-wallhack traite d’une approche visant à réduire les données de position des joueurs côté client
- Il est aussi relativement plus facile de détecter les actions invalides
- Jouer une carte que l’on n’a pas en main
- Agir alors que ce n’est pas son tour
Point de départ de l’analyse : réseau et client C#
- L’approche choisie pour commencer le piratage du jeu a été d’examiner les communications réseau
- La présentation DEF CON de Manfred sur des cas de piratage de MMO montre que la rétro-ingénierie des protocoles réseau est au cœur de nombreuses analyses de bugs
- MTGA étant écrit en C#, il était possible de manipuler des objets à l’exécution au lieu de hooker les fonctions d’envoi et de réception du trafic
- La réflexion .NET permet aussi d’accéder aux champs et méthodes privés
- Un tutoriel sur le piratage de jeux Unity par réflexion est donné comme exemple de base lié
- Grâce aux metadata tokens, les assemblies .NET non obfusqués permettent de voir les noms des fonctions, variables et classes sous une forme lisible par un humain
- De ce fait, le résultat de la décompilation s’apparente presque à une revue de code source
La structure de Sparky découverte dans JoinMatch
- En recherchant des noms de fonctions liés à l’initialisation de l’entrée dans un match, la fonction
JoinMatcha été découverte JoinMatchétait une longue fonction de plus de 200 lignes, et un appel àConnectAndJoinMatcha été identifié dans sa partie inférieure- Cette fonction semblait correspondre au flux qui reçoit les informations de configuration du match puis se connecte au serveur de jeu
- Le même flux de code contenait des branches
MatchType.NPEetMatchType.FamiliarNPEsemblait désigner les matchs de type tutoriel de la New Player ExperienceFamiliarcorrespondait aux matchs standards contre un bot
- La logique appelée dans cette branche était liée à Sparky, l’adversaire bot de MTGA
- Sparky est l’adversaire de type mascotte utilisé dans les tutoriels et les combats contre un bot de MTGA
- Dans les matchs contre un bot, la logique du bot s’exécute dans le client de jeu de la machine locale
Bot local et HeadlessClient
- Le véritable gestionnaire de logique du bot se trouvait dans la classe
HeadlessClient HeadlessClientest un client headless qui ne rend pas le plateau de jeu et qui se connecte au serveur pour jouer la partie- Le client bot créé localement utilise les mêmes informations d’authentification que le client de jeu
- Le
PersonaID, qui sert d’identifiant utilisateur - Le JSON Web Token attribué au jeu après la connexion
- Le
- Dans un match contre un bot, le fait que le même client se connecte en pratique aux deux côtés du match n’était pas considéré comme un problème par le serveur
- Le siège (seat) sert à distinguer quel joueur on est dans la partie
- Dans les matchs contre un bot, il était possible de remplir des sièges différents avec les mêmes informations d’authentification
Méthode de prise de contrôle d’un match normal
- La logique des matchs contre un bot a été appliquée à un match normal pour tester s’il était possible de se connecter aux deux sièges
- Les informations nécessaires étaient récupérées depuis les objets du jeu à l’exécution
- La configuration du match en cours
- L’hôte et le port du serveur de match
controllerFabricUrimatchIdPersonaIDJwt- La base de données des cartes
- Le gestionnaire de match
- Le système de recherche des assets
- Le siège adverse était calculé à partir de son propre siège
- Dans le code,
man.LocalPlayerSeatId % 2U + 1Upermettait d’obtenir l’autre siège
- Dans le code,
UnityFamiliar.SpawnFamiliar_DEBUG(...)connectait un bot au siège adverse- L’objet
UnityFamiliarcréé était ensuite retrouvé, puischeatbot.Client.Gre.ConcedeGame()était appelé pour le faire concéder immédiatement
Résultat et patch
- Cette méthode fonctionnait aussi dans les matchs normaux
- Elle fonctionnait même lorsque l’adversaire était déjà connecté et que la partie était en cours
- Comme il s’agissait d’une partie en matchmaking, les récompenses étaient obtenues comme si un adversaire humain avait été battu
- Le cœur de la vulnérabilité était que le serveur de matchs normaux autorisait les deux sièges à se connecter avec le même compte et le même JWT
- Le serveur a ensuite été patché afin d’empêcher, dans les parties en matchmaking, que les deux sièges utilisent le même compte et le même JWT
- Le code
InstaWinen annexe est unMonoBehaviourUnity qui crée un bouton d’interface graphique et, lors du clic sur ce bouton, collecte les informations du match en cours, attache un bot au siège adverse, puis le fait concéder
1 commentaires
Commentaires Hacker News
Ce qui m’a vraiment poussé à creuser Linux pour la première fois, c’était d’inspecter le trafic réseau avec ShowEQ pour EverQuest
À l’époque, le trafic n’était pas chiffré et contenait beaucoup d’informations utiles. Je dupliquais le trafic vers une machine Linux via un hub pour dessiner une carte en temps réel de la zone, afficher la position des monstres, des PNJ et des joueurs, et même le butin porté par les monstres, ce qui permettait de ne cibler que certains monstres précis. L’avantage, c’est que comme c’était passif, c’était impossible à détecter, puis SOE s’en est rendu compte et a commencé à chiffrer le trafic
Ensuite, l’adversaire introduit des signatures basées sur des clés, on essaie alors de voler les clés depuis le client, de casser le chiffrement, puis des systèmes anti-triche apparaissent, et le jeu du chat et de la souris commence
Est-ce que ça valait le coup pour l’avantage en jeu ? Non. Je ne jouais pas assez sérieusement ; j’ai passé deux semaines à l’installer et je ne l’ai utilisé qu’environ une semaine, mais comme expérience d’apprentissage, c’était excellent
Par exemple l’existence des hell levels, le fait que les halflings — et non les humains — bénéficiaient d’un bonus d’expérience, qu’il existait bien des écarts d’expérience selon la race et la classe, ou encore que l’alchimie des Shaman au début était réellement cassée. Et je crois que ça a ensuite mené à eqemulator.org
L’actuel propriétaire d’EQ semble ne pas trop s’en soucier, donc on peut plutôt les utiliser sans problème. Cela dit, aucune des deux applis n’a jamais affiché le butin porté par les monstres, à part l’équipement visible. Avec le temps, certaines données ont aussi changé : autrefois, les PV exacts des monstres étaient transmis, alors qu’aujourd’hui seul un pourcentage est envoyé
Je ne comprends pas très bien le passage disant que « le bot presque complet capable de jouer à des parties arbitraires de Magic: The Gathering est assez petit pour tourner sur la machine locale »
Si l’IA de MTG était trop lourde pour tourner sur la machine du client, j’imagine mal qu’elle tournerait côté serveur non plus. Pour des bots de jeu de cartes, on ne facture généralement pas à la partie, donc le coût grimpe vite. Un serveur n’est pas magique non plus : il utilise le plus souvent les mêmes CPU x86 qu’une machine locale, souvent à une fréquence plus basse qu’un desktop. Pour réduire le temps de tour du bot par rapport à une exécution locale, il faudrait probablement lui allouer bien plus de cœurs que chez le client ; avec 8 à 16 cœurs par joueur, ce serait un cauchemar aux pics de connexion. Et si le joueur CPU ne prend pas en charge le multicœur, une exécution locale devrait de toute façon être plus rapide
L’idée est que le moteur de règles du bot est assez compact pour tenir dans la mémoire d’un vieil iPhone ou d’un appareil Android. Sur un serveur, on peut garder en mémoire beaucoup de machines à états ou de moteurs de règles, et l’exécution d’une requête particulière peut ne presque rien coûter en calcul
Daniel, encore félicitations pour être revenu en haut de page. Après la publication de ce billet, j’ai essayé de hacker MTGA et j’en ai aussi un peu parlé sur GitHub [0]
Pour les personnes intéressées, je travaille en ce moment, à l’arrêt, sur un client non officiel de MTGA presque sans fonctionnalités. L’objectif est d’automatiser le jeu classé et de proposer des bots adverses plus solides. J’ai été pris par d’autres choses récemment et je ne vois toujours pas clairement comment construire une bonne UI facile à lire, donc il m’est difficile d’estimer quand cela deviendra au moins un minimum utile. À part ça, je serais toujours curieux d’entendre Daniel ou d’autres raconter leurs expériences de game hacking. J’aimerais en savoir plus sur la façon de trouver ce genre de bugs, comment éviter de s’inquiéter d’un bannissement après avoir hacké, la manière de divulguer les bugs comme l’a mentionné @aethros, ou encore comment structurer un client non officiel pour un jeu de cartes
[0] https://github.com/MayerDaniel/mayerdaniel.github.io/issues/...
Dire qu’on s’attendait à un gros overhead pour créer un adversaire IA dans un jeu complexe comme MTG est un assez gros euphémisme
Le jeu est pratiquement Turing-complet, et même sans compter les boucles infinies, les gens s’amusent beaucoup avec ça. Cela dit, j’imagine qu’il existe déjà pas mal de recherche sur la stratégie d’IA. Et comme le billet a été publié par son propre auteur, on pourrait presque mettre « Show HN: » dans le titre
Pour l’auteur du bot, la difficulté semble plutôt être la seconde
Par exemple en créant et sacrifiant des créatures-jetons avec des effets d’arrivée en jeu, de sortie de jeu ou de dégagement. Et ces boucles n’infligent pas toujours des dégâts à l’un des deux camps
Je n’ai jamais joué à MTG, mais cette affirmation semble vouloir dire que c’est techniquement possible si l’on construit volontairement une structure avec état, pas que cela sert réellement à gagner
Jouer à Old School Magic 93/94 avec des cartes physiques avec mon fils est un vrai plaisir
Nous allons chaque année à Madrid pour participer au championnat du monde de 7pts Singleton. Cet été, mon fils a fini 9e, et j’en suis très fier. Le 7pts Singleton est un excellent format, avec une grande variété de constructions de deck possibles et un gameplay équilibré pour un coût relativement abordable (https://7pts-singleton.com)
Même si on ne peut pas tous les jouer en 7pts Singleton, un seul de ces cartes coûte déjà de quelques milliers à plusieurs dizaines de milliers de dollars. Cela dit, c’est super de pouvoir partager ce jeu avec son fils, et félicitations pour ce classement
Une logique de jeu entièrement côté client ? Quand je faisais de petits jeux, il m’est arrivé de devoir mettre de la logique sur le client pour la réactivité, mais rien n’empêche de réexécuter la même logique côté serveur, donc c’est ce que je faisais
Cela peut être difficile pour des jeux en temps réel comme les FPS ou les RTS, mais pour un jeu de cartes, il n’y a aucune excuse. Dans un jeu de cartes comme celui-ci, le client ne devrait jamais recevoir plus d’informations que ce qu’un vrai joueur peut voir. Par exemple, il ne faut pas envoyer le contenu de la main adverse, seulement le nombre de cartes. Les actions envoyées au serveur ne devraient concerner que soi-même, donc il ne devrait pas être possible de déclarer la reddition de l’adversaire. Si je dis « Je concède ! », le serveur doit l’interpréter comme ma propre reddition
Il ne reçoit que les informations nécessaires au moment où elles sont nécessaires, et n’envoie que ses propres actions. La faille venait du fait qu’on pouvait lancer un second client et se connecter à la partie en cours sur le siège de l’adversaire. Une fois cela fait, il devenait possible d’envoyer les actions de l’adversaire, y compris la reddition
Ce qu’a exploité l’auteur n’était pas une logique de jeu côté client, mais un problème d’autorisation et de code d’accès à la partie. Dire qu’« on ne devrait pas pouvoir concéder à la place de l’adversaire » revient simplement à répéter le fonctionnement de l’exploit découvert par l’auteur
Dans League of Legends, il y a déjà eu un bug de division par zéro avec certaines combinaisons de champions et d’objets, qui faisait planter le serveur après avoir expulsé tous les joueurs
Comme l’abuseur était expulsé en dernier, son équipe recevait la victoire, tandis que l’équipe adverse obtenait un Loss Prevented au lieu d’un résultat normal
J’aime bien cet article : il reste accessible tout en apportant des détails pertinents
En revanche, je ne comprends pas le fait de connecter un bot pendant une partie réelle. Pourquoi le jeu autorise-t-il une arrivée en cours de match, et pourquoi la reddition du bot est-elle traitée comme celle de l’adversaire ? Même dans un jeu à 3 joueurs, le fait que le joueur 3 concède ne devrait pas faire concéder aussi le joueur 2
Le code détermine l’indice de siège de mon compte, puis fait rejoindre le bot avec l’autre indice. Le problème, c’est qu’il ne vérifie pas si l’utilisateur qui rejoint est bien celui qui devrait occuper ce siège. On pourrait aussi penser qu’il ne faudrait pas autoriser une reconnexion sur un siège déjà connecté, mais pour un jeu compatible mobile, on veut sans doute une fenêtre de reconnexion assez longue. On ne veut pas bloquer un joueur qui a eu une brève coupure et tente de se reconnecter avant que le délai n’ait considéré l’ancienne connexion comme morte. J’ai déjà vu dans d’autres jeux des messages du type « impossible de rejoindre une partie en cours » pendant une dizaine de secondes avant qu’une reconnexion ne finisse par fonctionner. Pour prendre une analogie, c’est comme débarquer à un tournoi MTG dans une boutique locale, pousser quelqu’un de sa chaise, s’asseoir et crier « Je concède ! », puis voir l’arbitre considérer que, puisque la personne assise sur cette chaise l’a dit, le joueur B a bien abandonné
C’est sans doute pour cela qu’il pouvait concéder à sa place. Les développeurs n’ont peut-être tout simplement pas imaginé que quelqu’un essaierait un jour une telle manipulation, donc ils n’ont pas mis de vérification
Cela me rappelle l’époque de Diablo 2, où l’on pouvait réutiliser le même paquet de connexion pour faire entrer des personnages de serveur ouvert (LAN) sur les serveurs internet officiels de bnet
Comme toutes les données des serveurs ouverts étaient stockées en local, on pouvait créer toutes sortes d’objets qui n’auraient jamais dû exister, et les serveurs officiels les acceptaient quand même
Je me suis remis à MTG récemment avec MTGA. Comme c’est un jeu Unity sans il2cpp, je l’ai rapidement décompilé ; et même si c’avait été du il2cpp, je doute que cela aurait offert une grande protection. J’y ai trouvé des choses assez amusantes
Il y avait par exemple des clés pour les builds Epic Launcher ou des API non documentées. Je ne cherche pas à tricher, j’aimerais juste avoir un historique des combats : savoir quels matchs j’ai gagnés ou perdus, ou revoir l’état du champ de bataille à la fin d’une partie. Je vais aussi jeter un œil à cet article, et j’espère que ce sera vite corrigé
Si tu veux des historiques, tu peux regarder https://untapped.gg/en. J’ai un peu échangé avec eux, et cela fait pratiquement ce que tu veux. La plupart des informations proviennent du journal de débogage MTG dans le répertoire de l’application MTGA, donc si tu veux tu peux aussi créer ton propre tracker. Le site l’explique aussi ici : https://help.hearthsim.net/en/articles/3620440-how-do-i-supp...
Si je me souviens bien, il y avait pas mal d’applications qui faisaient du suivi à partir de ce journal