- L’environnement des addons Lua de World of Warcraft permettait de modifier facilement l’interface, mais séparait l’accès à l’information et les droits d’exécution pour empêcher l’automatisation des actions en jeu
- Le modèle de sécurité de Blizzard séparait un environnement non sécurisé, capable de lire beaucoup d’informations, et un environnement sécurisé, capable d’exécuter des actions, cette frontière étant au cœur de la prévention contre l’automatisation
- La fonction
random, autorisée dans l’environnement sécurisé, n’était qu’un léger wrapper autour de rand en C de MSVC, un générateur congruentiel linéaire faible partageant un état global, ce qui en faisait un canal possible de transmission d’informations
- Avec seulement deux sorties du RNG, il était possible de réduire les candidats pour l’état interne à 8 au maximum, puis d’utiliser des sorties supplémentaires pour n’en garder qu’un seul, avant de faire avancer le RNG jusqu’à obtenir l’indice d’action souhaité
- En apparence, cela ressemblait à une sélection aléatoire d’actions, mais en pratique cela permettait de choisir l’action voulue, comme un sort précis ; on ne sait pas exactement comment Blizzard a ensuite corrigé le problème
Les addons World of Warcraft comme porte d’entrée vers la programmation
- Les addons World of Warcraft reposaient sur un fonctionnement où le jeu chargeait directement quelques fichiers source
.lua placés dans un dossier, écrits en Lua
- Il suffisait de modifier un fichier, de l’enregistrer, puis de recharger l’interface pour voir immédiatement le résultat, ce qui rendait la barrière à l’entrée très faible
- Au début, plusieurs addons ont été créés en copiant du code d’autres personnes, puis en le refactorisant ou en le recomposant pour obtenir les fonctions voulues
- Avec le temps, davantage de temps a été consacré à écrire des addons qu’à jouer réellement, et World of Warcraft est ainsi devenu le véritable point de départ d’un parcours en programmation
Le modèle de sécurité des addons chez Blizzard
- Si les utilisateurs pouvaient créer des addons totalement programmables, cela poserait un problème de bots ; Blizzard a donc conçu une architecture limitant l’automatisation accessible aux joueurs ordinaires
- La plupart des éléments d’interface et des API de collecte d’informations pouvaient être utilisés sans restriction
- Par exemple, il était possible de créer une barre de vie qui lisait les points de vie du personnage pour ajuster la taille d’un cadre au premier plan
- Les API qui effectuaient de véritables actions en jeu, comme déplacer le personnage, lancer un sort ou utiliser un objet, étaient protégées et ne pouvaient être appelées que depuis le code officiel de Blizzard
- Les API permettant d’obtenir la position exacte dans le monde et l’orientation de la caméra ont elles aussi été protégées à partir d’un certain moment
- Cette mesure visait à contrer des addons qui dessinaient des éléments 3D au-dessus du monde du jeu pour rendre les combats de boss plus faciles
Séparation entre environnement sécurisé et non sécurisé
- Comme certains éléments d’interface devaient interagir avec de véritables actions du jeu, il était possible de créer des boutons spéciaux exécutant du code dans l’environnement sécurisé lors d’un clic
- Ces boutons ne pouvaient pas être créés, supprimés ni déplacés pendant le combat
- Cela empêchait d’automatiser des actions en plaçant conditionnellement un bouton sous le curseur en plein combat
- Dans l’environnement sécurisé, il était possible de programmer quel sort lancer, mais l’accès à l’état externe nécessaire à une automatisation arbitraire était bloqué
- Seules certaines API de collecte d’informations comparables à celles du système de macros étaient autorisées, tandis que des informations nécessaires pour choisir le meilleur sort, comme le temps de recharge des techniques ou les points de vie d’une unité, n’étaient pas fournies
- Au final, les deux environnements avaient des permissions différentes
- Environnement non sécurisé : peut obtenir beaucoup d’informations, mais ne peut pas agir
- Environnement sécurisé : peut agir, mais ne peut pas obtenir les informations nécessaires à l’automatisation
random comme canal dérobé
- L’objectif n’était pas de désactiver les restrictions de sécurité avec un logiciel tiers, mais de vérifier s’il était possible de contourner l’environnement sécurisé en n’utilisant que des outils techniquement autorisés
- En examinant la liste des fonctions autorisées dans l’environnement sécurisé,
random a attiré l’attention
- Comme les générateurs de nombres aléatoires des ordinateurs sont généralement des générateurs pseudo-aléatoires dotés d’un état interne, l’idée était que, s’il était possible de manipuler cet état, on pourrait transmettre des informations externes vers l’environnement sécurisé
- En réalité,
random n’était qu’un petit wrapper autour de rand en C
- Comme World of Warcraft était compilé avec MSVC, l’implémentation de
rand reposait sur un générateur à état global
uint32_t state;
int rand() {
state = state * 214013 + 2531011;
return (state >> 16) & 0x7fff;
}
- Cette implémentation partageait un unique état aléatoire global dans tout le processus et reposait sur un générateur congruentiel linéaire faible
Restauration de l’état de rand sous MSVC
- La sortie de
rand exposait directement 15 bits qui faisaient partie de l’état
- Comme l’espace des états était petit et qu’une partie de la sortie était visible, il était possible de conserver des candidats d’état plausibles puis d’éliminer les mauvais à l’aide de sorties supplémentaires
- Une méthode plus efficace consistait à observer deux sorties du RNG,
r0 et r1, afin de calculer les candidats pour la partie cachée de l’état sur 16 bits
- Les constantes de
rand sous MSVC étaient les suivantes
- Si l’on note
h0 et h1 les parties cachées sur 16 bits après le premier et le deuxième appel respectivement, on peut déduire les candidats de h0 à partir de l’équation de transition d’état
- Le résultat du calcul montrait qu’il suffisait d’examiner les valeurs de
k dans l’intervalle -1 <= k < 7, ce qui réduisait les candidats pour l’état caché à 8 au maximum
- Il suffisait ensuite de comparer les résultats d’appels RNG supplémentaires avec la sortie des RNG candidats pour éliminer les mauvais candidats
- L’implémentation Python d’exemple utilisait une boucle
while, mais en pratique la troisième sortie semblait suffire pour réduire à un candidat unique
- À l’époque, comme cette dérivation en mathématiques discrètes était difficile à faire seul, une question a été posée sur crypto.SE, avec comme justification : « montrer à un collègue à quel point ce RNG est faible »
Choisir l’action voulue dans l’environnement sécurisé
- Une fois l’état interne du RNG restauré, il devenait possible de transmettre à l’environnement sécurisé des décisions d’automatisation arbitraires
- Le mécanisme fonctionnait ainsi
- Enregistrer un hook non sécurisé exécuté juste avant l’exécution du code de l’environnement sécurisé
- Dans ce hook, comme toutes les informations sont accessibles, décider quelle action effectuer
- Retrouver cette action dans une liste d’actions codée en dur et la convertir en indice
- Restaurer l’état actuel du RNG avec la méthode décrite plus haut
- Si la prochaine sortie du RNG, modulo la longueur de la liste d’actions, ne correspond pas à l’indice voulu, faire avancer le RNG et vérifier à nouveau
- Quand vient enfin le tour de la sortie souhaitée, le hook se termine et l’environnement sécurisé appelle
random pour exécuter l’action à cet indice
- Dans l’environnement sécurisé, cela ressemblait à une indexation de la liste d’actions à l’aide d’un nombre aléatoire, mais en réalité le moment du prochain appel à
random était ajusté pour faire sélectionner l’action voulue
- Si la liste d’actions avait une taille
n, il fallait en moyenne faire avancer le RNG de n étapes pour transmettre la valeur souhaitée, mais cela ne posait pas de problème en pratique
Ce qu’il s’est passé ensuite
- Lors d’une nouvelle tentative quelques années plus tard, cette méthode ne fonctionnait plus
- On ne sait pas quand Blizzard a corrigé le problème lié à cet état de RNG faible et partagé, ni même s’il l’a explicitement identifié comme tel
- Parmi les changements possibles : un passage à un autre algorithme, ou l’introduction d’un état de RNG correctement isolé dans l’environnement sécurisé
- Le code envisagé n’a pas été utilisé, mais il s’agissait d’un cas limité de contournement de la sécurité d’un jeu où un comportement apparemment aléatoire avait bien été manipulé pour produire le résultat voulu
1 commentaires
Avis sur Hacker News
Il y a longtemps, quand je travaillais comme GM sur WoW, une certaine version du populaire add-on Titan Bar avait un bug qui appelait en boucle l’API affichant l’interface « créer un ticket pour un GM ».
Même si on annulait, elle réapparaissait ; à part supprimer l’add-on en cause, il fallait écrire quelque chose dans la zone de texte et l’envoyer pour faire disparaître la fenêtre.
Côté GM, ça a été vraiment pénible pendant un ou deux jours, et beaucoup de joueurs ne savaient même pas ce qu’était un GM, ni qu’ils pouvaient envoyer un ticket.
Expliquer que « vous avez probablement soumis un ticket à cause d’un add-on, et je vous contacte en tant qu’agent du support client » n’était généralement pas simple non plus.
Comme cela ruinait pratiquement les temps de réponse normaux, les développeurs ont publié un hotfix et ont placé cet appel d’API derrière une protection, de sorte qu’il ne soit possible que depuis le code LUA propre à Blizzard.
C’était un add-on qui regroupait plusieurs éléments d’interface, comme le total d’or ou le pourcentage de réparation, dans une barre unique.
Curieusement, j’avais répondu sur StackOverflow à une question presque identique sur le fait de « résoudre un générateur de nombres aléatoires à l’envers », quelques mois avant qu’une question soit posée sur Crypto.SE : https://stackoverflow.com/a/15237585/1204143
Sauf que j’attaquais le générateur aléatoire de Java au lieu de celui de MSVC.
Les deux sont de simples générateurs congruentiels linéaires (LCG) qui sortent une valeur tronquée de leur état interne, donc les méthodes d’attaque sont très similaires.
import java.util.Random;
public class Hayley {
public static void main(String... args) {
byte[] b1 = new byte[4], b2 = new byte[2];
(new Random(0x2effe2140e00L)).nextBytes(b1);
(new Random(0xc2f0097)).nextBytes(b2);
System.out.println(new String(b1) + new String(b2));
}
}
Je me demande s’il était possible que le serveur utilise le même générateur de nombres aléatoires.
Si c’était le cas, avec un serveur très peu utilisé, une faible latence, la capacité de réduire assez précisément l’état courant du générateur, et la possibilité de redéclencher rapidement un événement dont l’issue aléatoire a un fort impact économique, cela semblerait théoriquement exploitable.
Blizzard a publié pas mal de détails sur la façon dont la génération aléatoire côté serveur fonctionnait, dans une tentative globalement ratée de dissiper les rumeurs de joueurs superstitieux convaincus qu’il existait des choses comme des « graines de butin ».
L’économie et les incitations des générateurs de nombres aléatoires sont fascinantes.
Quand il n’y a pratiquement rien en jeu et que les gens ne peuvent pas facilement s’en apercevoir, un générateur imparfait, voire mauvais, peut suffire pour un jeu.
Mais dès que de l’argent ou de la sécurité est en jeu, comme pour un portefeuille de cryptomonnaies ou un casino en ligne, cela devient soudain extrêmement important.
Plus grave encore, le générateur congruentiel linéaire présenté ne produit que 12 445 nombres distincts avant de boucler.
Utiliser simplement de meilleurs paramètres pour cet algorithme aurait déjà été bien plus raisonnable.
Pour des tâches simples, comme le mouvement de particules dans des traces de fumée, il faut un générateur de nombres aléatoires simple mais rapide.
En cryptographie ou dans les jeux de casino, entre autres, on utilise non seulement un meilleur générateur avec au moins 32 octets d’état, mais on fait aussi avancer le moteur à des intervalles aléatoires pour rendre les résultats encore plus difficiles à prédire.
World of Warcraft est ce qui m’a donné envie de m’intéresser à la programmation.
J’avais 6 ans à sa sortie, et quand les serveurs privés ont commencé à apparaître, j’ai voulu comprendre comment créer mes propres PNJ, alors j’ai appris Lua.
Beaucoup de mes messages de l’époque sur le forum MMOwned existent encore, et l’une des personnes qui m’a aidé à apprendre poste encore de temps en temps.
En compilant moi-même des cœurs de serveurs privés, j’ai appris la compilation, SVN et l’application de patchs ; il me semble qu’il y avait un bug lié à Fireball chez le mage, mais je ne m’en souviens pas précisément.
Aujourd’hui, j’ai l’impression qu’une grande partie du meilleur d’Internet a été remplacée par des applications de réseaux sociaux dopaminergiques et des forums fermés comme Reddit.
Si je reste encore sur HN, c’est sans doute parce que cela me rappelle ces vieux forums.
« Quiconque cherche à produire des nombres aléatoires par des méthodes arithmétiques est, bien sûr, dans un état de péché. Comme on l’a souligné à maintes reprises, les nombres aléatoires n’existent pas. Il n’existe que des méthodes pour produire des nombres aléatoires, et une procédure arithmétique stricte n’en fait évidemment pas partie. » — John von Neumann
Je n’en veux pas à Neumann, mais les gens qui la propagent comme un sage aphorisme ne font, la plupart du temps, que diffuser de la désinformation.
Dans son contexte, cela ressemble davantage à une plaisanterie philosophique superflue.
Nous disposons depuis des décennies de générateurs de nombres aléatoires cryptographiques rapides et fiables.
Ils fonctionnent correctement avec une très petite graine et des opérations arithmétiques ; les nombres produits sont impossibles à distinguer de ceux d’une autre source aléatoire correcte, et peuvent servir à n’importe quel usage.
Trop de gens ne comprennent pas cela, au point que certaines entreprises vendent même de l’huile de serpent sous l’étiquette de « vrais nombres aléatoires ».
Un très bon exemple de générateur de nombres aléatoires vraiment horrible se trouve dans Monster Hunter 3 Ultimate, et peut-être aussi dans d’autres opus.
Non seulement il était entièrement exploitable pour obtenir du bon butin, mais il a aussi donné naissance aux « charm tables ».
Certains objets ont des statistiques générées aléatoirement, mais la graine initiale étant enregistrée dans le fichier de sauvegarde, l’éventail des objets qu’on pouvait obtenir était limité.
Avec de la malchance, on pouvait se retrouver sur une « table maudite », qui est en fait un générateur de nombres aléatoires à période très courte, et ne jamais pouvoir obtenir les meilleures valeurs.
Ce n’est pas au point de ruiner le jeu, mais c’est mauvais pour le jeu à haut niveau.
J’avais écrit un script pour l’hôtel des ventes.
Il scannait toutes les enchères, achetait les objets sous-évalués et les remettait en vente.
Si quelqu’un publiait moins cher, je pouvais annuler puis republier juste en dessous, et cela créait en pratique de l’or illimité.
« Eh bien, en fait c’est une assez bonne analogie. La différence, c’est… euh… oui, c’est une assez bonne analogie ! »
« Quand les variables ne varient pas et que les constantes ne restent pas constantes »
https://www.theregister.com/2006/07/26/constants_are_not/
« J’ai lu pour la première fois cette lamentation sur les caprices de la programmation, “Variables Won't Constants Aren't”, dans Creative Computing, plusieurs années avant la création de C. »
S’il existe un document particulier qui vous semble être la version canonique de ce mème, j’aimerais le lire.