Pular para o conteúdo

Decodificação de Protocolo Personalizado

Capturou um protocolo socket privado ou interno que os formatos embutidos não conseguem reconhecer, deixando você apenas com um monte de bytes? A decodificação personalizada permite que você ensine a ferramenta a lê-lo com um script curto: fatiar um fluxo contínuo de bytes em mensagens individuais, remover o cabeçalho do protocolo, descomprimir quando necessário e deixar o resto para o reconhecimento estruturado automático da ferramenta.

Esta página é para quem quer escrever um decodificador por conta própria. Ela explica como escrever a função, quando ela é chamada e o que o valor de retorno significa.


1. O que é: um gancho de decodificação por quadro

Seção intitulada “1. O que é: um gancho de decodificação por quadro”

Você escreve um gancho de decodificação, e a ferramenta alimenta-o com os bytes de uma conexão, perguntando repetidamente uma única coisa: “A partir daqui, você consegue fatiar uma mensagem completa?” Você fatia uma mensagem e informa quantos bytes usou; ela pergunta de novo com o restante, e assim por diante até que o fluxo esteja totalmente fatiado.

Se você já escreveu um decodificador de bytes acumulativo em algum framework de rede, o modelo mental é o mesmo: dado um fluxo contínuo de bytes, você mesmo decide os limites das mensagens. Duas convenções-chave:

  • Você não se preocupa com a direção: o fluxo de envio e o fluxo de recebimento de uma conexão passam separadamente pelo seu gancho, e a ferramenta rotula automaticamente cada mensagem fatiada com sua direção. Seu script só cuida de “como fatiar”.
  • Depois de fatiar, você não cuida da renderização: toda mensagem que você emite passa por reconhecimento automático novamente. Se ela contiver protobuf, JSON ou plist por dentro, isso é analisado mais a fundo em forma estruturada. Você é responsável apenas pelo enquadramento, pela remoção de cabeçalhos e pela descompressão.

2. A função de decodificação: decode(buf, out)

Seção intitulada “2. A função de decodificação: decode(buf, out)”

O decodificador inteiro é uma única função:

function decode(buf, out) {
// buf: o fluxo de bytes atualmente pendente de análise; out: o coletor de saída; return: o número de bytes consumidos desta vez
}

O que são as entradas, saídas e o valor de retorno

Seção intitulada “O que são as entradas, saídas e o valor de retorno”

buf (entrada): o fluxo de bytes pendente de análise

  • Tipo: ArrayBuffer. Seu conteúdo são todos os bytes restantes “de onde você terminou de consumir por último até o fim atual”.
  • Propriedade buf.byteLength: quantos bytes restam neste momento (pode crescer a cada chamada; é assim que você julga se há o suficiente para fatiar uma mensagem).
  • ⚠️ É um buffer de bytes bruto, não um Uint8Array, então você não consegue indexar bytes diretamente (buf[0] retorna undefined). Para ler bytes, use os auxiliares embutidos na seção 3 (u8 / u16be / sub …), ou construa você mesmo uma visão: new Uint8Array(buf), new DataView(buf).

out (saída): o coletor de saída

  • Tipo: um array comum.
  • Método out.push(bytes da mensagem): emite uma mensagem; você pode emitir várias em uma única chamada.
  • “Bytes da mensagem” podem ser: um ArrayBuffer retornado por um auxiliar embutido (como sub(...), gunzip(...)), ou uma string. Outros tipos são ignorados.

Valor de retorno: quantos bytes você consumiu

  • Tipo: inteiro (number), o número de bytes que você usou a partir do início de buf.
  • > 0: você consumiu esta quantidade de bytes do começo e fatiou uma mensagem → a ferramenta descarta-os e chama você de novo com o restante.
  • 0 / sem return / número negativo: significa “ainda não há uma mensagem completa no começo” ou “nada mais pode ser fatiado” → a ferramenta para o laço e exibe os bytes restantes como brutos.
  • O valor de retorno é automaticamente limitado ao comprimento do buffer (para evitar estouros), mas informe o número real de bytes que você de fato usou.
  • A ferramenta executa uma passada sobre o fluxo de envio de uma conexão e uma sobre seu fluxo de recebimento. Em cada passada, decode é chamada em laço: a cada vez, ela entrega os bytes restantes ainda não consumidos, você fatia uma mensagem e retorna o número consumido, e assim por diante até você retornar 0.
  • Uma mensagem só aparece quando seu consumo é confirmado: a mensagem que você emite só aparece quando você também retorna > 0. Se você emitir mas retornar 0, o laço para e a emissão é descartada, então emitir uma mensagem deve sempre ser acompanhado de retornar quantos bytes ela ocupou.
  • Depois que o laço termina, os bytes finais não consumidos não são perdidos; eles são mostrados como um único bloco de dados brutos (por exemplo, a segunda metade de uma mensagem incompleta).
  • Cada fluxo (direção) recebe um ambiente de script separado e totalmente novo; o fluxo de envio e o fluxo de recebimento nunca se misturam.
  • Para manter estado entre chamadas (um contador, o tipo da mensagem anterior e assim por diante), declare as variáveis fora de decode. Elas persistem entre chamadas dentro do mesmo fluxo, e são reiniciadas quando a direção muda ou quando você redecodifica.
  • A decodificação roda sobre dados já capturados e pode ser reexecutada repetidamente: edite o script, salve e clique de novo para redecodificar a mesma conexão com as novas regras.
  • Se o script lançar uma exceção ou uma única execução exceder o tempo, isso é tratado como “não consumiu nada desta vez”, os bytes restantes são mostrados como dados brutos, e a captura continua rodando sem perda de dados.
  • Em outras palavras, o pior que um script defeituoso pode fazer é falhar em decodificar, deixando você com bytes brutos; ele não vai derrubar a conexão. Edite à vontade.
  • E os erros não são engolidos: exceções e estouros de tempo são mostrados no painel de saída de depuração acima dos resultados (veja a seção 5), rotulados conforme vieram do fluxo de envio ou de recebimento . Siga-os direto até a correção.

3. Auxiliares embutidos (disponíveis diretamente no script)

Seção intitulada “3. Auxiliares embutidos (disponíveis diretamente no script)”

As tarefas comuns, ler bytes, ler inteiros, converter para texto e descomprimir, são todas embutidas, para que você não precise reinventá-las:

Categoria Assinatura Descrição
Extrair subfatia sub(buf, off[, len]) Fatia a partir de off (omita len para ir até o fim), retorna ArrayBuffer
Ler inteiro u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Lê inteiros sem sinal em big-endian / little-endian
Converter para texto hex(buf) / ascii(buf) Converte para uma string hexadecimal / lê como texto
Detectar compressão gzipMagic(buf) Se é gzip (retorna um booleano)
Descomprimir gunzip / inflate / unzstd / lz4dtx (buf) Descomprime; se não conseguir, retorna a entrada inalterada sem erro
Depuração log(...args) / console.log(...args) Imprime no painel de depuração (veja a seção 5)

As capacidades padrão de leitura/escrita de bytes (DataView, Uint8Array, etc.) também podem ser usadas diretamente.


① Prefixo de comprimento [comprimento big-endian de 4 bytes][payload], o formato de protocolo privado mais comum:

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // o cabeçalho de comprimento ainda não chegou por completo, aguardar
const total = 4 + u32be(buf, 0) // mensagem inteira = cabeçalho de 4 bytes + payload
if (buf.byteLength < total) return 0 // a mensagem inteira ainda não chegou por completo, aguardar
out.push(sub(buf, 4, total - 4)) // remove o cabeçalho, entrega o payload ao reconhecimento automático
return total // consome esta mensagem, segue para fatiar a próxima
}

② Por delimitador / por linha, fatiando quadros em um marcador como uma quebra de linha:

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // ainda sem quebra de linha, aguardar
out.push(sub(buf, 0, i)) // emite esta linha (sem a quebra de linha)
return i + 1 // consome a quebra de linha junto
}

③ Processar tudo de uma vez, tratando o fluxo inteiro como uma mensagem, por exemplo descomprimindo tudo:

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // se for gzip, descompacta
return buf.byteLength // consome tudo, o laço termina imediatamente
}

④ Despachar por tipo, com um campo de tipo no cabeçalho e tratamento diferente por 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) // o primeiro byte é o tipo da mensagem
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // o tipo 2 é comprimido
return total
}

O editor tem modelos prontos para todos esses padrões; insira um, ajuste alguns números e ele roda.

Editor de decodificador personalizado: escreva um script decode(buf, out); “Inserir modelo” fornece esqueletos prontos para prefixo de comprimento / assinatura mágica / delimitador / comprimento fixo / gzip / despacho por tipo, além de uma referência rápida de funções


  • Escreva e salve: crie, nomeie e salve no editor de decodificador; insira um modelo para começar, com as funções embutidas em uma referência rápida à mão.
  • Aplique: para uma conexão que você não consegue entender, clique com o botão direito e escolha “Decodificar como” → seu decodificador, e todo o tráfego de envio e recebimento da conexão é imediatamente fatiado e mostrado conforme suas regras.
  • Depure e inspecione variáveis: dentro de decode, use log(...) ou console.log(...) para imprimir qualquer valor: contagens de bytes, campos de tipo, hex(sub(buf, 0, 8)) e assim por diante. A saída aparece no painel “Saída de depuraçãoacima dos resultados de “Decodificar como”, cada linha rotulada conforme veio do fluxo de envio ou de recebimento . Erros e estouros de tempo do script aparecem no mesmo painel, também rotulados com a direção. Imprima e localize dessa forma; é muito mais rápido do que editar às cegas.

    O sandbox não tem um console completo de navegador / Node; apenas console.log e o equivalente log estão ligados a este painel. TextDecoder, fetch, setTimeout e afins não estão disponíveis. Para ler bytes como texto, use ascii(buf).

  • Edite e veja ao vivo: não é preciso recapturar. Salve seu script, clique em “Decodificar como” de novo, e a mesma conexão é redecodificada na hora com as novas regras. Itere até fatiar do jeito que você quer.
  • Rumo à estrutura: cada trecho decodificado é devolvido ao reconhecimento automático, então protobuf, JSON e plist são analisados mais a fundo em forma estruturada, visualizáveis pelas múltiplas visões em Inspecionar e decodificar dados.
  • Os decodificadores podem ser nomeados e mantidos como uma lista, para serem adicionados, editados ou removidos a qualquer momento.

  • Quando você captura um protocolo socket interno ou privado (um formato comum é prefixo de comprimento + protobuf / JSON / binário) que os formatos embutidos não conseguem reconhecer, escreva um decodificador para fatiá-lo e restaurá-lo a uma estrutura legível.
  • Quando você quer transformar um bloco de bytes envolto em um cabeçalho personalizado, ou submetido a uma rodada de compressão, de volta em conteúdo legível.

Uma referência rápida consolidada: o ponto de entrada de decodificação, as funções auxiliares embutidas e o ambiente de execução. Toda função que recebe buf aceita tanto um ArrayBuffer quanto uma string.

decode(buf, out) → number
Propósito O único ponto de entrada do decodificador, deve ser implementado. Fatie mensagens do começo de buf, push-e-as para out e retorne o número de bytes consumidos desta vez.
buf ArrayBuffer, o fluxo de bytes restante pendente de análise. Use buf.byteLength para seu comprimento; você não consegue indexar bytes, use os auxiliares abaixo ou new DataView(buf) / new Uint8Array(buf).
out Array, o coletor de saída. out.push(bytes) emite uma mensagem (bytes é um ArrayBuffer ou uma string; outros tipos são ignorados); você pode emitir várias de uma vez.
Retorno number, os bytes consumidos do começo de buf. > 0 continua; 0 / negativo / sem retorno → para, e os bytes restantes são mostrados como brutos.
Convenção de chamada Executa uma passada em cada um dos fluxos de envio e de recebimento da conexão; cada passada chama repetidamente, passando os bytes restantes não consumidos a cada vez, até você retornar 0.

Extrair subfatia

Assinatura Retorna Descrição
sub(buf, off) ArrayBuffer Fatia de off até o fim
sub(buf, off, len) ArrayBuffer Fatia len bytes a partir de off; um off / len fora do intervalo é automaticamente limitado, sem erro

Ler inteiro (sem sinal; retorna 0 quando off está fora do intervalo)

Assinatura Retorna Descrição
u8(buf, off) number Lê 1 byte
u16be(buf, off) / u16le(buf, off) number Lê 2 bytes, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Lê 4 bytes, big-endian / little-endian

Converter para texto

Assinatura Retorna Descrição
hex(buf) string Converte para hexadecimal minúsculo
ascii(buf) string Lê como texto (adequado para conteúdo ASCII / texto)

Compressão / descompressão (retorna a entrada inalterada quando não consegue descomprimir; limite por descompressão em torno de 16 MB)

Assinatura Retorna Descrição
gzipMagic(buf) boolean Se começa com o número mágico gzip
gunzip(buf) ArrayBuffer Descompressão gzip
inflate(buf) ArrayBuffer Descompressão zlib / deflate
unzstd(buf) ArrayBuffer Descompressão zstd
lz4dtx(buf) ArrayBuffer Descompressão de fluxo LZ4 por bloco

Depuração

Assinatura Retorna Descrição
log(...args) (nenhum) Junta os argumentos em uma linha e imprime no painel “Saída de depuração”; um ArrayBuffer é mostrado em hexadecimal
console.log(...args) (nenhum) Igual a log (conveniente para quem está acostumado com console.log)
  • JavaScript padrão: objetos embutidos comuns como ArrayBuffer, arrays tipados (Uint8Array, etc.), DataView, JSON, Math, RegExp, Date estão todos disponíveis; quando você precisar de leitura/escrita de bytes de mais baixo nível, new DataView(buf) / new Uint8Array(buf) estão à mão.
  • Depuração: log(...) / console.log(...) imprimem no painel “Saída de depuração”; erros e estouros de tempo do script também são impressos lá, rotulados conforme vieram do fluxo de envio ou de recebimento .
  • Não são fornecidas as demais capacidades específicas de navegador / Node: métodos de console além de log, TextDecoder / TextEncoder, fetch, setTimeout, require, etc. estão todos indisponíveis. Para ler bytes como texto, use ascii(buf).
  • Cada chamada de decode tem um limite de tempo de execução: um laço infinito ou um estouro de tempo é interrompido e tratado como “não consumiu nada”, então não trava a ferramenta.

Voltar a Captura por proxy · Relacionado: Inspecionar e decodificar dados · Captura local na placa de rede