Comment analyser le protocole réseau MMORPG mobile

Au fil des années de jeu sur un MMORPG mobile, j'ai acquis une certaine expérience dans sa rétro-ingénierie, que je voudrais partager dans une série d'articles. Exemples de sujets:

  1. Analyser le format des messages entre le serveur et le client.
  2. Écrire une application d'écoute pour visualiser le trafic du jeu de manière pratique.
  3. Interception du trafic et sa modification à l'aide d'un serveur proxy non HTTP.
  4. Les premières étapes vers votre propre serveur ("piraté").

Dans cet article, je vais discuter de l' analyse du format de message entre le serveur et le client . Intéressé, je demande chat.

Outils requis


Pour pouvoir répéter les étapes décrites ci-dessous, vous aurez besoin de:

  • PC (je l'ai fait sur Windows 7/10, mais MacOS pourrait aussi fonctionner si les éléments ci-dessous sont disponibles là-bas);
  • Wireshark pour l'analyse des paquets;
  • 010Éditeur pour analyser les paquets par modèle (facultatif, mais vous permet de décrire rapidement et facilement le format du message);
  • l'appareil mobile lui-même avec le jeu.

De plus, il est très souhaitable d'avoir à portée de main des données lisibles du jeu, comme une liste d'objets, de créatures, etc. avec leurs identifiants. Cela simplifie considérablement la recherche de points clés dans les packages et parfois vous permet de filtrer le message souhaité dans un flux constant de données.

Analyse format de message entre serveur et client


Pour commencer, nous devons voir le trafic de l'appareil mobile. C'est assez simple à faire (même si j'ai pris cette décision évidente pendant très longtemps): sur notre PC, nous créons un point d'accès Wi-Fi, nous nous connectons à partir d'un appareil mobile, sélectionnons l'interface souhaitée dans Wireshark - et nous avons tout le trafic mobile devant nos yeux.

Après être entré dans le jeu et avoir attendu un certain temps afin que les requêtes qui ne sont pas liées au serveur de jeu lui-même soient arrêtées, vous pouvez observer l'image suivante:


À ce stade, nous pouvons déjà utiliser des filtres Wireshark pour voir uniquement les paquets entre le jeu et le serveur, ainsi qu'avec la charge utile uniquement:

tcp && tcp.payload && tcp.port == 44325 

Si vous vous tenez dans un endroit calme, loin des autres joueurs et du PNJ, et que vous ne faites rien, vous pouvez voir constamment répéter les messages du serveur et du client (taille 76 et 84 octets respectivement). Dans mon cas, le nombre minimum de packages différents a été envoyé sur l'écran de sélection des personnages.


La fréquence de la demande du client est très similaire au ping. Prenons quelques messages pour vérification (3 groupes, ci-dessus est une demande d'un client, ci-dessous est une réponse du serveur):


La première chose qui attire votre attention est l'identité des colis. Les 8 octets supplémentaires dans la réponse une fois convertis au système décimal sont très similaires à l'horodatage en secondes: 5CD008F8 16 = 1557137656 10 (de la première paire). Nous vérifions l'horloge - oui, ça l'est. Les 4 octets précédents correspondent aux 4 derniers octets de la demande. Lors de la traduction, nous obtenons: A4BB 16 = 42171 10 , ce qui est également très similaire au temps, mais en millisecondes. Cela coïncide à peu près avec le temps écoulé depuis le lancement du jeu, et c'est probablement le cas.

Il reste à considérer les 6 premiers octets de la demande et de la réponse. Il est facile de remarquer la dépendance de la valeur des quatre premiers octets du message (appelons ce paramètre L ) sur la taille du message: la réponse du serveur est supérieure à 8 octets, la valeur de L également augmenté de 8, cependant, la taille du paquet est supérieure de 6 octets à la valeur de L dans les deux cas. Vous pouvez également remarquer que les deux octets après L conservent leur valeur à la fois dans les requêtes du client et du serveur, et étant donné que leur valeur diffère de un, nous pouvons dire avec certitude qu'il s'agit du code de message C (les codes de message associés seront très probablement déterminés séquentiellement). La structure générale est suffisamment claire pour écrire un modèle minimal pour 010Editor:

  • 4 premiers octets - L - taille de la charge utile du message;
  • 2 octets suivants - C - code de message;
  • charge utile elle-même.

 struct Event { uint payload_length <bgcolor=0xFFFF00, name="Payload Length">; ushort event_code <bgcolor=0xFF9988, name="Event Code">; byte payload[payload_length] <name="Event Payload">; }; 

Par conséquent, le format du message ping client: envoyer l'heure ping locale; format de réponse du serveur: envoyez la même heure et l'heure d'envoi de la réponse en secondes. Cela ne semble pas difficile, non?

Essayons de rendre un exemple plus compliqué. Debout dans un endroit calme et cachant les paquets ping, vous pouvez trouver des messages se téléporter et créer un objet (artisanat). Commençons par le premier. Possédant les données du jeu, je savais quelle valeur du point de téléportation rechercher. Pour les tests, j'ai utilisé des points avec les valeurs 0x2B , 0x67 , 0x6B et 0x1AF . Comparez avec les valeurs dans les messages: 0x2B , 0x67 , 0x6B et 0x3AF :


Le désordre. Deux problèmes sont visibles:

  1. les valeurs ne sont pas 4 octets, mais de tailles différentes;
  2. toutes les valeurs ne correspondent pas aux données des fichiers, et dans ce cas, la différence est de 128.

De plus, lors de la comparaison avec le format ping, vous pouvez remarquer une différence:

  • incompréhensible 0x08 avant la valeur attendue;
  • Une valeur de 4 octets, 4 de moins que L (appelons-le D Ce champ n'apparaît pas dans tous les messages, ce qui est un peu étrange, mais là où il est, la dépendance L - 4 = D préservée. D'une part, pour les messages avec une structure simple (comme ping) ce n'est pas nécessaire, mais de l'autre - ça a l'air inutile).

Certains d'entre vous, je pense, auraient déjà pu deviner la raison de l'inadéquation des valeurs attendues, mais je vais continuer. Voyons ce qui se passe dans le métier:


Les valeurs attendues de 14183 et 14285 ne correspondent pas non plus aux valeurs réelles 28391 et 28621, mais la différence ici est déjà beaucoup plus grande que 128. Après de nombreux tests (y compris avec d'autres types de messages), il s'est avéré que plus le nombre attendu est élevé, plus la différence entre la valeur dans le paquet est grande. Ce qui était étrange, c'est que les valeurs jusqu'à 128 restaient seules. Compris, quoi de neuf? La situation évidente est pour ceux qui l'ont déjà rencontré, et, sans le savoir, j'ai dû démonter ce «code» pendant deux jours (au final, l'analyse des valeurs sous forme binaire a aidé au «piratage»). Le comportement décrit ci-dessus est appelé quantité de longueur variable - une représentation d'un nombre qui utilise un nombre indéfini d'octets, où le huitième bit d'un octet (bit de continuation) détermine la présence de l'octet suivant. D'après la description, il est évident que la lecture de VLQ n'est possible que dans l'ordre Little-Endian. Par coïncidence, toutes les valeurs dans les paquets sont dans cet ordre.

Maintenant que nous savons comment obtenir la valeur initiale, nous pouvons écrire un modèle pour le type:

 struct VLQ { local char size = 1; while(true) { byte obf_byte; if ((obf_byte & 0x80) == 0x80) { size++; } else { break; } } FSeek(FTell() - size); byte bytes[size]; local uint64 _ = FromVLQ(bytes, size); }; 

Et la fonction de conversion d'un tableau d'octets en une valeur entière:

 uint64 FromVLQ(byte bytes[], char size) { local uint64 source = 0; local int i = 0; local byte x; for (i = 0; i < size; i++) { x = bytes[i]; source |= (x & 0x7F) * Pow(2, i * 7); //   <<   , ..     ,  uint32,        uint64 if ((x & 0x80) != 0x80) { break; } } return source; }; 

Mais revenons à la création du sujet. D apparaît à nouveau et à nouveau 0x08 devant la valeur changeante. Les deux derniers octets du message 0x10 0x01 sont étrangement similaires au nombre d'éléments de fabrication, où 0x10 a un rôle similaire à 0x08 mais toujours incompréhensible. Mais maintenant, vous pouvez écrire un modèle pour cet événement:

 struct CraftEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker1; VLQ craft_id <bgcolor=0x00FF00, name="Craft ID">; byte marker2; VLQ quantity <bgcolor=0x00FF00, name="Craft Quantity">; }; 

Qui ressemblerait à ceci:


Et pourtant, c'étaient des exemples simples. Il sera plus difficile d'analyser l'événement du mouvement du personnage. Quelles informations attendons-nous? Au minimum, les coordonnées du personnage où il regarde, sa vitesse et son état (debout, courir, sauter, etc.). Puisqu'aucune ligne n'est visible dans le message, l'état est très probablement décrit par enum . En énumérant les options, en les comparant simultanément avec les données des fichiers de jeu, ainsi qu'à travers de nombreux tests, vous pouvez trouver trois vecteurs XYZ en utilisant ce modèle encombrant:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker; VLQ move_time <bgcolor=0x00FFFF>; FSkip(2); byte marker; float position_x <bgcolor=0x00FF00>; byte marker; float position_y <bgcolor=0x00FF00>; byte marker; float position_z <bgcolor=0x00FF00>; FSkip(2); byte marker; float direction_x <bgcolor=0x00FFFF>; byte marker; float direction_y <bgcolor=0x00FFFF>; byte marker; float direction_z <bgcolor=0x00FFFF>; FSkip(2); byte marker; float speed_x <bgcolor=0x00FFFF>; byte marker; float speed_y <bgcolor=0x00FFFF>; byte marker; float speed_z <bgcolor=0x00FFFF>; byte marker; VLQ character_state <bgcolor=0x00FF00>; }; 

Résultat visuel:


Les trois verts se sont avérés être les coordonnées de l'emplacement, les trois jaunes, très probablement, montrent où le personnage regarde et le vecteur de sa vitesse, et le dernier est l'état du personnage. Vous pouvez remarquer des octets constants (marqueurs) entre les valeurs de coordonnées ( 0x0D avant la valeur X , 0x015 avant Y et 0x1D avant Z ) et avant l'état ( 0x30 ), dont la signification est 0x08 similaire à 0x08 et 0x10 . Après avoir analysé de nombreux marqueurs d'autres événements, il s'est avéré qu'il détermine le type de valeur qui le suit (les trois premiers bits) et la signification sémantique, c'est-à-dire dans l'exemple ci-dessus, si vous échangez les vecteurs tout en conservant leurs marqueurs ( 0x120F devant les coordonnées, etc.), le jeu (théoriquement) devrait normalement analyser le message. Sur la base de ces informations, vous pouvez ajouter quelques nouveaux types:

 struct Packed { VLQ marker <bgcolor=0xFFBB00>; //    VLQ! local uint size = marker.size; //       ( , )          switch (marker._ & 0x7) { case 1: double v; size += 8; break; //     case 5: float v; size += 4; break; default: VLQ v; size += v.size; break; } }; struct PackedVector3 { Packed marker <name="Marker">; Packed x <name="X">; Packed y <name="Y">; Packed z <name="Z">; }; 

Maintenant, notre modèle de message de mouvement a été considérablement réduit:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; Packed move_time <bgcolor=0x00FFFF>; PackedVector3 position <bgcolor=0x00FF00>; PackedVector3 direction <bgcolor=0x00FF00>; PackedVector3 speed <bgcolor=0x00FF00>; Packed state <bgcolor=0x00FF00>; }; 

Un autre type dont nous pourrions avoir besoin dans l'article suivant est les lignes qui sont précédées de la valeur Packed de leur taille:

 struct PackedString { Packed length; char str[length.v._]; }; 

Maintenant, en connaissant l'exemple de format de message, vous pouvez écrire votre application d'écoute pour la commodité de filtrer et d'analyser les messages, mais c'est le sujet de l'article suivant.

Upd: merci aml pour l'allusion que la structure de message décrite ci-dessus est Protocol Buffer , et aussi Tatikoma pour un lien vers un article connexe utile.

Source: https://habr.com/ru/post/fr451512/


All Articles