Ir al contenido

Decodificación de protocolos personalizados

¿Capturaste un protocolo socket privado o interno que los formatos integrados no pueden reconocer, dejándote con nada más que un montón de bytes? La decodificación personalizada te permite enseñar a la herramienta a leerlo con un script corto: dividir un flujo de bytes continuo en mensajes individuales, quitar la cabecera del protocolo, descomprimir cuando sea necesario, y dejar el resto al reconocimiento estructurado automático de la herramienta.

Esta página es para cualquiera que quiera escribir un decodificador por sí mismo. Explica cómo escribir la función, cuándo se llama y qué significa el valor de retorno.


1. Qué es: un hook de decodificación por trama

Sección titulada «1. Qué es: un hook de decodificación por trama»

Escribes un hook de decodificación, y la herramienta le pasa los bytes de una conexión, preguntando repetidamente una sola cosa: “A partir de aquí, ¿puedes extraer un mensaje completo?” Extraes un mensaje e informas cuántos bytes usaste; te pregunta de nuevo con el resto, y así sucesivamente hasta que el flujo se divide por completo.

Si alguna vez escribiste un decodificador de bytes acumulativo en un framework de red, el modelo mental es el mismo: dado un flujo de bytes continuo, tú mismo decides los límites de los mensajes. Dos convenciones clave:

  • No te preocupas por la dirección: el flujo de envío y el flujo de recepción de una conexión pasan cada uno por tu hook por separado, y la herramienta etiqueta automáticamente cada mensaje extraído con su dirección. Tu script solo se ocupa de “cómo dividir”.
  • Una vez dividido, no te ocupas de la representación: cada mensaje que expulsas pasa de nuevo por el reconocimiento automático. Si contiene protobuf, JSON o plist dentro, eso se analiza más a fondo en forma estructurada. Tú solo eres responsable del enmarcado, de quitar cabeceras y de descomprimir.

Todo el decodificador es una sola función:

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

Qué son las entradas, las salidas y el valor de retorno

Sección titulada «Qué son las entradas, las salidas y el valor de retorno»

buf (entrada): el flujo de bytes pendiente de analizar

  • Tipo: ArrayBuffer. Su contenido son todos los bytes restantes “desde donde terminaste de consumir por última vez, hasta el final actual”.
  • Propiedad buf.byteLength: cuántos bytes quedan ahora mismo (puede crecer con cada llamada; así es como juzgas si hay suficiente para extraer un mensaje).
  • ⚠️ Es un búfer de bytes en bruto, no un Uint8Array, así que no puedes indexar bytes directamente (buf[0] devuelve undefined). Para leer bytes, usa los ayudantes integrados de la sección 3 (u8 / u16be / sub …), o envuelve una vista tú mismo: new Uint8Array(buf), new DataView(buf).

out (salida): el colector de salida

  • Tipo: un array simple.
  • Método out.push(bytes del mensaje): expulsa un mensaje; puedes expulsar varios en una sola llamada.
  • Los “bytes del mensaje” pueden ser: un ArrayBuffer devuelto por un ayudante integrado (como sub(...), gunzip(...)), o una cadena. Otros tipos se ignoran.

Valor de retorno: cuántos bytes consumiste

  • Tipo: entero (number), el número de bytes que usaste desde el inicio de buf.
  • > 0: consumiste esta cantidad de bytes del frente y extrajiste un mensaje → la herramienta los descarta y te llama de nuevo con el resto.
  • 0 / sin return / número negativo: significa “aún no hay un mensaje completo en el frente” o “no se puede extraer nada más” → la herramienta detiene el bucle y muestra los bytes restantes en bruto.
  • El valor de retorno se limita automáticamente dentro de la longitud del búfer (para evitar desbordamientos), pero devuelve el número real de bytes que realmente usaste.
  • La herramienta ejecuta una pasada sobre el flujo de envío de una conexión y otra sobre su flujo de recepción. En cada pasada, decode se llama en bucle: cada vez, te pasa los bytes restantes sin consumir, tú extraes un mensaje y devuelves el número consumido, y así hasta que devuelves 0.
  • Un mensaje solo aparece una vez que su consumo se confirma: el mensaje que expulsas aparece solo cuando también devuelves > 0. Si expulsas pero devuelves 0, el bucle se detiene y la expulsión se descarta, así que expulsar un mensaje siempre debe ir acompañado de devolver cuántos bytes ocupó.
  • Después de que el bucle termina, los bytes finales sin consumir no se pierden; se muestran como una sola pieza de datos en bruto (por ejemplo, la segunda mitad de un mensaje incompleto).
  • Cada flujo (dirección) obtiene un entorno de script separado y totalmente nuevo; el flujo de envío y el flujo de recepción nunca se mezclan entre sí.
  • Para mantener el estado entre llamadas (un contador, el tipo del mensaje anterior, etc.), declara las variables fuera de decode. Persisten entre llamadas dentro del mismo flujo, y se reinician cuando la dirección cambia o vuelves a decodificar.
  • La decodificación se ejecuta sobre datos ya capturados, y puede volver a ejecutarse repetidamente: edita el script, guarda y haz clic de nuevo para volver a decodificar la misma conexión con las nuevas reglas.
  • Si el script lanza una excepción o una sola ejecución agota el tiempo, se trata como “no consumió nada esta vez”, los bytes restantes se muestran como datos en bruto, y la captura sigue ejecutándose sin perder datos.
  • En otras palabras, lo peor que puede hacer un script roto es fallar al decodificar, dejándote con bytes en bruto; no bloqueará la conexión. Edita con libertad.
  • Y los errores no se ocultan: las excepciones y los tiempos de espera agotados se muestran en el panel de salida de depuración situado encima de los resultados (consulta la sección 5), etiquetados con si provienen del flujo de envío o de recepción . Síguelos directamente hasta la solución.

3. Ayudantes integrados (disponibles directamente en el script)

Sección titulada «3. Ayudantes integrados (disponibles directamente en el script)»

Las tareas comunes, leer bytes, leer enteros, convertir a texto y descomprimir, están todas integradas, así que no tienes que reinventarlas:

Categoría Firma Descripción
Tomar subporción sub(buf, off[, len]) Divide desde off (omite len para llegar al final), devuelve ArrayBuffer
Leer entero u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Lee enteros sin signo en big-endian / little-endian
Convertir a texto hex(buf) / ascii(buf) Convierte a una cadena hexadecimal / lee como texto
Detectar compresión gzipMagic(buf) Si es gzip (devuelve un booleano)
Descomprimir gunzip / inflate / unzstd / lz4dtx (buf) Descomprime; si no puede, devuelve la entrada sin cambios sin dar error
Depurar log(...args) / console.log(...args) Imprime en el panel de depuración (consulta la sección 5)

Las capacidades estándar de lectura/escritura de bytes (DataView, Uint8Array, etc.) también son utilizables directamente.


① Prefijo de longitud [longitud big-endian de 4 bytes][carga útil], la forma de protocolo privado más común:

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
}

② Por delimitador / por línea, dividiendo tramas en un marcador como un salto de línea:

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
}

③ Procesar todo de una vez, tratando todo el flujo como un solo mensaje, por ejemplo descomprimiéndolo todo:

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
}

④ Despachar por tipo, con un campo de tipo en la cabecera y un manejo 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) // 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
}

El editor tiene plantillas listas para usar para todos estos patrones; inserta una, ajusta unos cuantos números y funciona.

Editor de decodificador personalizado: escribe un script decode(buf, out); “Insertar plantilla” ofrece esqueletos listos para usar de prefijo de longitud / firma mágica / delimitador / longitud fija / gzip / despacho por tipo, además de una referencia rápida de funciones


  • Escribe y guarda: crea, nombra y guarda en el editor de decodificadores; inserta una plantilla para empezar, con las funciones integradas en una referencia rápida a mano.
  • Aplica: para una conexión que no logras entender, haz clic derecho y elige “Decodificar como” → tu decodificador, y todo el tráfico de envío y recepción de la conexión se divide y muestra de inmediato según tus reglas.
  • Depura e inspecciona variables: dentro de decode, usa log(...) o console.log(...) para imprimir cualquier valor: recuentos de bytes, campos de tipo, hex(sub(buf, 0, 8)), etc. La salida aparece en el panel “Salida de depuraciónencima de los resultados de “Decodificar como”, cada línea etiquetada con si provino del flujo de envío o de recepción . Los errores de script y los tiempos de espera agotados aparecen en el mismo panel, también etiquetados con la dirección. Imprime y localiza así; es mucho más rápido que editar a ciegas.

    El sandbox no tiene un console completo de navegador / Node; solo console.log y el equivalente log están conectados a este panel. TextDecoder, fetch, setTimeout y similares no están disponibles. Para leer bytes como texto, usa ascii(buf).

  • Edita y velo en vivo: no hace falta volver a capturar. Guarda tu script, haz clic en “Decodificar como” de nuevo, y la misma conexión se vuelve a decodificar al instante con las nuevas reglas. Itera hasta que se divida como quieres.
  • A por la estructura: cada pieza decodificada se devuelve al reconocimiento automático, de modo que protobuf, JSON y plist se analizan más a fondo en forma estructurada, visualizables a través de las múltiples vistas de Inspeccionar y decodificar datos.
  • Los decodificadores pueden nombrarse y mantenerse como una lista, para añadirse, editarse o eliminarse en cualquier momento.

  • Cuando capturas un protocolo socket interno o privado (una forma común es prefijo de longitud + protobuf / JSON / binario) que los formatos integrados no pueden reconocer, escribe un decodificador para dividirlo y restaurarlo a una estructura legible.
  • Cuando quieres convertir un fragmento de bytes envuelto en una cabecera personalizada, o pasado por una ronda de compresión, de nuevo en contenido legible.

Una referencia rápida consolidada: el punto de entrada de decodificación, las funciones ayudantes integradas y el entorno de ejecución. Cada función que toma buf acepta ya sea un ArrayBuffer o una cadena.

decode(buf, out) → number
Propósito El único punto de entrada del decodificador, debe implementarse. Extrae mensajes del frente de buf, hazles push a out, y devuelve el número de bytes consumidos esta vez.
buf ArrayBuffer, el flujo de bytes restante pendiente de analizar. Usa buf.byteLength para su longitud; no puedes indexar bytes, usa los ayudantes de abajo o new DataView(buf) / new Uint8Array(buf).
out Array, el colector de salida. out.push(bytes) expulsa un mensaje (bytes es un ArrayBuffer o una cadena; otros tipos se ignoran); puedes expulsar varios de una vez.
Retorno number, los bytes consumidos del frente de buf. > 0 continúa; 0 / negativo / sin retorno → detener, y los bytes restantes se muestran en bruto.
Convención de llamada Ejecuta una pasada cada uno sobre el flujo de envío y el flujo de recepción de la conexión; cada pasada llama repetidamente, pasando los bytes restantes sin consumir cada vez, hasta que devuelves 0.

Tomar subporción

Firma Devuelve Descripción
sub(buf, off) ArrayBuffer Divide desde off hasta el final
sub(buf, off, len) ArrayBuffer Divide len bytes desde off; un off / len fuera de rango se limita automáticamente, sin error

Leer entero (sin signo; devuelve 0 cuando off está fuera de rango)

Firma Devuelve Descripción
u8(buf, off) number Lee 1 byte
u16be(buf, off) / u16le(buf, off) number Lee 2 bytes, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Lee 4 bytes, big-endian / little-endian

Convertir a texto

Firma Devuelve Descripción
hex(buf) string Convierte a hexadecimal en minúsculas
ascii(buf) string Lee como texto (adecuado para contenido ASCII / texto)

Compresión / descompresión (devuelve la entrada sin cambios cuando no puede descomprimir; límite de descompresión única en torno a 16 MB)

Firma Devuelve Descripción
gzipMagic(buf) boolean Si empieza con el número mágico de gzip
gunzip(buf) ArrayBuffer Descompresión gzip
inflate(buf) ArrayBuffer Descompresión zlib / deflate
unzstd(buf) ArrayBuffer Descompresión zstd
lz4dtx(buf) ArrayBuffer Descompresión de flujo LZ4 por bloques

Depurar

Firma Devuelve Descripción
log(...args) (ninguno) Une los argumentos en una línea e imprime en el panel “Salida de depuración”; un ArrayBuffer se muestra en hexadecimal
console.log(...args) (ninguno) Igual que log (conveniente para quienes están acostumbrados a console.log)
  • JavaScript estándar: objetos integrados comunes como ArrayBuffer, arrays tipados (Uint8Array, etc.), DataView, JSON, Math, RegExp, Date están todos disponibles; cuando necesitas lectura/escritura de bytes de más bajo nivel, new DataView(buf) / new Uint8Array(buf) están a mano.
  • Depurar: log(...) / console.log(...) imprimen en el panel “Salida de depuración”; los errores de script y los tiempos de espera agotados también se imprimen ahí, etiquetados con si provienen del flujo de envío o de recepción .
  • No se proporcionan las demás capacidades específicas de navegador / Node: los métodos de console distintos de log, TextDecoder / TextEncoder, fetch, setTimeout, require, etc. no están disponibles. Para leer bytes como texto, usa ascii(buf).
  • Cada llamada a decode tiene un límite de tiempo de ejecución: un bucle infinito o un tiempo de espera agotado se interrumpe y se trata como “no consumió nada”, así que no bloqueará la herramienta.

Volver a Captura con proxy · Relacionado: Inspeccionar y decodificar datos · Captura local por NIC