Aller au contenu

Décodage de protocole personnalisé

Vous avez capturé un protocole socket privé ou maison que les formats intégrés ne reconnaissent pas, et il ne vous reste qu’un tas d’octets ? Le décodage personnalisé vous permet d’apprendre à l’outil comment le lire à l’aide d’un court script : découper un flux d’octets continu en messages individuels, retirer l’en-tête du protocole, décompresser au besoin, et confier le reste à la reconnaissance structurée automatique de l’outil.

Cette page s’adresse à quiconque veut écrire son propre décodeur. Elle explique comment écrire la fonction, quand elle est appelée, et ce que signifie la valeur de retour.


1. De quoi il s’agit : un hook de décodage par trame

Section intitulée « 1. De quoi il s’agit : un hook de décodage par trame »

Vous écrivez un hook de décodage, et l’outil lui fournit les octets d’une connexion en posant sans cesse la même question : « À partir d’ici, peux-tu découper un message complet ? » Vous découpez un message et indiquez combien d’octets vous avez utilisés ; il pose de nouveau la question avec le reste, et ainsi de suite jusqu’à ce que le flux soit entièrement découpé.

Si vous avez déjà écrit un décodeur d’octets à accumulation dans un framework réseau, le modèle mental est le même : à partir d’un flux d’octets continu, vous décidez vous-même des frontières des messages. Deux conventions essentielles :

  • Vous ne vous souciez pas du sens : le flux d’envoi et le flux de réception d’une connexion passent chacun séparément par votre hook, et l’outil étiquette automatiquement chaque message découpé avec son sens. Votre script ne gère que « comment découper ».
  • Une fois découpé, vous ne gérez pas le rendu : chaque message que vous poussez repasse par la reconnaissance automatique. S’il contient du protobuf, du JSON ou du plist, cela est analysé plus loin sous forme structurée. Vous êtes seulement responsable du découpage en trames, du retrait des en-têtes et de la décompression.

Tout le décodeur tient dans une seule fonction :

function decode(buf, out) {
// buf: the byte stream currently pending parse; out: the output collector; return: the number of bytes consumed this time
}

Ce que sont les entrées, les sorties et la valeur de retour

Section intitulée « Ce que sont les entrées, les sorties et la valeur de retour »

buf (entrée) : le flux d’octets en attente d’analyse

  • Type : ArrayBuffer. Son contenu, ce sont tous les octets restants « depuis là où vous avez terminé votre dernière consommation, jusqu’à la fin actuelle ».
  • Propriété buf.byteLength : combien d’octets restent en ce moment (elle peut croître à chaque appel ; c’est ainsi que vous jugez s’il y en a assez pour découper un message).
  • ⚠️ Il s’agit d’un tampon d’octets brut, pas d’un Uint8Array, vous ne pouvez donc pas indexer les octets directement (buf[0] renvoie undefined). Pour lire des octets, utilisez les fonctions intégrées de la section 3 (u8 / u16be / sub …), ou enveloppez vous-même une vue : new Uint8Array(buf), new DataView(buf).

out (sortie) : le collecteur de sortie

  • Type : un tableau simple.
  • Méthode out.push(octets du message) : pousse un message ; vous pouvez en pousser plusieurs en un seul appel.
  • Les « octets du message » peuvent être : un ArrayBuffer renvoyé par une fonction intégrée (comme sub(...), gunzip(...)), ou une chaîne de caractères. Les autres types sont ignorés.

Valeur de retour : combien d’octets vous avez consommés

  • Type : entier (number), le nombre d’octets que vous avez utilisés depuis le début de buf.
  • > 0 : vous avez consommé ce nombre d’octets en tête et découpé un message → l’outil les écarte et vous rappelle avec le reste.
  • 0 / aucun return / nombre négatif : signifie « il n’y a pas encore de message complet en tête » ou « rien de plus à découper » → l’outil arrête la boucle et affiche les octets restants tels quels (bruts).
  • La valeur de retour est automatiquement bornée à la longueur du tampon (pour éviter les dépassements), mais veillez à renvoyer le nombre réel d’octets que vous avez effectivement utilisés.
  • L’outil effectue un passage sur le flux d’envoi d’une connexion et un sur son flux de réception. À chaque passage, decode est appelée en boucle : à chaque fois, elle vous remet les octets restants non consommés, vous découpez un message et renvoyez le nombre consommé, et ainsi de suite jusqu’à ce que vous renvoyiez 0.
  • Un message n’apparaît qu’une fois sa consommation confirmée : le message que vous poussez ne s’affiche que si vous renvoyez aussi une valeur > 0. Si vous poussez mais renvoyez 0, la boucle s’arrête et le push est écarté, donc pousser un message doit toujours aller de pair avec renvoyer le nombre d’octets qu’il a pris.
  • Une fois la boucle terminée, les octets de fin non consommés ne sont pas perdus ; ils sont affichés comme un unique bloc de données brutes (par exemple, la seconde moitié d’un message incomplet).
  • Chaque flux (sens) reçoit un environnement de script tout neuf et distinct ; le flux d’envoi et le flux de réception ne se mélangent jamais.
  • Pour conserver un état d’un appel à l’autre (un compteur, le type du message précédent, etc.), déclarez les variables en dehors de decode. Elles persistent d’un appel à l’autre au sein d’un même flux, et se réinitialisent lorsque le sens change ou que vous redécodez.
  • Le décodage s’exécute sur des données déjà capturées, et peut être relancé à volonté : modifiez le script, enregistrez, puis cliquez de nouveau pour redécoder la même connexion avec les nouvelles règles.
  • Si le script lève une exception ou qu’une exécution unique dépasse le temps imparti, cela est traité comme « rien consommé cette fois », les octets restants sont affichés comme données brutes, et la capture continue sans aucune perte de données.
  • Autrement dit, le pire qu’un script cassé puisse faire, c’est ne pas réussir à décoder, vous laissant des octets bruts ; il ne fera pas planter la connexion. Modifiez sans crainte.
  • Et les erreurs ne sont pas escamotées : les exceptions et les dépassements de délai s’affichent dans le panneau de sortie de débogage au-dessus des résultats (voir section 5), étiquetés selon qu’ils proviennent du flux d’envoi ou de réception . Suivez-les droit vers le correctif.

3. Fonctions intégrées (directement disponibles dans le script)

Section intitulée « 3. Fonctions intégrées (directement disponibles dans le script) »

Les tâches courantes, lire des octets, lire des entiers, convertir en texte et décompresser, sont toutes intégrées, vous n’avez donc pas à les réinventer :

Catégorie Signature Description
Extraire une sous-tranche sub(buf, off[, len]) Découpe à partir de off (omettez len pour aller jusqu’à la fin), renvoie un ArrayBuffer
Lire un entier u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Lit des entiers non signés en big-endian / little-endian
Convertir en texte hex(buf) / ascii(buf) Convertit en chaîne hexadécimale / lit comme texte
Détecter la compression gzipMagic(buf) S’il s’agit de gzip (renvoie un booléen)
Décompresser gunzip / inflate / unzstd / lz4dtx (buf) Décompresse ; en cas d’échec, renvoie l’entrée inchangée sans erreur
Déboguer log(...args) / console.log(...args) Affiche dans le panneau de débogage (voir section 5)

Les capacités standard de lecture/écriture d’octets (DataView, Uint8Array, etc.) sont également utilisables directement.


① Préfixe de longueur [longueur sur 4 octets big-endian][charge utile], la forme la plus courante des protocoles privés :

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // the length header hasn't fully arrived, wait
const total = 4 + u32be(buf, 0) // whole message = 4-byte header + payload
if (buf.byteLength < total) return 0 // the whole message hasn't fully arrived, wait
out.push(sub(buf, 4, total - 4)) // strip the header, hand the payload to auto recognition
return total // consume this message, go on to slice the next
}

② Par délimiteur / par ligne, en découpant les trames sur un marqueur tel qu’un retour à la ligne :

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // no newline yet, wait
out.push(sub(buf, 0, i)) // push this line (without the newline)
return i + 1 // consume the newline along with it
}

③ Traiter le tout d’un coup, en considérant l’ensemble du flux comme un seul message, par exemple pour tout décompresser :

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // if it's gzip, unpack it
return buf.byteLength // consume everything, the loop ends right away
}

④ Aiguiller par type, avec un champ de type dans l’en-tête et un traitement différent selon le type :

function decode(buf, out) {
if (buf.byteLength < 4) return 0
const total = 4 + u32be(buf, 0)
if (buf.byteLength < total) return 0
const type = u8(buf, 4) // the first byte is the message type
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // type 2 is compressed
return total
}

L’éditeur propose des modèles prêts à l’emploi pour tous ces schémas ; insérez-en un, ajustez quelques nombres, et ça tourne.

Éditeur de décodeur personnalisé : écrivez un script decode(buf, out) ; « Insérer un modèle » fournit des squelettes prêts à l’emploi pour préfixe de longueur / signature magique / délimiteur / longueur fixe / gzip / aiguillage par type, ainsi qu’un aide-mémoire des fonctions


  • Écrire et enregistrer : créez, nommez et enregistrez dans l’éditeur de décodeur ; insérez un modèle pour démarrer, avec les fonctions intégrées à portée de main dans un aide-mémoire.
  • Appliquer : pour une connexion que vous n’arrivez pas à comprendre, faites un clic droit et choisissez « Décoder comme » → votre décodeur, et tout le trafic d’envoi et de réception de la connexion est aussitôt découpé et affiché selon vos règles.
  • Déboguer et inspecter les variables : à l’intérieur de decode, utilisez log(...) ou console.log(...) pour afficher n’importe quelle valeur : compteurs d’octets, champs de type, hex(sub(buf, 0, 8)), etc. La sortie apparaît dans le panneau « Sortie de débogage » au-dessus des résultats de « Décoder comme », chaque ligne étiquetée selon qu’elle provient du flux d’envoi ou de réception . Les erreurs de script et les dépassements de délai s’affichent dans le même panneau, également étiquetés avec le sens. Affichez et localisez ainsi ; c’est bien plus rapide que d’éditer à l’aveugle.

    Le bac à sable ne dispose pas d’un console complet façon navigateur / Node ; seuls console.log et son équivalent log sont reliés à ce panneau. TextDecoder, fetch, setTimeout, et consorts ne sont pas disponibles. Pour lire des octets comme texte, utilisez ascii(buf).

  • Éditer et voir le résultat en direct : inutile de recapturer. Enregistrez votre script, cliquez de nouveau sur « Décoder comme », et la même connexion est redécodée sur-le-champ avec les nouvelles règles. Itérez jusqu’à ce que le découpage vous convienne.
  • Puis place à la structure : chaque bloc décodé est rendu à la reconnaissance automatique, de sorte que protobuf, JSON et plist sont analysés plus loin sous forme structurée, consultables via les multiples vues de Inspection et décodage des données.
  • Les décodeurs peuvent être nommés et conservés sous forme de liste, à ajouter, modifier ou supprimer à tout moment.

  • Lorsque vous capturez un protocole socket maison ou privé (une forme courante étant préfixe de longueur + protobuf / JSON / binaire) que les formats intégrés ne reconnaissent pas, écrivez un décodeur pour le découper et le restituer sous une structure lisible.
  • Lorsque vous voulez retransformer un bloc d’octets enveloppé dans un en-tête personnalisé, ou passé par un tour de compression, en contenu lisible.

Un aide-mémoire consolidé : le point d’entrée de décodage, les fonctions intégrées et l’environnement d’exécution. Chaque fonction qui prend buf accepte soit un ArrayBuffer, soit une chaîne de caractères.

decode(buf, out) → number
Objet Le seul point d’entrée du décodeur, doit être implémenté. Découpez des messages depuis le début de buf, push-ez-les dans out, et renvoyez le nombre d’octets consommés cette fois.
buf ArrayBuffer, le flux d’octets restant en attente d’analyse. Utilisez buf.byteLength pour sa longueur ; vous ne pouvez pas indexer les octets, utilisez les fonctions ci-dessous ou new DataView(buf) / new Uint8Array(buf).
out Array, le collecteur de sortie. out.push(bytes) pousse un message (bytes est un ArrayBuffer ou une chaîne ; les autres types sont ignorés) ; vous pouvez en pousser plusieurs d’un coup.
Retour number, les octets consommés depuis le début de buf. > 0 continue ; 0 / négatif / aucun retour → arrêt, et les octets restants sont affichés tels quels (bruts).
Convention d’appel Un passage sur le flux d’envoi et un sur le flux de réception de la connexion ; chaque passage appelle de façon répétée, en passant à chaque fois les octets restants non consommés, jusqu’à ce que vous renvoyiez 0.

Extraire une sous-tranche

Signature Renvoie Description
sub(buf, off) ArrayBuffer Découpe de off jusqu’à la fin
sub(buf, off, len) ArrayBuffer Découpe len octets à partir de off ; un off / len hors limites est automatiquement borné, sans erreur

Lire un entier (non signé ; renvoie 0 lorsque off est hors limites)

Signature Renvoie Description
u8(buf, off) number Lit 1 octet
u16be(buf, off) / u16le(buf, off) number Lit 2 octets, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Lit 4 octets, big-endian / little-endian

Convertir en texte

Signature Renvoie Description
hex(buf) string Convertit en hexadécimal minuscule
ascii(buf) string Lit comme texte (adapté au contenu ASCII / texte)

Compression / décompression (renvoie l’entrée inchangée en cas d’échec de décompression ; plafond d’environ 16 Mo par décompression unique)

Signature Renvoie Description
gzipMagic(buf) boolean S’il commence par le nombre magique gzip
gunzip(buf) ArrayBuffer Décompression gzip
inflate(buf) ArrayBuffer Décompression zlib / deflate
unzstd(buf) ArrayBuffer Décompression zstd
lz4dtx(buf) ArrayBuffer Décompression de flux LZ4 par blocs

Débogage

Signature Renvoie Description
log(...args) (aucun) Assemble les arguments en une ligne et l’affiche dans le panneau « Sortie de débogage » ; un ArrayBuffer est affiché en hexadécimal
console.log(...args) (aucun) Identique à log (pratique pour ceux qui ont l’habitude de console.log)
  • JavaScript standard : les objets intégrés courants tels que ArrayBuffer, les tableaux typés (Uint8Array, etc.), DataView, JSON, Math, RegExp, Date sont tous disponibles ; quand vous avez besoin d’une lecture/écriture d’octets bas niveau, new DataView(buf) / new Uint8Array(buf) sont à portée de main.
  • Débogage : log(...) / console.log(...) affichent dans le panneau « Sortie de débogage » ; les erreurs de script et les dépassements de délai y sont également affichés, étiquetés selon qu’ils proviennent du flux d’envoi ou de réception .
  • Ne sont pas fournies les autres capacités propres au navigateur / à Node : les méthodes de console autres que log, TextDecoder / TextEncoder, fetch, setTimeout, require, etc. sont toutes indisponibles. Pour lire des octets comme texte, utilisez ascii(buf).
  • Chaque appel à decode a un plafond de temps d’exécution : une boucle infinie ou un dépassement de délai est interrompu et traité comme « rien consommé », de sorte que l’outil ne se bloque pas.

Retour à Capture par proxy · À voir aussi : Inspection et décodage des données · Capture locale sur carte réseau