Перейти к содержимому

Декодирование пользовательских протоколов

Захватили приватный или внутренний сокет-протокол, который встроенные форматы не распознают, и остались с одной лишь кучей байтов? Пользовательское декодирование позволяет научить инструмент читать его коротким скриптом: нарежьте непрерывный байтовый поток на отдельные сообщения, снимите заголовок протокола, при необходимости распакуйте, а остальное оставьте автоматическому структурному распознаванию инструмента.

Эта страница для тех, кто хочет написать декодировщик самостоятельно. В ней объясняется, как писать функцию, когда она вызывается и что означает возвращаемое значение.


1. Что это такое: хук покадрового декодирования

Заголовок раздела «1. Что это такое: хук покадрового декодирования»

Вы пишете хук декодирования, а инструмент подаёт ему байты соединения, повторно задавая один и тот же вопрос: «Начиная отсюда, можешь нарезать одно полное сообщение?» Вы нарезаете сообщение и сообщаете, сколько байтов использовали; инструмент спрашивает снова с остатком, и так далее, пока поток не будет нарезан полностью.

Если вы когда-нибудь писали накопительный байтовый декодер в сетевом фреймворке, мысленная модель та же: имея непрерывный байтовый поток, вы сами решаете границы сообщений. Две ключевые договорённости:

  • Вам не нужно беспокоиться о направлении: поток отправки и поток приёма соединения каждый прогоняется через ваш хук отдельно, а инструмент автоматически помечает каждое нарезанное сообщение его направлением. Ваш скрипт занимается только тем, «как нарезать».
  • После нарезки вам не нужно заниматься отображением: каждое выпущенное вами сообщение снова проходит через автоматическое распознавание. Если внутри него protobuf, JSON или plist, это будет разобрано дальше в структурированную форму. Вы отвечаете только за разбиение на кадры, снятие заголовков и распаковку.

Весь декодировщик — это одна функция:

function decode(buf, out) {
// buf: байтовый поток, ожидающий разбора в данный момент; out: коллектор вывода; возврат: число потреблённых на этот раз байтов
}

Что представляют собой входные данные, выходные данные и возвращаемое значение

Заголовок раздела «Что представляют собой входные данные, выходные данные и возвращаемое значение»

buf (вход): байтовый поток, ожидающий разбора

  • Тип: ArrayBuffer. Его содержимое — это все оставшиеся байты «от места, где вы в прошлый раз закончили потребление, до текущего конца».
  • Свойство buf.byteLength: сколько байтов остаётся прямо сейчас (может расти с каждым вызовом; так вы определяете, достаточно ли данных, чтобы нарезать сообщение).
  • ⚠️ Это сырой байтовый буфер, а не Uint8Array, поэтому напрямую индексировать байты нельзя (buf[0] вернёт undefined). Чтобы читать байты, используйте встроенные помощники из раздела 3 (u8 / u16be / sub …) либо оберните представление сами: new Uint8Array(buf), new DataView(buf).

out (выход): коллектор вывода

  • Тип: обычный массив.
  • Метод out.push(байты сообщения): выпустить одно сообщение; за один вызов можно выпустить несколько.
  • «Байты сообщения» могут быть: ArrayBuffer, возвращённым встроенным помощником (например, sub(...), gunzip(...)), либо строкой. Другие типы игнорируются.

Возвращаемое значение: сколько байтов вы потребили

  • Тип: целое число (number) — число байтов, использованных вами от начала buf.
  • > 0: вы съели столько байтов спереди и нарезали сообщение → инструмент отбрасывает их и вызывает вас снова с остатком.
  • 0 / без return / отрицательное число: означает «полного сообщения впереди пока нет» или «больше ничего нарезать нельзя» → инструмент останавливает цикл и показывает оставшиеся байты в сыром виде.
  • Возвращаемое значение автоматически ограничивается длиной буфера (чтобы предотвратить выход за границы), но, пожалуйста, возвращайте истинное число реально использованных байтов.
  • Инструмент делает один проход по потоку отправки соединения и один по его потоку приёма. В каждом проходе decode вызывается в цикле: каждый раз ей передаются оставшиеся непотреблённые байты, вы нарезаете одно сообщение и возвращаете число потреблённых байтов, и так далее, пока не вернёте 0.
  • Сообщение появляется только после подтверждения его потребления: выпущенное вами сообщение появится только тогда, когда вы также вернёте > 0. Если вы выполнили push, но вернули 0, цикл останавливается, и push отбрасывается, поэтому выпуск сообщения всегда должен сопровождаться возвратом того, сколько байтов он занял.
  • После завершения цикла оставшиеся непотреблённые байты в конце не теряются; они показываются как один сырой фрагмент данных (например, вторая половина неполного сообщения).
  • Каждый поток (направление) получает отдельное, совершенно новое окружение скрипта; поток отправки и поток приёма никогда не пересекаются друг с другом.
  • Чтобы сохранять состояние между вызовами (счётчик, тип предыдущего сообщения и так далее), объявляйте переменные вне decode. Они сохраняются между вызовами в рамках одного и того же потока и сбрасываются при смене направления или при повторном декодировании.
  • Декодирование выполняется на уже захваченных данных и может запускаться повторно много раз: отредактируйте скрипт, сохраните и нажмите снова, чтобы заново декодировать то же соединение по новым правилам.
  • Если скрипт выбрасывает исключение или один запуск превышает таймаут, это трактуется как «на этот раз ничего не потреблено», оставшиеся байты показываются как сырые данные, а захват продолжает работать без потери данных.
  • Иными словами, худшее, что может сделать сломанный скрипт, — это не декодировать, оставив вам сырые байты; он не обрушит соединение. Редактируйте свободно.
  • И ошибки не проглатываются: исключения и таймауты показываются на панели отладочного вывода над результатами (см. раздел 5), с пометкой, пришли ли они из потока отправки или приёма . Идите по ним прямо к исправлению.

3. Встроенные помощники (доступны прямо в скрипте)

Заголовок раздела «3. Встроенные помощники (доступны прямо в скрипте)»

Типичные задачи — чтение байтов, чтение целых чисел, преобразование в текст и распаковка — все встроены, так что изобретать их заново не нужно:

Категория Сигнатура Описание
Взять подсрез sub(buf, off[, len]) Срез от off (без len — до конца), возвращает ArrayBuffer
Прочитать целое u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Прочитать целые числа без знака в big-endian / little-endian
Преобразовать в текст hex(buf) / ascii(buf) Преобразовать в hex-строку / прочитать как текст
Определить сжатие gzipMagic(buf) Является ли это gzip (возвращает булево значение)
Распаковать gunzip / inflate / unzstd / lz4dtx (buf) Распаковать; если не получилось, возвращает вход без изменений, без ошибки
Отладка log(...args) / console.log(...args) Вывод в панель отладки (см. раздел 5)

Стандартные возможности чтения/записи байтов (DataView, Uint8Array и т. д.) также доступны напрямую.


① Префикс длины [4-байтовая длина big-endian][полезная нагрузка], самая распространённая форма приватного протокола:

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // заголовок длины ещё не пришёл полностью, ждём
const total = 4 + u32be(buf, 0) // всё сообщение = 4-байтовый заголовок + полезная нагрузка
if (buf.byteLength < total) return 0 // всё сообщение ещё не пришло полностью, ждём
out.push(sub(buf, 4, total - 4)) // снять заголовок, передать полезную нагрузку автораспознаванию
return total // потребить это сообщение, идём нарезать следующее
}

② По разделителю / построчно, нарезка кадров по маркеру вроде переноса строки:

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // переноса строки ещё нет, ждём
out.push(sub(buf, 0, i)) // выпустить эту строку (без переноса)
return i + 1 // потребить перенос строки вместе с ней
}

③ Обработать всё целиком сразу, трактуя весь поток как одно сообщение, например распаковывая всё целиком:

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // если это gzip, распаковать
return buf.byteLength // потребить всё, цикл заканчивается сразу же
}

④ Разбор по типу, с полем типа в заголовке и разной обработкой для каждого типа:

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) // первый байт — это тип сообщения
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // тип 2 сжат
return total
}

В редакторе есть готовые шаблоны для всех этих паттернов; вставьте один, подправьте несколько чисел, и он заработает.

Редактор пользовательского декодировщика: напишите скрипт decode(buf, out); «Вставить шаблон» предоставляет готовые каркасы для префикса длины / магической сигнатуры / разделителя / фиксированной длины / gzip / разбора по типу, а также краткий справочник функций


  • Написать и сохранить: создайте, назовите и сохраните в редакторе декодировщика; вставьте шаблон, чтобы начать, со встроенными функциями в кратком справочнике под рукой.
  • Применить: для соединения, в котором не удаётся разобраться, кликните правой кнопкой и выберите «Декодировать как» → ваш декодировщик, и весь трафик отправки и приёма соединения тут же нарезается и показывается по вашим правилам.
  • Отладка и просмотр переменных: внутри decode используйте log(...) или console.log(...), чтобы вывести любое значение: число байтов, поля типа, hex(sub(buf, 0, 8)) и так далее. Вывод появляется на панели «Отладочный вывод» над результатами «Декодировать как», каждая строка помечена, пришла ли она из потока отправки или приёма . Ошибки скрипта и таймауты показываются на той же панели, тоже с пометкой направления. Так вы печатаете и находите проблему; это намного быстрее, чем редактировать вслепую.

    В песочнице нет полноценного браузерного / Node console; к этой панели подключены только console.log и эквивалентный log. TextDecoder, fetch, setTimeout и подобное недоступны. Чтобы прочитать байты как текст, используйте ascii(buf).

  • Редактировать и видеть результат сразу: повторный захват не нужен. Сохраните скрипт, снова нажмите «Декодировать как», и то же соединение тут же перекодируется по новым правилам. Итерируйте, пока не нарежется так, как нужно.
  • Дальше к структуре: каждый декодированный фрагмент возвращается автоматическому распознаванию, так что protobuf, JSON и plist разбираются дальше в структурированную форму, доступную для просмотра через несколько режимов в Просмотре и декодировании данных.
  • Декодировщики можно называть и хранить списком, добавлять, редактировать или удалять в любой момент.

  • Когда вы захватываете внутренний или приватный сокет-протокол (типичная форма — префикс длины + protobuf / JSON / бинарные данные), который встроенные форматы не распознают, напишите декодировщик, чтобы нарезать его и восстановить в читаемую структуру.
  • Когда хотите превратить кусок байтов, обёрнутый в пользовательский заголовок или прошедший через раунд сжатия, обратно в читаемое содержимое.

Сводный краткий справочник: точка входа декодирования, встроенные вспомогательные функции и среда выполнения. Каждая функция, принимающая buf, принимает либо ArrayBuffer, либо строку.

decode(buf, out) → number
Назначение Единственная точка входа декодировщика, должна быть реализована. Нарежьте сообщения из начала buf, выполните push их в out и верните число байтов, потреблённых на этот раз.
buf ArrayBuffer, оставшийся байтовый поток, ожидающий разбора. Используйте buf.byteLength для его длины; байты индексировать нельзя, используйте помощники ниже или new DataView(buf) / new Uint8Array(buf).
out Array, коллектор вывода. out.push(bytes) выпускает одно сообщение (bytes — это ArrayBuffer или строка; другие типы игнорируются); можно выпустить несколько за раз.
Возврат number, байты, потреблённые от начала buf. > 0 — продолжить; 0 / отрицательное / без возврата → остановка, оставшиеся байты показываются как сырые.
Соглашение о вызове Выполняется один проход по потоку отправки соединения и один по потоку приёма; в каждом проходе вызов повторяется, каждый раз передавая оставшиеся непотреблённые байты, пока вы не вернёте 0.

Взять подсрез

Сигнатура Возвращает Описание
sub(buf, off) ArrayBuffer Срез от off до конца
sub(buf, off, len) ArrayBuffer Срез длиной len от off; выход за диапазон off / len автоматически ограничивается, без ошибки

Прочитать целое (без знака; возвращает 0, если off вне диапазона)

Сигнатура Возвращает Описание
u8(buf, off) number Прочитать 1 байт
u16be(buf, off) / u16le(buf, off) number Прочитать 2 байта, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Прочитать 4 байта, big-endian / little-endian

Преобразовать в текст

Сигнатура Возвращает Описание
hex(buf) string Преобразовать в шестнадцатеричную строку нижнего регистра
ascii(buf) string Прочитать как текст (подходит для содержимого ASCII / текста)

Сжатие / распаковка (возвращает вход без изменений, если распаковать не удаётся; предел одной распаковки — около 16 МБ)

Сигнатура Возвращает Описание
gzipMagic(buf) boolean Начинается ли с магического числа gzip
gunzip(buf) ArrayBuffer Распаковка gzip
inflate(buf) ArrayBuffer Распаковка zlib / deflate
unzstd(buf) ArrayBuffer Распаковка zstd
lz4dtx(buf) ArrayBuffer Распаковка блочного потока LZ4

Отладка

Сигнатура Возвращает Описание
log(...args) (нет) Объединить аргументы в одну строку и вывести на панель «Отладочный вывод»; ArrayBuffer показывается в шестнадцатеричном виде
console.log(...args) (нет) То же, что и log (удобно для тех, кто привык к console.log)
  • Стандартный JavaScript: доступны распространённые встроенные объекты, такие как ArrayBuffer, типизированные массивы (Uint8Array и т. д.), DataView, JSON, Math, RegExp, Date; когда нужно низкоуровневое чтение/запись байтов, под рукой new DataView(buf) / new Uint8Array(buf).
  • Отладка: log(...) / console.log(...) выводят на панель «Отладочный вывод»; ошибки скрипта и таймауты тоже выводятся там, с пометкой, пришли ли они из потока отправки или приёма .
  • Не предоставляются другие специфичные для браузера / Node возможности: методы console, кроме log, TextDecoder / TextEncoder, fetch, setTimeout, require и т. д. — всё это недоступно. Чтобы прочитать байты как текст, используйте ascii(buf).
  • Каждый вызов decode имеет предел времени выполнения: бесконечный цикл или таймаут прерывается и трактуется как «ничего не потреблено», так что инструмент не зависает.

Назад к Захвату через прокси · Смотрите также: Просмотр и декодирование данных · Локальный захват на сетевой карте