Netcode 1.60

De T4C Tech
Aller à la navigation Aller à la recherche

Généralités

Le protocole réseau utilisé par T4C est du type non-connecté. Ce sont des datagrammes UDP. Les données de ce datagramme comprennent un en-tête sur 16 bits (2 octets), suivis d'une signature sur 4 octets (sauf dans le cas d'un fragment de datagramme), d'une chaîne de données (le pak), éventuellement d'une clé de cryptage sur 4 octets (seulement en cas de double cryptage) ainsi que d'une somme de contrôle sur un octet. L'ensemble du datagramme est crypté ; mais un second cryptage, similaire au cryptage 1.50 est optionnellement appliqué sur les datagrammes en provenance du client.

Le cryptage de base est un OU exclusif avec la valeur 0x99 sur les 512 premiers octets du datagramme, puis avec la valeur 0x2F sur les suivants.

Le second cryptage, optionnel, s'applique lui au niveau du pak rassemblé, et est identique au cryptage basé sur le générateur de nombres pseudo-aléatoires de la version 1.50 défini ici. Seuls les paks en provenance du client sont susceptibles de porter ce second cryptage.

Le checksum (somme de contrôle) est donné au dernier octet de chaque datagramme et porte sur l'ensemble du datagramme moins celui-ci, après encryption.

Les datagrammes de taille supérieure à 1024 octets sont systématiquement fragmentés (c'est-à-dire, envoyés en plusieurs parties de sorte que chaque partie envoyée ne fasse pas plus de 1024 octets).

Schéma canonique d'un datagramme du protocole 1.60 (1.61, 1.62, 1.63) :

  • SANS encryption client optionnelle :
[HEADER][signature][DATA.........................][checksum]
[.............partie encryptée XOR...............]
  • AVEC encryption client optionnelle :
[HEADER][signature][DATA.........................][checksum][seed]
        [..........partie encryptée 1.50.........]
[................partie encryptée XOR............]

NOTE: dans le cas d'un pak fragmenté, la signature est présente dans le premier fragment uniquement.

NOTE: dans le cas d'un pak fragmenté, la clé optionnelle de double cryptage est présente dans le dernier fragment uniquement.

ATTENTION: toutes les valeurs de l'en-tête, la signature, et la clé de cryptage optionnelle de l'algorithme 1.50 sont en LITTLE ENDIAN (format de représentation des types de données Intel), mais les données du pak sont en BIG ENDIAN (format de représentation des types de données données Motorola). En lisant les données du pak sur un ordinateur de type x86, le programmeur doit penser à INVERSER l'ordre des octets. Les chaînes de caractères, étant considérées comme des tableaux d'octets, ne sont pas affectées.

Innovations en sécurité

Voici en bref les deux innovations de sécurité de ce protocole, qui sont censées le protéger contre l'imposture. Celles-ci seront détaillées plus loin dans notre article.

ID de datagrammes incrémentielles

Le client T4C 1.60 est pointilleux sur la qualité des datagrammes qu'il reçoit du serveur, ce qui est une bonne chose. On regrettera cependant que cette vérification se limite aux 16 bits d'en-tête et non au contenu. Les serveurs T4C 1.60 non protégés sont donc ainsi toujours vulnérables à toutes sortes d'attaques malicieuses.

Les ID des datagrammes de la version 1.60 qui sont reçues par le client sont, pour chaque client, strictement incrémentielles. Leur gamme va de :

  • 0 à 255 compris (00000000b à 11111111b) pour les paks fragmentés
  • 5760 à 8190 compris (1010010000000b 3 à 1111111111110b) pour les paks standard (non fragmentés).

Les paks fragmentés et non fragmentés tiennent le compte de leurs IDs sur deux listes distinctes.

On supposera que l'idée sous-jacente à cette modification fut la volonté de restriction des proxies client comme le fameux T4C Pak Process, bien connu de longue date des tricheurs. La surveillance de l'incrémentation des ID étant censée protéger le client de l'insertion de paks intrus au milieu d'un discours. On observera cependant qu'il est pourtant très simple de faire fonctionner TPP à nouveau sur ce type de serveurs en laissant TPP gérer lui-même l'incrémentation des ID des datagrammes en fonction de ceux qu'il reçoit du serveur et de ceux qu'il envoie. Cette protection, même si elle partait d'un bon sentiment, est donc malheureusement caduque avant d'avoir été efficace.

Il n'y a qu'un seul moyen de se protéger contre les pirates et les tricheurs, c'est d'utiliser une cryptographie forte.

Le challenge CRC16

Cette version du protocole implémente une vérification côté client de la validité du serveur contacté par l'envoi d'un datagramme normal portant une signature particulière (0x66600666), et à la place des données du pak une valeur particulière sur 4 octets (0x00666001) suivie de 6 octets de challenge, auquel le serveur est censé appliquer un CRC16-CCITT et renvoyer le résultat au client dans un datagramme identique.

  • Si ce datagramme est renvoyé mais est porteur d'un résultat erronné, le client déconnecte.
  • Si ce datagramme n'est pas renvoyé, le client crée volontairement une fuite de mémoire importante (leak) sur la machine sur laquelle il s'exécute, jusqu'à 50 tentatives, auquel cas le thread qui s'occupe de l'envoi des données au serveur est terminé de force par l'application, interrompant ainsi la communication entre le client et le serveur.

Le datagramme challenge ainsi que l'examen de son résultat sont gérés par le client par un thread dédié à cette fonction, temporisé par un timer aléatoire de l'ordre de cinq minutes.

Quelques remarques à ce propos :

  • Le fait de dédier un thread à cette tâche est une très mauvaise idée. En effet, il suffit que ce thread ne se lance pas, ou que son timer de rafraîchissement soit porté à une valeur très grande, pour faire tomber cette protection.
  • Il eût fallu, au minimum, séparer le code de la formulation de la requête et celui de l'analyse de la réponse : malheureusement, toute la sécurité de cette mesure tient en quelques centaines d'octets contigus et immédiatement lisibles.
  • L'utilisation de constantes immédiatement identifiables est une très mauvaise idée en sécurité, car elle facilite sa recherche et son identification à travers l'image de l'exécutable.
  • Pour que cette protection soit efficace, il eût en outre fallu implémenter un mécanisme robuste de décision de la validité de la réponse. Or ici, il s'agit d'un simple test : une comparaison d'une valeur entière avec les constantes 0 et 1. Il suffit alors simplement de modifier ces constantes pour que la protection soit caduque et que le client accepte comme légitimes n'importe quelle réponse qui lui sera envoyée.

(note personnelle de l'auteur de cette page : et enfin, on ne peut que sourire à la puérilité de l'emploi d'une telle valeur hexadécimale (666! \o/) dans une considération de sécurité. Autant en avertir les reverse-engineerers dans le changelog, ça fera le même effet.)

Méthode de cryptage

Pour cette version, l'auteur du client T4C est revenu à une cryptographie plus simple. On supposera qu'il s'est rendu compte que la technique d'encodage à la volée à base de rand() utilisée dans la version précédente était consommatrice d'un temps processeur non négligeable et non nécessaire.

Le reverse-engineering de l'algorithme 1.60 à la portée des novices

Le reverse-engineering d'un tel algorithme est très simple. Il n'y a même pas besoin de désassembler quoi que ce soit.

Examinons un datagramme T4C grâce à un reniflard réseau tel qu'EtherSnoop.

Il s'agit d'un pak PAK_SERVER_MessageOfTheDay, le message du jour, enregistré sur un serveur officiel en version 1.63, le serveur Abomination. Ce message du jour est connu, il s'agit d'un texte faisant 2178 octets de long. Il s'agira donc d'un pak fragmenté ; ce datagramme en est le premier fragment.

Si nous ignorons

  • En bleu, l'en-tête Ethernet du datagramme
  • En jaune, l'en-tête IP du datagramme
  • En rouge, l'en-tête UDP du datagramme

Nous aboutissons aux données en vert, qui sont nos données utiles.

Visuellement tout d'abord, nous remarquons une très forte polarisation des valeurs des octets (point numéro 4). Tout ce qui se situe avant l'octet 512 possède une valeur haute, tout ce qui se situe après l'octet 512 possède une valeur basse. Témoin la représentation ASCII du datagramme juste à côté. C'est notre premier constat.

Nous en concluons que l'opération d'encryption sur ces données a été significativement différente après l'octet 512 du datagramme. En constatant visuellement d'autres datagrammes du même type, nous obtenons confirmation de cette théorie.

Analysons maintenant ces données de plus près. Nous sommes censés y trouver un texte, et ce texte nous est connu. Commençons tout d'abord pour voir si à chaque valeur d'octet correspond une lettre : il s'agit du codage le plus simple, le XOR avec une valeur unique. Prenons par exemple la lettre "=", qui est utilisée dans le message du jour pour créer une double ligne horizontale de 44 caractères de long.

ABOMINATION T4C SERVER - LAST NEWS

============================================

PERFORMANCES WARNING

Assez rapidement nous trouvons dans le datagramme notre suite de 44 octets identiques : à l'offset 0x0048, la valeur 0xA4 (vous pouvez le voir sur les lignes 4 et 5). Il s'ensuit donc que le codage de ce datagramme est un simple XOR à valeur unique.

Il nous reste à trouver cette valeur. C'est tout simple : la valeur ASCII du caractère "=" étant 0x3D, un XOR de 0x3D avec 0xA4 nous donne "0x99".

Les 512 premiers octets du datagramme sont donc masqués par un OU exclusif avec la valeur 0x99. Ceci se confirme car, en déconstruisant le pak à l'envers en fonction de ce qui nous est connu de manière à retrouver sa structure, au point n°3 nous trouvons la valeur 99 DB, qui une fois XORée nous donne 0x0042. Or ceci est la valeur de l'ID du pak PAK_SERVER_MessageOfTheDay.

Puisque nous savons maintenant déXORer les 512 premiers octets de notre datagramme, et que nous savons que la suite est masquée à l'aide d'une valeur différente, comme nous connaissons à partir du texte original du message du jour la première lettre masquée (il s'agit d'un espace, soit en valeur ASCII 0x20), il nous suffit de la masquer avec ce que nous lisons et nous aurons la seconde valeur.

0x0F XOR 0x20 = 0x2F

Nous savons donc décrypter maintenant l'intégralité du datagramme pour afficher le message du jour en clair. Le tout dernier octet, par contre ne correspond à rien de connu : il s'agit donc probablement soit d'un octet de padding, soit d'une somme de contrôle. Additionnons un à un tous les octets du datagramme jusqu'à celui-ci et, dépassement de capacité aidant, nous trouvons... 0x00. Il s'agit donc bien du checksum le plus simple qui soit : l'addition.

Nous savons donc maintenant comment s'articule notre premier datagramme 1.60 :

[2 octets de header][4 octets inconnus][données du pak][checksum]

Le tout encrypté par un brave XOR des familles.

Fonctions de masquage et de démasquage de premier niveau

Voici donc les fonctions de masquage et de démasquage de premier niveau de cet algorithme. Ces fonctions calculent le checksum à la volée, ce qui est la meilleure méthode plutôt que de le faire après coup et devoir parcourir toute la longueur du datagramme un deuxième fois.

  • Voici tout d'abord les constantes de masquage :
unsigned char DATAGRAM_DECRYPT_163_MASK = {0x99, 0x2F};
  • Démasquage :
bool Datagram_DeXOR_163 (datagram_t *datagram, int length)
{
   // this function decrypts a standard 1.60 datagram. It's a simple XOR. The 1-byte checksum is
   // computed after the encrypted bytes and a padding character is added at the end to make it
   // reach zero by overflow.

   unsigned char checksum;
   int index;

   // for each byte of data...
   checksum = 0;
   for (index = 0; index < length; index++)
   {
      checksum += datagram->bytes[index]; // compute checksum at the same time with the crypted data
      datagram->bytes[index] ^= DATAGRAM_DECRYPT_163_MASK[index / 512]; // mask it out
   }

   if (checksum != 0)
      return (false); // if checksum is NOT null, then something went wrong

   return (true); // checksum has been verified to be null, everything went fine
}
  • Masquage :
void Datagram_EnXOR_163 (datagram_t *datagram, int length)
{
   // this function encrypts a standard 1.60 datagram. It's a simple XOR. The 1-byte checksum is
   // computed after the encrypted bytes and a padding character is added at the end to make it
   // reach zero by overflow.

   unsigned char checksum;
   int index;

   // for each byte of the datagram...
   checksum = 0;
   for (index = 0; index < length; index++)
   {
      datagram->bytes[index] ^= DATAGRAM_DECRYPT_163_MASK[index / 512]; // mask it out
      checksum -= datagram->bytes[index]; // compute checksum at the same time with the crypted data
   }
   datagram->bytes[length] = checksum; // add a padding byte to ensure checksum reaches 0

   return; // finished
}

Anecdote

A ce stade de notre étude nous sommes en droit de nous demander pourquoi le programmeur en charge du client T4C a utilisé une méthode de cryptage aussi bête, avec deux valeurs de XOR seulement. La réponse nous est donnée en désassemblant le client T4C.

En cherchant nos deux constantes de masquage, nous tombons sur un tableau de constantes codé en dur, de 512 octets de long.

.data:006C22FC decrypt_xor_key db 99h, 2Fh, 99h, 0ACh, 4Bh, 71h, 0Ah, 83h, 8Bh, 0F1h
.data:006C22FC                 db 50h, 86h, 0C4h, 6Ah, 7Ch, 24h, 17h, 7Ah, 45h, 0C3h
.data:006C22FC                 db 77h, 0Bh, 0AAh, 0A8h, 0DAh, 0B2h, 9Ch, 0D5h, 55h, 18h
.data:006C22FC                 db 9Dh, 0F2h, 79h, 43h, 10h, 2Ah, 0FEh, 0F9h, 0DAh, 82h
.data:006C22FC                 db 2Eh, 0BFh, 1, 0D9h, 0ECh, 0E3h, 59h, 36h, 0F5h, 0Ch
.data:006C22FC                 db 0A7h, 0D1h, 2Eh, 12h, 0D7h, 28h, 0EEh, 8Bh, 3Ah, 6Ch
.data:006C22FC                 db 2Bh, 5Eh, 0Ch, 6Ch, 91h, 0Bh, 0B5h, 75h, 0B0h, 0E5h
.data:006C22FC                 db 36h, 3Ah, 1Ch, 76h, 0, 0B1h, 91h, 0DFh, 2 dup(2), 18h
.data:006C22FC                 db 40h, 68h, 5Eh, 2Bh, 1Ah, 42h, 0EDh, 0D7h, 0B4h, 26h
.data:006C22FC                 db 0B0h, 0E4h, 7Eh, 0D4h, 61h, 40h, 5Ah, 0A5h, 2Bh, 5Ch
.data:006C22FC                 db 70h, 88h, 1Ah, 0F6h, 8Fh, 17h, 0A7h, 10h, 8Bh, 46h
.data:006C22FC                 db 0F0h, 0F4h, 77h, 0DBh, 59h, 0ACh, 8Eh, 3Dh, 76h, 0D3h
.data:006C22FC                 db 9, 12h, 0FEh, 7Bh, 0E3h, 2Ch, 83h, 22h, 3Dh, 0BCh, 0C3h
.data:006C22FC                 db 0D3h, 0B2h, 6, 94h, 53h, 0C6h, 0B6h, 0C1h, 3Eh, 46h
.data:006C22FC                 db 21h, 0F0h, 29h, 0ACh, 47h, 8Bh, 0CDh, 85h, 0D0h, 99h
.data:006C22FC                 db 29h, 0D1h, 9Ah, 0ADh, 0A0h, 6Dh, 5Bh, 0C5h, 26h, 71h
.data:006C22FC                 db 49h, 2Dh, 5Bh, 96h, 0BBh, 67h, 0B7h, 9Eh, 0D1h, 9, 0ABh
.data:006C22FC                 db 0B0h, 15h, 0A8h, 0C1h, 0C5h, 1Eh, 0B8h, 0C8h, 6, 36h
.data:006C22FC                 db 6Eh, 0F0h, 0Fh, 0, 0AAh, 0F3h, 0F7h, 16h, 0D4h, 39h
.data:006C22FC                 db 77h, 0B2h, 0C8h, 0F5h, 83h, 53h, 0B2h, 58h, 18h, 5Ah
.data:006C22FC                 db 0F5h, 0A1h, 0BBh, 0B6h, 14h, 54h, 0E0h, 83h, 8Ch, 52h
.data:006C22FC                 db 4Eh, 0B1h, 0C2h, 0FEh, 0Ch, 0A5h, 0E1h, 6Ah, 1, 0DEh
.data:006C22FC                 db 0BFh, 6Fh, 0EDh, 0Dh, 0C1h, 0D9h, 35h, 82h, 0C2h, 88h
.data:006C22FC                 db 10h, 35h, 0F3h, 95h, 61h, 9, 0C0h, 75h, 19h, 9Dh, 0B5h
.data:006C22FC                 db 1Dh, 98h, 87h, 63h, 28h, 17h, 0EFh, 40h, 0F8h, 5, 3Ch
.data:006C22FC                 db 90h, 91h, 0Bh, 0B5h, 75h, 0B0h, 0E5h, 36h, 3Ah, 1Ch
.data:006C22FC                 db 76h, 0, 0B1h, 91h, 0DFh, 2 dup(2), 18h, 40h, 68h, 5Eh
.data:006C22FC                 db 2Bh, 1Ah, 42h, 0EDh, 0D7h, 0B4h, 26h, 0B0h, 0E4h, 7Eh
.data:006C22FC                 db 0D4h, 61h, 40h, 5Ah, 0A5h, 2Bh, 5Ch, 70h, 88h, 1Ah
.data:006C22FC                 db 0F6h, 8Fh, 17h, 0A7h, 10h, 8Bh, 46h, 0F0h, 0F4h, 77h
.data:006C22FC                 db 0DBh, 59h, 0ACh, 8Eh, 3Dh, 76h, 0D3h, 9, 12h, 0FEh
.data:006C22FC                 db 7Bh, 0E3h, 2Ch, 83h, 22h, 3Dh, 0BCh, 0C3h, 0D3h, 0B2h
.data:006C22FC                 db 6, 94h, 53h, 0C6h, 0B6h, 0C1h, 3Eh, 46h, 21h, 0F0h
.data:006C22FC                 db 29h, 0ACh, 47h, 8Bh, 0CDh, 85h, 0D0h, 99h, 29h, 0D1h
.data:006C22FC                 db 9Ah, 0ADh, 0A0h, 6Dh, 5Bh, 0C5h, 26h, 71h, 49h, 2Dh
.data:006C22FC                 db 5Bh, 96h, 0BBh, 67h, 0B7h, 9Eh, 0D1h, 9, 0ABh, 0B0h
.data:006C22FC                 db 15h, 0A8h, 0C1h, 0C5h, 1Eh, 0B8h, 0C8h, 6, 36h, 6Eh
.data:006C22FC                 db 0F0h, 0Fh, 0, 0AAh, 0F3h, 0F7h, 16h, 0D4h, 0C1h, 0C5h
.data:006C22FC                 db 1Eh, 0B8h, 0C8h, 6, 36h, 6Eh, 0F0h, 0Fh, 0, 0AAh, 0F3h
.data:006C22FC                 db 0F7h, 16h, 0D4h, 39h, 77h, 0B2h, 0C8h, 0F5h, 83h, 53h
.data:006C22FC                 db 0B2h, 58h, 18h, 5Ah, 0F5h, 0A1h, 0BBh, 0B6h, 14h, 54h
.data:006C22FC                 db 0E0h, 83h, 8Ch, 52h, 4Eh, 0B1h, 0C2h, 0FEh, 0Ch, 0A5h
.data:006C22FC                 db 0E1h, 6Ah, 1, 0DEh, 0BFh, 6Fh, 0EDh, 0Dh, 0C1h, 0D9h
.data:006C22FC                 db 35h, 82h, 0C2h, 88h, 10h, 35h, 0F3h, 95h, 61h, 9, 0C0h
.data:006C22FC                 db 0F5h, 0Ch, 0A7h, 0D1h, 2Eh, 12h, 0D7h, 28h, 0EEh, 8Bh
.data:006C22FC                 db 3Ah, 6Ch, 2Bh, 5Eh, 0Ch, 6Ch, 91h, 0Bh, 0B5h, 75h, 0B0h
.data:006C22FC                 db 0E5h, 36h, 3Ah, 1Ch, 76h, 0, 0B1h, 91h, 0DFh, 2 dup(2)
.data:006C22FC                 db 91h, 0Bh, 0B5h, 75h, 0B0h, 0E5h, 36h, 3Ah, 1Ch, 76h
.data:006C22FC                 db 0, 0B1h, 91h, 0DFh, 2 dup(2), 18h, 40h, 68h, 5Eh, 2Bh
.data:006C22FC                 db 1Ah, 42h, 0EDh, 0D7h, 0B4h, 26h, 0B0h, 0E4h, 7Eh, 0D4h
.data:006C22FC                 db 61h

Le fait que ce tableau fasse 512 octets de long est significatif. Tout se passe comme si le programmeur en charge du client T4C avait voulu utiliser toute la longueur de ce tableau pour masquer ses datagrammes, à l'instar de la méthode de cryptage utilisée pour les fichiers ELNG, mais qu'il avait confondu le code suivant :

      datagram->bytes[index] ^= DATAGRAM_DECRYPT_163_MASK[index % 512]; // mask it out

...code qui est effectivement censé masquer chaque octet du datagramme avec son correspondant dans la table de valeurs de masquage, jusqu'à 512 puis retour au début de la table, et :

      datagram->bytes[index] ^= DATAGRAM_DECRYPT_163_MASK[index / 512]; // mask it out

...code qui est présent dans les clients T4C 1.60 actuels, et qui, parce que les datagrammes ne dépassent jamais 1024 octets de long, ne peut indexer que les positions 0 et 1 de cette table.

Ce programmeur a tout simplement confondu division et modulo. 

C'est regrettable, car s'il avait testé ne serait-ce qu'une seule fois son code, et l'avait mis à l'épreuve, soit d'un debugger, soit d'un reniflard réseau, cette malheureuse erreur de débutant lui aurait sauté aux yeux tout de suite.

Nous ne lui en tiendrons pas rigueur. 

Structure de l'en-tête

En enregistrant les datagrammes échangés entre un client T4C 1.63 et son serveur, nous constatons tout d'abord que le header, les deux premiers octets, sont toujours la même séquence. C'est le signe qu'un calcul est appliqué sur au moins l'ID des datagrammes pour que les valeurs de ces en-têtes se succèdent selon une suite logique.

Examinons les valeurs de cette curieuse en-tête sur deux octets, tant pour les paks normaux que pour les paks fragmentés.

Pour un pak normal

9c 2d        soit en binaire        1011010000000-101
94 2d        soit en binaire        1011010000001-101
88 2d        soit en binaire        1011010000010-001
81 2d        soit en binaire        1011010000011-000

Nous constatons une incrémentation logique sur les 13 premiers bits, décalés à gauche de 3 bits. Si on ne stoppe pas l'enregistrement, cette incrémentation se poursuit inlassablement, jusqu'à la valeur maximale suivante :

6d 66        soit en binaire        1111111111110 100
99 2d        soit en binaire        1011010000000 000

Et on revient à 1011010000000b. Nous ignorons pourquoi la valeur 1111111111111b n'est pas prise en compte dans l'incrémentation ; certainement le programmeur a-t-il dû confondre les opérateurs > et >= lors de son test de dépassement. Peu importe.

Les 13 premiers bits de cette en-tête sont donc de toute évidence l'ID du datagramme, les 3 derniers étant selon toute logique des flags, dont nous verrons la signification plus tard.

Pour un pak fragmenté

La réception d'un pak fragmenté est simple à provoquer : le message du jour en est souvent un pour peu qu'il soit plus long qu'un kilo-octet, l'affichage de l'inventaire, l'affichage de la liste de sorts en provoquent à coup sûr.

Premier pak fragmenté (4 fragments) :
98 98        soit en binaire        00000001-00000-001
90 98        soit en binaire        00000001-00001-001
88 98        soit en binaire        00000001-00010-001
82 98        soit en binaire        00000001-00011-011

Second pak fragmenté (2 fragments) :
98 9b        soit en binaire        00000010-00000-001
92 9b        soit en binaire        00000010-00001-011

Ici, l'incrémentation n'est plus la même. L'en-tête est visiblement divisée non en deux parties, mais en trois.

  • Les premiers 8 bits semblent être l'ID du pak (et non celle du datagramme, ce qui est une curieuse conception d'attribuer une valeur sélective aux ID des paks)
  • Les 5 bits suivants semblent être le numéro du fragment,
  • Les 3 derniers bits sont donc, par déduction, les flags.

Déduisons-en plusieurs choses :

  • Le numéro du fragment est codé sur 5 bits, ce qui signifie qu'il ne peut pas y avoir plus de 2^5 = 32 fragments dans un pak fragmenté, soit 32 kilo-octets de données. Nous pouvons tout de suite parier qu'un message du jour de taille supérieure, par exemple, ne passerait pas. Quel serait le comportement de l'algorithme ? Peut-être un crash. A déterminer.
  • L'ID des paks fragmentés évolue donc de 1 à 255 inclus.
  • On remarque que systématiquement, quand un dernier fragment de pak fragmenté arrive, le deuxième bit en partant de la fin est à un. Nous pouvons d'ores et déjà en inférer que ce bit correspond au flag fin de pak fragmenté.

Déductions complémentaires

Il nous reste deux flags à identifier dans cet en-tête. Le premier est le mécanisme d'accusé de réception. C'est très simple, il suffit à l'aide d'un reniflard de relever quels sont les datagrammes qui provoquent en réponse le retour d'un mini-datagramme ne contenant quasiment rien (l'accusé de réception), et de trouver le flag qu'ils ont en commun. Il s'agit du premier bit en partant de la droite.

L'autre bit, lui, semble être positionné à 1 quand le décryptage du pak échappe à notre démasquage avec nos deux valeurs de XOR. C'est donc, selon toute logique, un signe indiquant qu'une cryptographie complémentaire est utilisée.

Le résultat est le suivant :

// T4C 1.60 pak header types (bitmapped)
#define PAKHEADER_163_TYPE_SECONDCRYPTO (unsigned char) (1 << 2)
#define PAKHEADER_163_TYPE_FRAGMENTTAIL (unsigned char) (1 << 1)
#define PAKHEADER_163_TYPE_ACKREQUEST (unsigned char) (1 << 0)

Les accusés de réception

Les accusés de réception, dans ce protocole, ne consistent qu'en un en-tête (2 octets), non masqué par le OU exclusif, et ne contenant que l'ID du datagramme concerné, tous les autres flags étant à zéro, comme nous pouvons le constater dans ce tableau.

En-tête du datagramme            | Accusé de réception
---------------------------------+---------------------------------
98 98 (00000001 00000 001)       | 00 01 (00000001 00000 000)
90 98 (00000001 00001 001)       | 08 01 (00000001 00001 000)
88 98 (00000001 00010 001)       | 10 01 (00000001 00010 000)
82 98 (00000001 00011 011)       | 18 01 (00000001 00011 000)

Cryptographie de second niveau

Certains datagrammes en provenance du client échappent à notre décryptage à base de XOR. Ils ont plusieurs points en commun.

  1. Le troisième bit en partant de la droite, dans leur en-tête, est à 1.
  2. Leur "signature", c'est-à-dire les quatre premiers octets qui suivent immédiatement l'en-tête, ne vaut pas 0.
  3. Leur taille est 4 octets plus longue que ce à quoi nous nous attendons.

Prenons par exemple le premier pak que le client est censé envoyer au serveur. Il s'agit du pak PAK_CLIENT_MessageOfTheDay dont voici la structure :

#define PAK_CLIENT_MessageOfTheDay 0x0042
// aucune donnée

On devrait donc, après encryption (et sachant qu'il s'agit du premier pak, donc le n°1), obtenir un datagramme comme ceci :

[HEADER  ][SIGNATURE  ][PAK     ][CHECKSUM]
[2 octets][4 octets   ][2 octets][1 octet ]
[9c 2d   ][99 99 99 99][99 db   ][6c      ]

Or, voici ce que l'on enregistre :

9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
  • Le header est bon. Identique, nous ne nous sommes pas trompés.
  • La signature diffère.
  • Les données sont plus longues, il y a 4 octets en trop.
  • Le checksum est correct, pour ce datagramme, si on ignore ces 4 octets en trop : 9c+2d+65+5e+79+e6+88+73+1a = 0.

Ces quatre octets ne font donc décidément pas partie des données du pak. Par contre, elles désignent probablement une clé de cryptage.

Démasquons tout d'abord les données légitimes avec le XOR habituel.

      65 5e 79 e6 88 73
 XOR (99 99 99 99 99 99)
      -----------------
      fc c7 e0 7f 11 ea

Si nous utilisons maintenant la clé de cryptage 29 a0 d0 4d qui suit ce datagramme avec l'algorithme de cryptographie précédent de Dialsoft, nous obtenons un datagramme raccourci de quatre octets :

00 00 00 00 00 42

Nous retrouvons bien notre signature, 4 octets à 00 00 00 00, puis notre type de pak, 00 42 soit PAK_CLIENT_MessageOfTheDay.

Méthodologie de décryptage globale

Considérons un datagramme brut lu sur le réseau. Le processus de décryptage d'un datagramme en 1.60, quel qu'il soit, sera donc le suivant.

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d

1. On masque en faisant un OU exclusif avec 0x9999 sur l'en-tête pour récupérer celle-ci

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
        |  |
        05 b4

2. On lit le header sous forme d'unsigned short (16 bits), en inversant les octets donc

        b405 = 1011010000000 101 = ID 5760 (pak normal), 2e crypto + accusé de réception demandé

3. On confirme la présence éventuelle de la cryptographie client supplémentaire en lisant la signature et en vérifiant qu'elle soit différente de 0 (soit 99 99 99 99 masqué), et de celle du pak de challenge (0x66600666 soit FF 9F F9 FF masqué)

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
              |  |  |  |
            ((65 5e 79 e6 != 99 99 99 99)
          && (65 5e 79 e6 != ff 9f f9 ff)) => encryption 1.50

4. On détermine alors sa structure en fonction de la présence ou non de la cryptographie 1.50

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
        [HEA] [SIGNATURE] [DAT] [] [SEED-1.50]

5. On vérifie le checksum sur toute la longueur des données brutes MOINS la clé optionnelle

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
        [HEA] [SIGNATURE] [DAT] []
        9c+2d+65+5e+79+e6+88+73+1a = 0

6. On démasque tout le reste avec 0x99 pour obtenir la signature et les données

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
              [SIGNATURE] [DAT]
              |  |  |  |  |  |
              fc c7 e0 7f 11 ea

7. OPTIONNEL: On déchiffre avec l'algorithme 1.50 en utilisant la clé donnée en fin de datagramme

        9c 2d 65 5e 79 e6 88 73 1a 29 a0 d0 4d
              |  |  |  |  |  | <-- [SEED-1.50]
              00 00 00 00 00 42

8. On récupère la signature et les données décryptées

              0x00000000 / 00 42 = pak normal / PAK_CLIENT_MessageOfTheDay

Fin de la procédure.

Le pak de challenge/réponse en CRC16

Toutes les cinq minutes environ, le client envoie sur le réseau un pak original, qui n'est pas dans la liste des paks connus, et n'a pas d'autre fonction que de s'assurer que le serveur sur lequel il se connecte est légitime. En fonction de la réponse de ce serveur, le comportement du client va varier : soit il continuera de fonctionner normalement, soit il créera une grosse fuite de mémoire volontaire, soit il coupera le jeu.

Ces paks ont tous un point commun : leur signature et les quatre premiers octets de leurs données sont identiques.

FF 9F F9 FF    98 F9 FF 99

Ce qui, une fois décrypté, nous donne les valeurs 0x66600666 et 0x00666001.

Ces deux valeurs au début d'un datagramme, juste après l'en-tête, signifient que vous avez affaire à un pak de challenge.

Requête du client au serveur

Voici les données brutes du challenge suivies des différentes étapes de leur décryptage.

        F9 2E FF 9F F9 FF 98 F9 FF 99 BA D0 F7 A8 BA D1 66
1.      60 b7
2.      b7 60 = 1011011101100 000
3.      [HEA] [SIGNATURE] [           DATA            ] []
4.      f9+2e+ff+9f+f9+ff+98+f9+ff+99+ba+d0+f7+a8+ba+d1+66 = 0
5.            66 06 60 66 01 60 66 00 23 49 6e 31 23 48
6.            [x66600666] [x00666001] 23 49 6e 31 23 48

On lit donc, outre la signature à 0x66600666 et le premier long à 0x00666001, une suite de six octets. Ce sont certainement les données du challenge, à partir desquelles il va falloir formuler une réponse.

Réponse du serveur au client

En enregistrant les datagrammes à l'aide de notre reniflard réseau, nous obtenons du serveur 1.63 (toujours Abomination) la séquence suivante.

        89 B8 FF 9F F9 FF 98 F9 FF 99 17 25 C4
1.      10 21
2.      21 10 = 0010000100010 000
3.      [HEA] [SIGNATURE] [     DATA      ] []
4.      89+b8+ff+9f+f9+ff+98+f9+ff+99+17+25+c4 = 0
5.            66 06 60 66 01 60 66 00 8e bc
6.            [x66600666] [x00666001] 8e bc

Nous voyons que le format du pak est rigoureusement identique, simplement les 6 octets de challenge sont remplacés par deux octets (16 bits) de réponse.

Comme il n'y a pas trente-six manières courantes de calculer une somme de contrôle sur 16 bits, et ayant eu dans ce bref exposé un petit aperçu des compétences des programmeurs Dialsoft, on se doute qu'ils n'ont pas été inventer leur propre algorithme.

L'algorithme de contrôle le plus connu est le contrôle de redondance cyclique ou CRC. Il se décline en de nombreuses variantes, principalement sur 16 et 32 bits. Les paramètres personnalisables de cet algorithme sont les suivants :

  • Le polynômial utilisé pour sélectionner les bits de contrôle (mais les polynômiaux les plus utilisés sont tous normalisés, et on les connaît)
  • La valeur de départ de la somme de contrôle (généralement 0 ou -1)
  • L'inversion ou non de la valeur de sortie

Or, si on prend l'algorithme de CRC-16 avec ses paramètres les plus connus, c'est-à-dire :

  • Polynômial CCITT (0xA001)
  • Valeur de départ à 0
  • Pas d'inversion en sortie

Autrement dit, la fonction suivante :

unsigned short CRC16_163 (unsigned char *data, int length)
{
   // this function computes the 16-bit CRC over an array of bytes pointed to by data

   unsigned short crc;
   unsigned char index;
   unsigned char bit;
   unsigned char parity;

   crc = 0; // start with all bits low

   // for each byte of data...
   for (index = 0; index < length; index++)
   {    
      crc ^= data[index]; // XOR CRC with byte at index

      // for each bit of CRC
      for (bit = 0; bit < 8; bit++)
      {
         parity = (unsigned char) crc; // remember its value for the parity check
         crc >>= 1; // right-shift the CRC
         if (parity % 2)
            crc ^= 0xa001; // if CRC was odd, XOR it with the CCITT polynomial (0xA001)
      }
   }

   return (crc); // and return the computed 16-bit CRC
}

Sur un challenge de 23 49 6e 31 23 48 on obtient un CRC de 0xbc8e.

C'est précisément la valeur de CRC qui est retournée dans la réponse du serveur.

Fin de l'analyse de la cryptographie 1.60.

--Sorkvild 23 mai 2008 à 22:10 (MSD)