Dofus et le reverse-engineering (1/2)

Mes expériences démoniaques sur Dofus, un jeu vidéo Flash.

What the fuck is Dofus ?

Je n'ai jamais vraiment joué à Dofus mais un pote m'a un jour dit: "Hé, ça serait cool de faire un serveur privé Dofus !". Alors bêtement, moi je me suis dit que ça devait être simple à faire. Je lui ai répondu cette phrase magique: "tkt bro, c'est fait dans 20 minutes" (spoiler alert: ça n'a pas pris 20 minutes). Pour un peu de contexte, Dofus est un MMORPG écrit en flash, très populaire dans les années 2010. Ce qui est assez drôle avec ce jeu est qu'il est français et qu'il dispose d'une assez grosse communauté, mais entièrement française elle aussi. Le jeu ne s'est quasiment pas développé à l'étranger.

Voilà à quoi ressemble la bête. J'aime beaucoup la patte graphique, pour un jeu 2D !

Je commence donc par regarder sur le site de l'éditeur, Ankama, pour créer mon propre serveur privé. Mais bien sûr, il n'est pas possible de faire de serveur privé officiellement, j'aurais dû m'en douter. Bien que le jeu soit gratuit de base, il propose un système d'abonnement bien huilé qui fait dépenser des fortunes à ses joueurs pour pouvoir progresser ... Proposer des serveurs privés n'est donc pas dans les interêts d'Ankama !

D'où l'idée d'un serveur privé

Mais ... C'est sans compter la magnifique communauté de Dofus, qui va bien sûr remédier à ce problème ! (Quand je dis magnifique communauté c'est ironique, vous allez vite découvrir pourquoi ...). C'est là que je découvre sur des sites comme cadernis.fr et dozenofelites.com le petit monde de l'émulation Dofus. Alors pour bien comprendre pourquoi on parle d'émulation, je vous ai préparé un petit schéma pas piqué des hannetons:

Fait avec Paint à la souris, en toute fierté

Comme vous le savez probablement déjà, dans les jeux-vidéos votre ordinateur communique avec un serveur, qui va indiquer la position des autres joueurs, et vous envoyer un tas d'informations utiles. L'astuce, avec l'émulation Dofus, se trouve dans le petit engrenage que j'ai dessiné. Le serveur, lorsqu'il reçoit un message du client, doit traiter celui-ci. Si on arrive à émuler le comportement du serveur, c'est à dire à traiter de la même façon que lui les messages qui lui parviennent, et à renvoyer des messages cohérents aux clients, alors on a gagné !

Ce qu'il faut bien comprendre, c'est que le client est une simple marionnette, il ne décide rien de lui-même. Toutes les informations qu'il transmet doivent être auparavant validées par le serveur (sinon je modifie mon client et je dis au serveur de me téléporter à l'autre bout du jeu !). Ce qu'on peut donc faire dans le cas de Dofus, c'est garder le client, mais coder notre propre serveur, qui doit avoir un comportement aussi proche que possible de l'original.

Donc arrivé à ce moment là, je commence à réaliser que en fait la config du serveur en 20 minutes c'est un peu mort. "Pas grave, me dis-je bravement. Je vais récupérer le meilleur émulateur open-source et comme ça on aura accès à la dernière version rapidement !" (Spoiler: là encore je me trompais).

C'est là qu'arrive le fameux dozenofelites.com. Véritable paradis de l'émulation Dofus, de codeurs brillantissimes tous plus géniaux les uns que les autres.

Plus toxique que Chernobyl en 1986

Pour résumer les choses brièvement, on peut dire que l'émulation Dofus tourne autour d'un émulateur, appelé Stump, que tout le monde se vole, modifie et s'approprie. Le reste de la communauté à une moyenne d'âge et de Q.I qui tourne autour de 12, apparemment. Enfin bon. En récupérant sur le forum un émulateur Stump pas trop vérolé, on a pu faire tourner notre petit serveur privé, après beaucoup de sueur et de coups de pied dans la base de données MySQL. Enfin bon. Là je raccourcis le truc encore, parce que Stump est écrit en C# non compatible Mono, donc pour le faire tourner sur mon serveur Debian j'ai dû mettre en place une machine virtuelle avec des redirections NAT hasardeuses ainsi que AAAAAAAAAAAAAAAHHHHHHHHHHHHH l'essentiel c'est que ça marche maintenant non ?

Ma brave VM du haut de ses 3,5Go de RAM

D'où l'idée de mon serveur privé

Donc vous imaginez bien que toutes ces péripéties m'ont mis en bouche. Les problèmes principaux de Stump sont que

  • il est volé
  • il ne recevra pas de mises à jour car il est volé
  • il ne peut pas tourner nativement sur Linux
  • c'est pas moi qui l'ait fait
  • il date de 2018
  • c'est pas bien de voler

Sur cette base de réflexion avancée, j'ai donc décider de me lancer dans mon propre émulateur (je viens à peine de le commencer au moment où j'écris ces lignes, mais j'ai appris des choses captivantes sur le sujet du coup je partage !!).

Alors comment on se lance ? Tout d'abord on choisit le tech stack, bien sûr ! Ce qui est important, pour plus tard, c'est d'utiliser un langage qui supporte assez facilement la réflectivité. J'ai donc jeté mon dévolu sur NodeJS, du JavaScript côté serveur. Et puisque TypeScript a le vent en poupe, je me suis dit que ça serait un bon moment pour apprendre ce truc là en même temps. TypeScript fait la promesse d'un JavaScript typé plus strict, ce qui m'a paru essentiel dans un gros projet.

Espionnage industriel

Dans notre projet, on va devoir mimiquer le serveur Dofus officiel. Quoi de mieux que d'écouter ce qu'il se passe entre le serveur et le client pour commencer ? Je vous poste mon plan d'attaque en mettant à jour mon xXX_Schéma_XXx :

L'astuce c'est de se placer entre le client et le serveur et de regarder les paquets qui sont échangés. Il y a plusieurs façons de faire ceci: un outil déjà existant, appelé AmaknaCore Sniffer, utilise la technique du Man In The Middle (MITM). Il suffit de se positionner entre le client et le serveur, et faire croire au client que nous sommes le serveur. "Bonjour ! Je suis le serveur Ankama ! Au fait, tu voulais me dire quelque chose :) ?". Le pauvre client, incapable de faire la différence entre le vrai serveur de l'imposteur nous délivre bien gentiment son message, sans résistance. Vous pouvez lire plus sur les attaques MITM sur Wikipedia. J'ai donc essayé cet outil, tout naturellement. Il suffit de lancer le jeu officiel Dofus, pendant que le sniffer tourne en arrière-plan. Le sniffer capture alors bien les paquets, mais je ne pouvais pas passer un certain stade de connexion. C'est peut-être dû à une nouvelle sécurité de la part d'Ankama, pour détecter les proxys ? J'ai donc dû tremper les mains dans le cambouis, et essayer d'autres techniques pour capturer nos précieux paquets. Le réflexe de tout bidouilleur qui se respecte est d'ouvrir Wireshark, qui permet d'aller capturer directement les paquets qui rentrent et sortent de notre carte réseau. Sympa, non ? Avec Wireshark, plus de bidouille de redirection de quoi que ce soit, on fait tout sans se faire remarquer !

Une recherche rapide sur Internet nous indique que le port qu'utilise Dofus pour discuter avec le serveur est le 5555. On peut alors facilement filtrer let traffic qui passe par notre carte réseau pour choper uniquement les paquets venant de Dofus.

Magnifique !

Première chose intéressante que l'on remarque sur Wireshark: le protocole qu'utilise Dofus pour communiquer est directement TCP, donc ça ne va pas être si simple que ça de décoder les paquets (Tiens, encore un imprévu ?!). J'ai sélectionné une trame TCP au hasard, et Wireshark nous affiche son contenu en hexadécimal en dessous. A droite, il tente de décoder en ASCII les données. Et on voit que pour une partie de notre paquet, ça marche ! Nous avons apparemment affaire à un paquet qui trimbale un message du chat: "Vent fragment de carte 120k" (par hyper chaud en orthographe moulasticot). Alors là, première réaction, ON EST CONTEN. Maintenant il va falloir apprendre à exploiter le reste du message, et là ça va se corser. Mais au moins on sait comment capturer nos paquets !

Décoder like a boss

Arrivé à ce point là, un petit coup de Google bien placé nous permet de tomber sur ce post de Bouh2 daté d'un peu moins d'une dizaine d'années, qui explique ses trouvailles sur le fonctionnement du protocole de Dofus. Héhé, et devinez quoi ? Bouh2, c'est le créateur de Stump, la boucle est bouclée ! On y apprend notamment la structure d'un paquet TCP Dofus, exactement ce que l'on cherchait. J'ai mis à nouveau à profit mes talents d'artiste-illustrateur pour vous produire ce petit schéma :

Chaque paquet commence par un header (en tête en anglais) qui contient des informations sur le message transporté. En tout premier, le packetId est écrit, c'est l'identifiant de notre paquet. Ensuite, on lit le LengthType, qui fait 2 bits. 2 bits permettent d'écrire 4 combinaisons différentes, 00 01 10 11. Cela va nous permettre de déterminer sur combien d'octets lire le prochain paramètre du header, Length. Par exemple, si LengthType vaut 3 (11 en binaire), on sait que le paramètre Length est codé sur 3 octets. Ce paramètre Length permet lui-même de connaître la taille du message qui va arriver.

Alors, chouettos, on peut maintenant comprendre un peu plus de choses sur nos paquets. On va mettre nos nouveaux talents à profit sur le message qu'on a intercepté avec Wireshark. Je vais réécrire en hexadécimal le début du contenu de celui-ci:

0d c5 4d 05 00 1f 76

Wireshark nous affiche les données en hexadécimal car c'est quand même plus court à lire que du binaire. Les chiffres hexadécimaux sont groupés deux à deux pour former un octet. On va donc commencer par lire les 14 premiers bits de notre paquet pour récupérer son identifiant: il faut lire les 4 premiers caractères hexadécimaux de notre paquet, les transformer en binaire pour obtenir 2 octets, ou 16 bits. Ensuite, on enlève les deux derniers bits et on a nos 14 premiers bits !

Ensuite, on retransforme ça en décimal (cimer les convertisseurs en ligne). On trouve un joli 881 ! On en a donc déduit que tous les messages de chat commencent par l'id 881. C'est bien gentil tout ça, mais ce qui nous intéresse surtout c'est de pouvoir décoder le message en lui-même ! En fait, l'id va nous permettre de savoir la structure de la partie Message du paquet. Car celle-ci change selon mon paquet: si j'envoie des informations de login au serveur, ma partie Message va contenir mon nom d'utilisateur et mon mot de passe, probablement. Si je déclenche un combat avec un monstre, le serveur va m'envoyer un paquet dont la partie Message va contenir des informations sur les PV de ce monstre.

On va donc devoir trouver une façon de relier cet Id au contenu de notre message. Pas easy hein ? Je me suis alors dit que tout ceci était forcément présent dans le code source de Dofus. Et Dofus étant fait en Flash ... Toute la programmation Flash est dans un langage de script, interprété. C'est à dire qu'on devrait pouvoir récuperer dans les sources du client Dofus le code réseau en noir sur blanc ! Evidemment, on ne peut pas faire la même chose pour le serveur car nous n'avons pas accès à ses fichiers, ça aurait été trop simple :p.

It's décompilation time

J'ai donc chopé le premier décompileur Flash que j'ai trouvé en ligne, JPEXS. En ouvrant le répertoire de jeu de Dofus, j'ai tout de suite repéré ma victime: DofusInvoker.swf. Et hop là, c'est ouvert !

Après m'être rincé trois fois les yeux à l'eau de Javel, j'ai vite remarqué le dossier scripts, qui contient toutes les sources du client Dofus. C'est incroyable. Je sais pas combien de milliers de fichiers y'a là dedans, mais y'en a un bon paquet. Quand tu te dis que tout ça est interprété, tu comprends pourquoi Dofus est si lent ! D'ailleurs, Ankama a un projet de port de Dofus sur Unity, mais vu le bordel que c'est je peux comprendre que ça prend du temps. On peut donc extraire ce dossier script du fichier .swf, et regarder tranquillement le contenu des sources dans un environnement un peu moins agressif que le bleu pétant de JPEXS. On remarque notamment au milieu de cette savane de fichiers MessageReceiver.as, qui contient une transcription de nos packetId en texte: on est sur la bonne voie !

_messagesTypes[5688] = EmotePlayErrorMessage;
_messagesTypes[801] = ChatSmileyMessage;
_messagesTypes[6730] = ChatCommunityChannelCommunityMessage;
_messagesTypes[6185] = LocalizedChatSmileyMessage;
_messagesTypes[6196] = MoodSmileyResultMessage;
_messagesTypes[6388] = MoodSmileyUpdateMessage;
_messagesTypes[6596] = ChatSmileyExtraPackListMessage;
_messagesTypes[880] = ChatAbstractServerMessage;
_messagesTypes[881] = ChatServerMessage;
_messagesTypes[6135] = ChatAdminServerMessage;
_messagesTypes[883] = ChatServerWithObjectMessage;
_messagesTypes[882] = ChatServerCopyMessage;
_messagesTypes[884] = ChatServerCopyWithObjectMessage;
_messagesTypes[870] = ChatErrorMessage;
_messagesTypes[892] = EnabledChannelsMessage;
_messagesTypes[891] = ChannelEnablingChangeMessage;
_messagesTypes[1200] = SpellListMessage;
_messagesTypes[5502] = LeaveDialogMessage;
_messagesTypes[6012] = PauseDialogMessage;
_messagesTypes[6384] = InteractiveUseErrorMessage;
_messagesTypes[5745] = InteractiveUsedMessage;
_messagesTypes[6112] = InteractiveUseEndedMessage;
_messagesTypes[5002] = InteractiveMapUpdateMessage;
_messagesTypes[5716] = StatedMapUpdateMessage;
_messagesTypes[5708] = InteractiveElementUpdatedMessage;
_messagesTypes[5709] = StatedElementUpdatedMessage;
Un extrait de MessageReceiver.as

Vous vous rappelez de notre id 881 ? Bim dans le mille, ça correspond à un `ChatServerMessage` !

En se baladant dans l'arborescence des scripts, on se rend compte que chaque message est implémenté dans sa propre classe. Voici donc le fichier ChatServerMessage.as:

 public class ChatServerMessage extends ChatAbstractServerMessage implements INetworkMessage
   {
      
      public static const protocolId:uint = 881;
      public var senderId:Number = 0;
      
      [Transient]
      public var senderName:String = "";
      public var prefix:String = "";
      public var senderAccountId:uint = 0;
 
      public function ChatServerMessage()
      {
         super();
      }
      
      override public function getMessageId() : uint
      {
         return 881;
      }
      
      public function initChatServerMessage(channel:uint = 0, content:String = "", timestamp:uint = 0, fingerprint:String = "", senderId:Number = 0, senderName:String = "", prefix:String = "", senderAccountId:uint = 0) : ChatServerMessage
      {
         super.initChatAbstractServerMessage(channel,content,timestamp,fingerprint);
         this.senderId = senderId;
         this.senderName = senderName;
         this.prefix = prefix;
         this.senderAccountId = senderAccountId;
         this._isInitialized = true;
         return this;
      }
      
      override public function reset() : void
      {
         super.reset();
         this.senderId = 0;
         this.senderName = "";
         this.prefix = "";
         this.senderAccountId = 0;
         this._isInitialized = false;
      }
      
      public function serializeAs_ChatServerMessage(output:ICustomDataOutput) : void
      {
         super.serializeAs_ChatAbstractServerMessage(output);
         if(this.senderId < -9007199254740990 || this.senderId > 9007199254740990)
         {
            throw new Error("Forbidden value (" + this.senderId + ") on element senderId.");
         }
         output.writeDouble(this.senderId);
         output.writeUTF(this.senderName);
         output.writeUTF(this.prefix);
         if(this.senderAccountId < 0)
         {
            throw new Error("Forbidden value (" + this.senderAccountId + ") on element senderAccountId.");
         }
         output.writeInt(this.senderAccountId);
      }
      
      public function deserializeAs_ChatServerMessage(input:ICustomDataInput) : void
      {
         super.deserialize(input);
         this._senderIdFunc(input);
         this._senderNameFunc(input);
         this._prefixFunc(input);
         this._senderAccountIdFunc(input);
      }
   }
Le code de ChatServerMessage.as, simplifié

Sérialisons

Il est important de noter les fonctions serialize et deserialize. Notre ami wikipedia nous dit:

La sérialisation est un procédé d'entrée-sortie permettant de sauvegarder et recharger l'état d'un objet.

Cela signifie que lorsque le client Dofus veut lire le contenu d'un paquet ChatServerMessage, il crée un objet ChatServerMessage et appelle deserialize_AsChatServerMessage, en passant en paramètre le contenu du paquet TCP. On peut ensuite accéder aux données stockées en accédant aux attributs de la classe. A l'inverse, pour sérialiser, c'est à dire transformer un objet de notre classe en paquet TCP, on va appeler la méthode serialize_AsChatServerMessage.

Essayons donc de voir à quoi ressemble la partie Message d'un paquet ChatServerMessage ! Il suffit de lire la fonction de sérialisation.

super.serializeAs_ChatAbstractServerMessage(output);

Histoire de simplifier encore les choses (lol) Les devs de chez Ankama ont décidé d'utiliser l'héritage. Notre classe ChatServerMessage hérite d'une classe ChatAbstractServerMessage. Cette ligne permet d'appeler son constructeur. Il faut donc se reporter dans le fichier ChatAbstractServerMessage.as pour lire comment sérialiser un ChatAbstractServerMessage.

  public function serializeAs_ChatAbstractServerMessage(output:ICustomDataOutput) : void
  {
     output.writeByte(this.channel);
     output.writeUTF(this.content);
     if(this.timestamp < 0)
     {
        throw new Error("Forbidden value (" + this.timestamp + ") on element timestamp.");
     }
     output.writeInt(this.timestamp);
     output.writeUTF(this.fingerprint);
  }
La fonction serialize de ChatAbstractServerMessage

On voit que le premier paramètre à lire est un nombre d'un octet représentant le canal de chat sur lequel le message est envoyé. Ensuite on lit du texte encodé en UTF-8. Comment savoir quel taille de texte lire, vous me direz ? Pour m'être tapé toutes les docs de Flash je peux vous répondre que les deux premiers octets représentent la taille à lire. On lit ensuite le timestamp, qui représente la date à lequel le message a été envoyé. Et enfin, on finit par le fingerprint, qui j'imagine doit être une façon d'identifier notre message. Ensuite on peut retourner dans notre ChatServerMessage  bien aimé et continuer la lecture de la fonction de sérialisation:

if(this.senderId < -9007199254740990 || this.senderId > 9007199254740990)
{
    throw new Error("Forbidden value (" + this.senderId + ") on element senderId.");
}
output.writeDouble(this.senderId);
output.writeUTF(this.senderName);
output.writeUTF(this.prefix);
if(this.senderAccountId < 0)
{
    throw new Error("Forbidden value (" + this.senderAccountId + ") on element senderAccountId.");
}
output.writeInt(this.senderAccountId);
La suite de notre fonction de sérialisation

En plus d'avoir des nouveaux paramètres, on a même des tests de validité. C'est pas beau tout ça ?

On a maintenant décodé notre premier paquet ! Voilà le petit récap que vous attendez tous, après avoir ingéré du code Flash dégueulasse sans broncher:

Structure d'un paquet TCP ChatServerMessage

Je peux donc, à la main, décoder mon paquet récupéré avec Wireshark pour en extraire toutes les informations ci-dessus ! On voit que la chaîne de caractère "Vent fragment de carte 120k" est stockée dans le paramètre content. Nouveau spoiler alert: on va pas se faire chier à décrypter chacun des paquets comme ça à la main, ça serait bien trop long ! Surtout que mon émulateur devra par la suite être capable par lui même de créer ces paquets pour les envoyer au client. On doit donc traduire ce code en TypeScript. Et on a pas mal de chance ! Certains d'entre vous ont remarqué que le langage de script Flash, ActionScript, est très proche de TypeScript, le langage que je vais utiliser dans mon émulateur ... Je peux donc récrire ChatServerMessage assez facilement:

class ChatAbstractServerMessage {

	public channel: number;
	public content: string;
	public timestamp: number;
	public fingerprint: string;

	constructor(channel: number = 0, content: string = '', timestamp: number = 0, fingerprint: string = '') {
		this.channel = channel;
		this.content = content;
		this.timestamp = timestamp;
		this.fingerprint = fingerprint;
	}
	serialize(output: CustomDataWrapper) {
		output.writeByte(this.channel);
		output.writeUTF(this.content);
		if(this.timestamp < 0)
		{
		throw new Error("Forbidden value (" + this.timestamp + ") on element timestamp.");
		}
		output.writeInt(this.timestamp);
		output.writeUTF(this.fingerprint);
	}
	deserialize(input: CustomDataWrapper) {
		this.channel = input.readByte();
		this.content = input.readUTF();
		this.timestamp = input.readInt();
		this.fingerprint = input.readUTF();
	}
	getMessageId() { return 880; }
}

export class ChatServerMessage extends ChatAbstractServerMessage {

	public senderId: number;
	public senderName: string;
	public prefix: string;
	public senderAccountId: number;

	constructor(channel: number = 0, content: string = '', timestamp: number = 0, fingerprint: string = '', senderId: number = 0, senderName: string = '', prefix: string = '', senderAccountId: number = 0) {
		super(channel, content, timestamp, fingerprint);
		this.senderId = senderId;
		this.senderName = senderName;
		this.prefix = prefix;
		this.senderAccountId = senderAccountId;
	}
	serialize(output: CustomDataWrapper) {
		super.serialize(output);
		if(this.senderId < -9007199254740990 || this.senderId > 9007199254740990)
		{
		throw new Error("Forbidden value (" + this.senderId + ") on element senderId.");
		}
		output.writeDouble(this.senderId);
		output.writeUTF(this.senderName);
		output.writeUTF(this.prefix);
		if(this.senderAccountId < 0)
		{
		throw new Error("Forbidden value (" + this.senderAccountId + ") on element senderAccountId.");
		}
		output.writeInt(this.senderAccountId);
	}
	deserialize(input: CustomDataWrapper) {
		super.deserialize(input);
		this.senderId = input.readDouble();
		this.senderName = input.readUTF();
		this.prefix = input.readUTF();
		this.senderAccountId = input.readInt();
	}
	getMessageId() { return 881; }
}
Notre classe ChatServerMessage, récrite

Recollons les morceaux

Maintenant, il faut qu'on implémente les objets ICustomDataInput et ICustomDataOutput. En fait, ce sont quasiment les mêmes, qui héritent de CustomDataWrapper. On peut donc écrire notre propre CustomDataWrapper, c'est suffisant ! On va juste se heurter à un écueuil (A L'AIDE J'EN PEUX PLUS): Il faut réimplémenter le type ByteArray qui n'a pas d'équivalent en NodeJS.

Ahaha nan je déconne on va juste récupérer un petit module node des familles:

Zaseth/bytearray-node
A Node.js implementation of the Actionscript 3 ByteArray supporting AMF0/AMF3. - Zaseth/bytearray-node

Merci Zaseth <3

Pfffiou ... Voilà. Au fait, je vous ai dit qu'il existe exactement 1077 classes comme celle-ci ?

Scrapping à la rescousse

When it's not possibeul to do more, laitsse the computeure do the weaurke, comme l'a dit un grand savant. On va se débrouiller pour refaire toutes les étapes d'au-dessus automatiquement, pour les 1077 classes ! On peut avoir deux approches:

  • Utiliser un analyseur syntaxique pour transformer le code ActionScript en Typescript (long, chiant et difficile)
  • Utiliser des regex comme un gros sagoin jusqu'à ce que ça marche (long chiant et difficile aussi mais bon)

Du coup en tant que bon programmeur j'ai utilisé la méthode 2  ̿̿ ̿̿ ̿̿ ̿'̿'\̵͇̿̿\з= ( ▀ ͜͞ʖ▀) =ε/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿

Normalement c'est quasi impossible de parser la syntaxe d'un langage de programmation avec des expressions régulières, mais comme dans notre cas la syntaxe des classes est suffisament simple, et qu'il n'y a pas trop de blocs impriqués, on va pouvoir se débrouiller. C'est parti pour réimplementer notre propre protocole !

Une vingtaine d'heures de souffrances plus tard, j'arrive à ça:

Comme ma santé mentale en a pris un coup, je vous fait un top 5 des pires expressions régulières que j'ai écrites:

  • /function serializeAs_[\s\S]+?\{([\s\S]*?)\}[^}]+?function/
  • /function deserializeByteBoxes[\s\S]+?\{([\s\S]*?)\}[^}]+?(?:function|}\r?\n}\r?\n$)/
  • /function deserializeAs_[\s\S]+?\{([\s\S]*?)\}[^}]+?function/
  • /public class[\s\S]*?\{([\s\S]*?)public function/
  • /public function init[^\(]+\(([^)]*)\)/

Voilà on en a fini pour le protocole ! On a réussi à recréer les 25000 lignes de classes utilisées par Dofus,  dans un fichier. On va maintenant utiliser les fonctions deserialize() générées afin de décoder l'intérieur des paquets Message.

Création d'un sniffer

Pour envoyer directement les paquets qu'on capte depuis Wireshark vers notre moulinette virtuelle de protocole, on va créer un sniffer. Celui-ci va choper les paquets de la même façon que Wireshark et nous afficher tout les données décryptées dans une interface aux petits oignons. On va même utiliser la même API que Wireshark utilise en interne, libpcap. Pour écrire l'interface graphique du sniffer j'ai choisi Electron pour pouvoir utiliser directement mon protocole en JavaScript. Electron permet de réaliser son interface utilisateur en HTML +CSS, ce qui est vraiment pratique pour faire un truc rapidement (pas faire un truc rapide hein, c'est pas pareil parce que la performance et Electron ça fait 3). J'obtiens finalement un truc de ce style :

Une première ébauche du sniffer

Ensuite il suffit d'importer les classes qu'on a générées et relier le tout ensemble. Comme on va avoir un peu de récursivité, car des objets peuvent contenir d'autres objets, j'ai utilisé Vue, un framework qui va nous faciliter le travail, en particulier la liaison entre les données et l'interface.

<div class="inspector">
      <p class="obj" v-if="depth !== 0">
        <i v-if="type !== '[]'" class="material-icons" @click="toggleChildren">{{ showChildren ? 'arrow_drop_down': 'arrow_right' }}</i>
        <span class="arrow-padding" v-else></span>
        {{ name }} 
        <span v-if="type" class="type">{{ type }}</span>
      </p>
      <div class="inspector-content" :style="'margin-left:' + depth*10 + 'px'" v-if="showChildren || depth === 0" v-for="(el, name) in object">
        <template v-if="typeof el === 'object'">
          <inspector 
            :name="name"
            :type="object['__' + name + 'Type']"
            :object="el"
            :depth="depth+1">
          </inspector>
        </template>
        <p class="primitive" v-else-if="typeof name !== 'string' || name.indexOf('__') === -1">
          {{ name }} :
          <span class="bool" v-if="typeof el === 'boolean'">{{ el }}</span>
          <span class="number" v-if="typeof el === 'number'">{{ el }}</span>
          <span class="string" v-if="typeof el === 'string'">"{{ el }}"</span>
        </p>
      </div>
    </div>
Un peu sale, mais ça fera l'affaire !

Ici, j'ai créé un composant appelé inspecteur, qui va contenir un objet Dofus. Si cet objet doit contenir d'autres objets, je peux créer un autre composant inspecteur dans mon premier inspecteur et ainsi de suite ! A la fin, ça me donne ça:

Le sniffer fini !

Et voilà ! Maintenant que notre notre Sniffer est fini, on va pouvoir écouter tous les paquets qui transitent entre le serveur et le client. Je peux maintenant m'atteler à la création de mon serveur privé, gnark gnark gnark ! D'autres posts sont à venir sur le sujet, j'ai déjà fait de nouvelles découverts croustillantes !

La deuxième partie être lue ici.