Salta ai contenuti

Decodifica di protocolli personalizzati

Hai catturato un protocollo socket privato o interno che i formati integrati non riescono a riconoscere, lasciandoti con niente più di un mucchio di byte? La decodifica personalizzata ti permette di insegnare allo strumento come leggerlo con un breve script: suddividere un flusso di byte continuo in singoli messaggi, rimuovere l’intestazione del protocollo, decomprimere quando serve, e lasciare il resto al riconoscimento strutturato automatico dello strumento.

Questa pagina è per chi vuole scrivere un decodificatore da sé. Spiega come scrivere la funzione, quando viene chiamata e cosa significa il valore restituito.


Scrivi un hook di decodifica, e lo strumento gli passa i byte di una connessione, ponendo ripetutamente una sola domanda: “A partire da qui, riesci a estrarre un messaggio completo?” Tu estrai un messaggio e segnali quanti byte hai usato; lo strumento richiede di nuovo con il resto, e così via finché il flusso non è completamente suddiviso.

Se hai mai scritto un decodificatore di byte ad accumulo in un framework di rete, il modello mentale è lo stesso: dato un flusso di byte continuo, sei tu a decidere i confini dei messaggi. Due convenzioni chiave:

  • Non ti preoccupi della direzione: il flusso in uscita e il flusso in entrata di una connessione passano ciascuno separatamente attraverso il tuo hook, e lo strumento etichetta automaticamente ogni messaggio estratto con la sua direzione. Il tuo script si occupa solo di “come suddividere”.
  • Una volta suddiviso, non ti occupi della resa visiva: ogni messaggio che invii passa di nuovo attraverso il riconoscimento automatico. Se al suo interno contiene protobuf, JSON o plist, viene analizzato ulteriormente in forma strutturata. Tu sei responsabile solo del framing, della rimozione delle intestazioni e della decompressione.

L’intero decodificatore è un’unica funzione:

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

Cosa sono gli input, gli output e il valore restituito

Sezione intitolata “Cosa sono gli input, gli output e il valore restituito”

buf (input): il flusso di byte in attesa di analisi

  • Tipo: ArrayBuffer. Il suo contenuto è costituito da tutti i byte rimanenti “da dove hai terminato l’ultimo consumo, fino alla fine attuale”.
  • Proprietà buf.byteLength: quanti byte rimangono in questo momento (può crescere a ogni chiamata; è così che valuti se ce ne sono abbastanza per estrarre un messaggio).
  • ⚠️ È un buffer di byte grezzo, non un Uint8Array, quindi non puoi indicizzare i byte direttamente (buf[0] restituisce undefined). Per leggere i byte, usa le funzioni integrate della sezione 3 (u8 / u16be / sub …), oppure avvolgi tu stesso una vista: new Uint8Array(buf), new DataView(buf).

out (output): il raccoglitore dell’output

  • Tipo: un semplice array.
  • Metodo out.push(byte del messaggio): invia un messaggio; puoi inviarne diversi in una singola chiamata.
  • I “byte del messaggio” possono essere: un ArrayBuffer restituito da una funzione integrata (come sub(...), gunzip(...)), oppure una stringa. Gli altri tipi vengono ignorati.

Valore restituito: quanti byte hai consumato

  • Tipo: intero (number), il numero di byte che hai usato dall’inizio di buf.
  • > 0: hai consumato questa quantità di byte dall’inizio ed estratto un messaggio → lo strumento li scarta e ti richiama con il resto.
  • 0 / nessun return / numero negativo: significa “non c’è ancora un messaggio completo all’inizio” oppure “non si può estrarre altro” → lo strumento interrompe il ciclo e mostra i byte rimanenti come grezzi.
  • Il valore restituito viene automaticamente limitato alla lunghezza del buffer (per prevenire sforamenti), ma restituisci comunque il numero reale di byte che hai effettivamente usato.
  • Lo strumento esegue una passata sul flusso in uscita di una connessione e una sul suo flusso in entrata. In ogni passata, decode viene chiamata in un ciclo: ogni volta ti passa i byte rimanenti non consumati, tu estrai un messaggio e restituisci il numero consumato, e così via finché non restituisci 0.
  • Un messaggio compare solo quando il suo consumo è confermato: il messaggio che invii compare solo quando restituisci anche > 0. Se invii ma restituisci 0, il ciclo si interrompe e l’invio viene scartato, quindi inviare un messaggio deve sempre essere abbinato al restituire quanti byte ha richiesto.
  • Al termine del ciclo, i byte finali non consumati non vanno persi; vengono mostrati come un unico blocco di dati grezzi (per esempio, la seconda metà di un messaggio incompleto).
  • Ogni flusso (direzione) riceve un ambiente di script separato e completamente nuovo; il flusso in uscita e il flusso in entrata non si mescolano mai tra loro.
  • Per mantenere lo stato tra una chiamata e l’altra (un contatore, il tipo del messaggio precedente, e così via), dichiara le variabili all’esterno di decode. Persistono tra le chiamate all’interno dello stesso flusso, e si azzerano quando la direzione cambia o quando ridecodifichi.
  • La decodifica opera su dati già catturati, e può essere rieseguita ripetutamente: modifica lo script, salva e fai clic di nuovo per ridecodificare la stessa connessione con le nuove regole.
  • Se lo script solleva un’eccezione o una singola esecuzione va in timeout, viene trattato come “questa volta non ho consumato nulla”, i byte rimanenti vengono mostrati come dati grezzi, e la cattura prosegue senza perdita di dati.
  • In altre parole, il peggio che uno script difettoso può fare è non riuscire a decodificare, lasciandoti con byte grezzi; non farà crashare la connessione. Modifica pure liberamente.
  • E gli errori non vengono nascosti: le eccezioni e i timeout vengono mostrati nel pannello di output di debug sopra i risultati (vedi sezione 5), etichettati a seconda che provengano dal flusso in uscita o in entrata . Seguili direttamente fino alla soluzione.

3. Funzioni integrate (disponibili direttamente nello script)

Sezione intitolata “3. Funzioni integrate (disponibili direttamente nello script)”

Le operazioni comuni, leggere byte, leggere interi, convertire in testo e decomprimere, sono tutte integrate, così non devi reinventarle:

Categoria Firma Descrizione
Estrai sotto-porzione sub(buf, off[, len]) Estrae a partire da off (ometti len per arrivare alla fine), restituisce ArrayBuffer
Leggi intero u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Legge interi senza segno in big-endian / little-endian
Converti in testo hex(buf) / ascii(buf) Converte in una stringa esadecimale / legge come testo
Rileva compressione gzipMagic(buf) Se è gzip (restituisce un valore booleano)
Decomprimi gunzip / inflate / unzstd / lz4dtx (buf) Decomprime; se non ci riesce, restituisce l’input invariato senza errori
Debug log(...args) / console.log(...args) Stampa nel pannello di debug (vedi sezione 5)

Anche le funzionalità standard di lettura/scrittura dei byte (DataView, Uint8Array, ecc.) sono utilizzabili direttamente.


① Prefisso di lunghezza [lunghezza a 4 byte big-endian][payload], la forma più comune dei protocolli privati:

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
}

② Per delimitatore / per riga, suddividendo i frame su un marcatore come un a capo:

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
}

③ Elabora tutto in una volta, trattando l’intero flusso come un unico messaggio, per esempio decomprimendolo tutto:

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
}

④ Smista per tipo, con un campo di tipo nell’intestazione e una gestione diversa per ciascun tipo:

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’editor ha modelli già pronti per tutti questi schemi; inseriscine uno, ritocca qualche numero, e funziona.

Editor del decodificatore personalizzato: scrivi uno script decode(buf, out); “Inserisci modello” fornisce scheletri già pronti per prefisso di lunghezza / firma magica / delimitatore / lunghezza fissa / gzip / smistamento per tipo, oltre a una guida rapida delle funzioni


  • Scrivi e salva: crea, assegna un nome e salva nell’editor del decodificatore; inserisci un modello per iniziare, con le funzioni integrate a portata di mano in una guida rapida.
  • Applica: per una connessione che non riesci a interpretare, fai clic con il tasto destro e scegli “Decodifica come” → il tuo decodificatore, e tutto il traffico in uscita e in entrata della connessione viene immediatamente suddiviso e mostrato secondo le tue regole.
  • Debug e ispezione delle variabili: all’interno di decode, usa log(...) o console.log(...) per stampare qualsiasi valore: conteggi di byte, campi di tipo, hex(sub(buf, 0, 8)), e così via. L’output compare nel pannello “Output di debugsopra i risultati di “Decodifica come”, ogni riga etichettata a seconda che provenga dal flusso in uscita o in entrata . Gli errori e i timeout dello script compaiono nello stesso pannello, anch’essi etichettati con la direzione. Stampa e individua i problemi così; è molto più veloce che modificare alla cieca.

    La sandbox non ha una console completa da browser / Node; solo console.log e l’equivalente log sono collegati a questo pannello. TextDecoder, fetch, setTimeout e simili non sono disponibili. Per leggere i byte come testo, usa ascii(buf).

  • Modifica e vedi il risultato dal vivo: non serve ricatturare. Salva il tuo script, fai di nuovo clic su “Decodifica come”, e la stessa connessione viene ridecodificata sul momento con le nuove regole. Itera finché non viene suddivisa come vuoi tu.
  • Passa alla struttura: ogni blocco decodificato viene restituito al riconoscimento automatico, così protobuf, JSON e plist vengono analizzati ulteriormente in forma strutturata, visualizzabili attraverso le varie viste in Ispezione e decodifica dei dati.
  • I decodificatori possono essere denominati e conservati in un elenco, per essere aggiunti, modificati o eliminati in qualsiasi momento.

  • Quando catturi un protocollo socket interno o privato (una forma comune è prefisso di lunghezza + protobuf / JSON / binario) che i formati integrati non riescono a riconoscere, scrivi un decodificatore per suddividerlo e riportarlo a una struttura leggibile.
  • Quando vuoi riportare a un contenuto leggibile un blocco di byte avvolto in un’intestazione personalizzata, o passato attraverso un ciclo di compressione.

Una guida rapida consolidata: il punto di ingresso decode, le funzioni integrate di supporto e l’ambiente di esecuzione. Ogni funzione che accetta buf accetta sia un ArrayBuffer sia una stringa.

decode(buf, out) → number
Scopo L’unico punto di ingresso del decodificatore, deve essere implementato. Estrae i messaggi dall’inizio di buf, li invia con push a out, e restituisce il numero di byte consumati questa volta.
buf ArrayBuffer, il flusso di byte rimanente in attesa di analisi. Usa buf.byteLength per la sua lunghezza; non puoi indicizzare i byte, usa le funzioni sottostanti oppure new DataView(buf) / new Uint8Array(buf).
out Array, il raccoglitore dell’output. out.push(bytes) invia un messaggio (bytes è un ArrayBuffer o una stringa; gli altri tipi vengono ignorati); puoi inviarne diversi in una volta.
Return number, i byte consumati dall’inizio di buf. > 0 continua; 0 / negativo / nessun return → interrompe, e i byte rimanenti vengono mostrati come grezzi.
Convenzione di chiamata Esegue una passata ciascuna sul flusso in uscita e sul flusso in entrata della connessione; ogni passata chiama ripetutamente, passando ogni volta i byte rimanenti non consumati, finché non restituisci 0.

Estrai sotto-porzione

Firma Restituisce Descrizione
sub(buf, off) ArrayBuffer Estrae da off fino alla fine
sub(buf, off, len) ArrayBuffer Estrae len byte a partire da off; un off / len fuori intervallo viene automaticamente limitato, senza errori

Leggi intero (senza segno; restituisce 0 quando off è fuori intervallo)

Firma Restituisce Descrizione
u8(buf, off) number Legge 1 byte
u16be(buf, off) / u16le(buf, off) number Legge 2 byte, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Legge 4 byte, big-endian / little-endian

Converti in testo

Firma Restituisce Descrizione
hex(buf) string Converte in esadecimale minuscolo
ascii(buf) string Legge come testo (adatto a contenuti ASCII / testuali)

Compressione / decompressione (restituisce l’input invariato quando non riesce a decomprimere; limite per singola decompressione intorno ai 16 MB)

Firma Restituisce Descrizione
gzipMagic(buf) boolean Se inizia con il numero magico di gzip
gunzip(buf) ArrayBuffer Decompressione gzip
inflate(buf) ArrayBuffer Decompressione zlib / deflate
unzstd(buf) ArrayBuffer Decompressione zstd
lz4dtx(buf) ArrayBuffer Decompressione di flusso LZ4 a blocchi

Debug

Firma Restituisce Descrizione
log(...args) (nessuno) Unisce gli argomenti in una riga e li stampa nel pannello “Output di debug”; un ArrayBuffer viene mostrato in esadecimale
console.log(...args) (nessuno) Come log (comodo per chi è abituato a console.log)
  • JavaScript standard: gli oggetti integrati comuni come ArrayBuffer, gli array tipizzati (Uint8Array, ecc.), DataView, JSON, Math, RegExp, Date sono tutti disponibili; quando ti serve la lettura/scrittura dei byte a più basso livello, new DataView(buf) / new Uint8Array(buf) sono a portata di mano.
  • Debug: log(...) / console.log(...) stampano nel pannello “Output di debug”; anche gli errori e i timeout dello script vengono stampati lì, etichettati a seconda che provengano dal flusso in uscita o in entrata .
  • Non fornite sono le altre funzionalità specifiche di browser / Node: i metodi di console diversi da log, TextDecoder / TextEncoder, fetch, setTimeout, require, ecc. non sono disponibili. Per leggere i byte come testo, usa ascii(buf).
  • Ogni chiamata a decode ha un limite di tempo di esecuzione: un ciclo infinito o un timeout viene interrotto e trattato come “non ha consumato nulla”, così non blocca lo strumento.

Torna a Cattura tramite proxy · Correlato: Ispezione e decodifica dei dati · Cattura locale dalla NIC