1 points par GN⁺ 2023-12-06 | 1 commentaires | Partager sur WhatsApp
  • 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, ConnectAndJoinMatch et HeadlessClient
  • Dans les matchs contre un bot, la structure permettait de se connecter aux deux sièges avec le même PersonaID de 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’appeler ConcedeGame()
  • 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
  • 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 JoinMatch a été découverte
  • JoinMatch était une longue fonction de plus de 200 lignes, et un appel à ConnectAndJoinMatch a é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.NPE et MatchType.Familiar
    • NPE semblait désigner les matchs de type tutoriel de la New Player Experience
    • Familiar correspondait 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
  • HeadlessClient est 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
  • 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
    • controllerFabricUri
    • matchId
    • PersonaID
    • Jwt
    • 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 + 1U permettait d’obtenir l’autre siège
  • UnityFamiliar.SpawnFamiliar_DEBUG(...) connectait un bot au siège adverse
  • L’objet UnityFamiliar créé était ensuite retrouvé, puis cheatbot.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 InstaWin en annexe est un MonoBehaviour Unity 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

 
GN⁺ 2023-12-06
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

    • Les données doivent de toute façon être déchiffrées pour être lues, donc on en vient à faire de la rétro-ingénierie du client pour trouver comment les déchiffrer en temps réel
      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
    • J’ai fait quelque chose de similaire à l’adolescence sur Dark Age of Camelot, et ça m’a énormément servi pour apprendre le network sniffing, la différence entre un hub et un switch, ainsi que Linux
      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
    • Je me souviens que ShowEQ avait servi à prouver plusieurs théories et bugs que Verant/Sony niait sans cesse
      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
    • Je me demande en quoi le chiffrement aide vraiment. Le client doit bien déchiffrer de toute façon, donc il suffit de se brancher à cet endroit, non ?
    • ShowEQ fonctionne encore, et il y a aussi MySEQ, qui lit la mémoire sous Windows
      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

    • Ici, il n’était pas question de puissance de calcul, mais de consommation mémoire
      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
    • C’est vrai pour un desktop, mais il faut aussi se rappeler que MTGA tourne aussi sur téléphone
    • MTG est un jeu doté d’un système de règles extrêmement complexe
  • 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

    • Show HN est destiné à des projets que les gens peuvent manipuler eux-mêmes, pas à des billets de blog : https://news.ycombinator.com/showhn.html
    • Dire qu’« on peut encoder une machine de Turing dans le jeu » et dire qu’« il est difficile d’écrire un programme capable de produire des actions légales dans toutes les situations » n’est absolument pas la même chose
      Pour l’auteur du bot, la difficulté semble plutôt être la seconde
    • C’est effectivement Turing-complet : https://arxiv.org/abs/1904.09828
    • Le jeu change en permanence au fil de la rotation des nouvelles extensions, mais je crois que certaines interactions permettent, ou ont permis à différentes époques, de créer des boucles infinies
      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 me demande s’il existe de vraies stratégies qui s’approchent de ce niveau de complexité
      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)

    • Il est difficile de qualifier de bon marché un format où Black Lotus, Ancestral Recall et les Moxen sont légaux
      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

    • À en croire l’article, le jeu est justement écrit de cette manière
      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
    • Comme l’article l’indique clairement, tout le gameplay est traité côté serveur
      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
    • Le premier tiers de l’article dit explicitement que c’est implémenté ainsi
  • 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

    • Si l’adversaire ne reçoit pas une défaite normale, alors l’équipe gagnante ne devrait pas non plus recevoir une victoire
  • 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

    • Cela ne devient pas une partie à 3 joueurs ; cela reste bien une partie à 2 joueurs
      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é
      1. C’est probablement lié à la manière dont la reconnexion après déconnexion est gérée, en mettant fin à l’ancienne connexion
      2. Le bot remplace ensuite le joueur 2, soumet une reddition, et le serveur l’enregistre comme celle du joueur 2
    • MTG: Arena n’autorise pas les parties à 3 joueurs, donc le bot s’incruste simplement à la place de l’adversaire
      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é

    • Cette faille avait déjà été corrigée avant la publication de l’article, et révélée à MTG
      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...
    • https://www.17lands.com/ collecte des statistiques de victoires et défaites en limité, et enregistre aussi l’historique des parties tour par tour, en limité comme en construit. J’y ai contribué
    • Pour la collecte des données de jeu, https://mtgaassistant.net/ est probablement le plus courant, à ma connaissance
    • Il me semble que cette fonctionnalité existe déjà. C’est probablement le journal du joueur
      Si je me souviens bien, il y avait pas mal d’applications qui faisaient du suivi à partir de ce journal