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

Wireshark Lua диссектор: разбираем бинарный протокол от сырых байтов до читаемых полей

Wireshark Lua диссектор: разбираем бинарный протокол от сырых байтов до читаемых полей
Время чтения: 14 мин.

На внутреннем пентесте IoT-шлюз слал UDP-пакеты на порт 9443 — по 12–40 байт каждые пять секунд. Wireshark показывал голый hex в панели Data. Ни одного распознанного поля. Четыре часа я вручную считал смещения в hex-редакторе, пока не разобрался: magic-байт, тип команды, ID сенсора и payload переменной длины. А потом за 20 минут написал Lua-диссектор на 25 строк — и оставшиеся 200 пакетов дампа превратились в читаемую таблицу с фильтрами и колонками. Эта статья — пошаговый разбор обоих этапов: от сырого hex-дампа до рабочего диссектора с Proto.new и ProtoField.

Зачем нужен Lua-диссектор: место в цепочке сетевого анализа ИБ

Wireshark «из коробки» разбирает сотни протоколов — от HTTP и DNS до Modbus и DNP3. Но стоит столкнуться с нестандартным бинарным протоколом, и вместо читаемых полей вы получаете серый блок hex-данных с подписью «Data». Вот три сценария, где это больно.

Анализ C2-трафика. Малварь общается с управляющим сервером по кастомному бинарному протоколу — в терминологии MITRE ATT&CK (открытая база тактик и техник атак, где каждый приём имеет свой T-код) это техника Non-Application Layer Protocol (T1095). Атакующий выбирает нестандартный формат, чтобы обойти DPI (Deep Packet Inspection — анализ содержимого пакетов на уровне приложений) и сигнатурный анализ. Часто payload ещё и обфусцирован — Data Obfuscation (T1001). Чтобы понять, какие команды передаёт C2-сервер заражённому хосту, аналитику нужно декодировать каждое поле пакета. Без диссектора — часы ручной работы. С ним — минуты.

Реверс-инжиниринг IoT и промышленных устройств. Проприетарные протоколы датчиков, контроллеров и камер — всё, что не описано в RFC и не распознаётся Wireshark. На пентесте промышленной сети попадаются «обёртки» поверх стандартных протоколов: штатный диссектор разбирает нижний уровень, а прикладные данные остаются нечитаемыми.

CTF-задачи и исследовательская работа. Pcap-файлы с кастомными протоколами — классика категорий forensics и network на CTF-соревнованиях.

Цепочка работы во всех сценариях одна: захват трафика (Network Sniffing, T1040) → ручной разбор структуры пакетов → автоматизация через Lua-диссектор → анализ в Wireshark с фильтрами и колонками. Диссектор стоит на стыке второго и третьего шага: вы уже понимаете формат протокола и «объясняете» его Wireshark на языке Lua.

Требования к окружению

Прежде чем писать диссектор, проверьте рабочее место.

Компонент Минимум Рекомендация
Wireshark 3.0+ с поддержкой Lua 4.2+ (расширенный Lua API, heuristic dissectors)
ОС Windows 10 / Linux / macOS Любая — Lua-диссекторы кроссплатформенны
RAM 4 ГБ 8 ГБ для pcap-файлов от 100k пакетов
Текстовый редактор Любой VS Code с Lua-расширением (подсветка синтаксиса)
Дамп трафика Pcap/pcapng с целевым протоколом

Убедитесь, что Lua включён: Wireshark → Help → About Wireshark → вкладка Folders. Там перечислены папки Personal Lua Plugins и Global Lua Plugins — туда кладутся файлы .lua. Если вкладка отсутствует, откройте init.lua в директории установки Wireshark и проверьте: enable_lua должен быть равен true.

Wireshark — активно поддерживаемый open-source проект с ежемесячными обновлениями стабильной ветки; Lua API документирован на wiki.wireshark.org в разделе Lua/Dissectors.

Разбор бинарного протокола вручную в Wireshark

Захват трафика и первый взгляд на hex-дамп

Зачем этот шаг: прежде чем автоматизировать разбор, нужно руками понять, что означает каждый байт. Без этого диссектор писать бессмысленно — вы не будете знать, какие поля объявлять и какие смещения указывать.

Допустим, вы перехватили трафик между IoT-датчиком и шлюзом. В Wireshark видны UDP-пакеты на порт 9443. Протокол не распознан: в панели Packet Details единственный элемент — «Data» с hex-содержимым. Выбираем пакет и смотрим на панель Packet Bytes внизу окна — hex-дамп и ASCII-представление.

В первом пакете: ab 01 01 00 2a 00 04 1e 28 00 64 d7 — 12 байт. Во втором: ab 01 02 00 2b 00 00 80 — 8 байт. В третьем: ab 01 01 00 2a 00 06 1f 29 01 62 00 03 a2 — 14 байт. Три пакета разной длины, но с одинаковым началом — хватит материала, чтобы искать закономерности.

Если пакетов много, удобно выгрузить payload нескольких штук рядом. Правый клик по пакету → Follow → UDP Stream покажет данные всех пакетов потока, а команда tshark -r capture.pcap -Y "udp.port==9443" -T fields -e data.data в терминале выведет hex каждого пакета отдельной строкой — удобно для визуального сравнения.

Поиск паттернов: magic bytes, порядок байтов, границы полей

Ручной разбор трафика в Wireshark — детективная работа с байтами. Вот алгоритм, который работает для большинства простых бинарных протоколов.

Шаг 1 — ищем magic byte (сигнатурный байт). Все три пакета начинаются с 0xAB. Фиксированная сигнатура, по которой можно отличить «наш» протокол от случайных данных. Magic byte — стандартная практика: у EGTS это 0x01, у SSH — строка SSH-, у HTTP — GET или POST. Если первые байты совпадают во всех пакетах — почти наверняка magic.

Шаг 2 — находим фиксированные поля. Второй байт во всех пакетах — 0x01. Скорее всего версия протокола. Третий байт меняется: 0x01, 0x02, 0x01 — предположительно тип сообщения.

Шаг 3 — определяем многобайтовые поля и порядок байтов. Байты на позициях 3–4 (смещение, оно же offset, считается от нуля): 00 2a, 00 2b, 00 2a. Похоже на 16-битный идентификатор. Но в каком порядке читать?

Здесь ключевое понятие — порядок байтов (endianness). Big-endian (сетевой порядок) — старший байт первым: 00 2a = 0×256 + 42 = 42. Little-endian — наоборот, младший байт первым: те же 00 2a читались бы как 2a 00 = 42×256 + 0 = 10752. Отличить просто: если двухбайтовое значение выглядит как 00 xx с небольшим xx — вероятно big-endian; если xx 00 — little-endian. Большинство сетевых протоколов используют big-endian, но MongoDB, например, работает в little-endian.

Шаг 4 — поле длины. Байты 5–6: 00 04, 00 00, 00 06. После этого поля в первом пакете идут ровно 4 байта данных и 1 финальный байт. В третьем — 6 байт данных и 1 байт. Пакет с 00 00 — 0 байт данных плюс 1 байт. Вывод: байты 5–6 — длина payload (полезной нагрузки), за payload следует контрольная сумма.

Итого — восстановленная структура протокола:

Смещение Размер Поле Тип
0 1 байт Magic uint8, всегда 0xAB
1 1 байт Version uint8
2 1 байт MsgType uint8 (1=data, 2=heartbeat)
3 2 байта SensorID uint16, big-endian
5 2 байта PayloadLen uint16, big-endian
7 N байт Payload Данные переменной длины
7+N 1 байт Checksum uint8

Запишите или зарисуйте эту таблицу. Диссектор будет её механическим отображением в код: каждая строка таблицы превратится в один ProtoField и одну строку в функции dissector.

Пишем Wireshark Lua диссектор с Proto.new и ProtoField

Объявление протокола и полей

Файл диссектора — обычный текстовый файл с расширением .lua. Создайте sens_dissector.lua и положите в папку Personal Lua Plugins (путь виден в Help → About → Folders).

Первая часть — объявление протокола через Proto() и всех полей через ProtoField. Каждый ProtoField принимает: фильтрационное имя (строка вида "протокол.поле"), человекочитаемое название и формат отображения (base.HEX для шестнадцатеричного, base.DEC для десятичного).

local sens = Proto("SENS", "Sensor Telemetry Protocol")

local f_magic = ProtoField.uint8("sens.magic",     "Magic",          base.HEX)
local f_ver   = ProtoField.uint8("sens.version",   "Version",        base.DEC)
local f_type  = ProtoField.uint8("sens.msgtype",    "Message Type",   base.HEX)
local f_sid   = ProtoField.uint16("sens.sensor_id", "Sensor ID",      base.DEC)
local f_plen  = ProtoField.uint16("sens.pay_len",   "Payload Length", base.DEC)
local f_pay   = ProtoField.bytes("sens.payload",    "Payload")
local f_csum  = ProtoField.uint8("sens.checksum",   "Checksum",       base.HEX)
sens.fields = { f_magic, f_ver, f_type, f_sid, f_plen, f_pay, f_csum }

Что здесь произошло. Proto("SENS", "Sensor Telemetry Protocol") создаёт объект протокола — первый аргумент станет фильтруемым идентификатором в Wireshark (можно будет вбить sens в строку фильтра), второй — подпись в панели Packet Details. ProtoField.uint16 объявляет 16-битное беззнаковое поле — Wireshark будет знать, что два байта по указанному смещению нужно интерпретировать как число. Массив sens.fields регистрирует все поля в протоколе. Без этой строки Wireshark о них не узнает.

Функция dissector: разбор пакетов и регистрация на порт

Вторая часть — функция, которую Wireshark вызывает для каждого пакета, попавшего под регистрацию. Три аргумента: buffer (объект Tvb — буфер с сырыми байтами), pinfo (метаданные пакета: номер, временная метка, колонки списка) и tree (корень дерева Packet Details, куда добавляются узлы).

function sens.dissector(buffer, pinfo, tree)
    if buffer:len() < 8 then return 0 end
    if buffer(0,1):uint() ~= 0xAB then return 0 end
    pinfo.cols.protocol = "SENS"
    local st = tree:add(sens, buffer(), "SENS Protocol")
    st:add(f_magic, buffer(0,1))
    st:add(f_ver,   buffer(1,1))
    st:add(f_type,  buffer(2,1))
    st:add(f_sid,   buffer(3,2))
    local plen = buffer(5,2):uint()
    st:add(f_plen,  buffer(5,2))
    if buffer:len() >= 7 + plen + 1 then
        st:add(f_pay,  buffer(7, plen))
        st:add(f_csum, buffer(7 + plen, 1))
    end
    pinfo.cols.info = "SENS Sensor=" .. buffer(3,2):uint()
end
DissectorTable.get("udp.port"):add(9443, sens)

Разберём ключевые строки.

Проверки в начале. buffer:len() < 8 отсекает пакеты короче минимального размера протокола (7 байт заголовка + 1 байт контрольной суммы). buffer(0,1):uint() ~= 0xAB проверяет magic byte: если первый байт не 0xAB, функция возвращает 0 — сигнал Wireshark, что пакет не наш.

Чтение байтов. buffer(3,2) создаёт TvbRange — срез буфера начиная с байта 3 длиной 2 байта. Метод :uint() интерпретирует срез как беззнаковое целое в big-endian. Для протоколов с little-endian (тот же MongoDB) есть :le_uint(). Wireshark Lua API по умолчанию считает big-endian — сетевой порядок байтов.

Добавление в дерево. st:add(f_sid, buffer(3,2)) привязывает ProtoField к диапазону байтов. После этого в Packet Details появится строка «Sensor ID: 42», а при выделении поля в hex-дампе подсветятся байты 00 2a. Вот главное преимущество диссектора перед ручным разбором: клик по полю показывает байты, клик по байтам выделяет поле.

Обновление колонки Info. Присвоение pinfo.cols.info меняет текст в колонке Info списка пакетов. По умолчанию Wireshark пишет «Data (12 bytes)». С диссектором — «SENS Sensor=42». Разница ощутима, когда перед вами сотни пакетов.

Регистрация. DissectorTable.get("udp.port"):add(9443, sens) говорит Wireshark: каждый UDP-пакет на порт 9443 передавай нашему диссектору. Для TCP — "tcp.port". Если порт заранее неизвестен, пользователь может применить «Decode As…» — правый клик по пакету → Decode As → выбрать SENS из списка.

Чтобы активировать диссектор, перезапустите Wireshark или нажмите Ctrl+Shift+L (Analyze → Reload Lua Plugins) — скрипты перезагрузятся без перезапуска.

Читаемые колонки и фильтры для анализа бинарного протокола

Диссектор уже показывает поля в Packet Details, но для массового анализа — когда перед вами не 3 пакета, а 300 — нужны колонки и фильтры.

Добавление пользовательской колонки. Правый клик по заголовку колонок в списке пакетов → Column Preferences → кнопка «+». В поле Fields впишите sens.sensor_id, тип — Custom. Теперь отдельная колонка показывает ID датчика для каждого пакета, и таблицу можно сортировать по этому значению.

Display-фильтры. Все имена, указанные при создании ProtoField, автоматически работают как фильтры Wireshark:

  • sens.msgtype == 0x01 — только пакеты с типом data
  • sens.sensor_id == 42 — только пакеты от датчика с ID 42
  • sens.pay_len > 10 — пакеты с payload длиннее 10 байт
  • sens — все пакеты, распознанные как SENS

Фильтры комбинируются стандартными операторами: sens.sensor_id == 42 && sens.msgtype == 0x02 покажет heartbeat-пакеты от конкретного датчика. Это уже аналитика, а не ручной перебор hex-дампов.

Человекочитаемые значения. Чтобы вместо «Message Type: 0x01» в дереве отображалось «Message Type: Data (0x01)», при объявлении ProtoField передайте таблицу соответствий четвёртым аргументом: ProtoField.uint8("sens.msgtype", "Message Type", base.HEX, {[1]="Data", [2]="Heartbeat", [3]="Config"}). Wireshark подставит текстовое описание автоматически.

Использование с tshark. Lua-диссекторы работают и в консольной версии Wireshark. Команда tshark -r capture.pcap -X lua_script:sens_dissector.lua -T fields -e sens.sensor_id -e sens.msgtype выведет значения полей в текстовом виде — удобно для скриптовой обработки через grep, awk или Python.

Отладка Lua-диссекторов: типичные ошибки

Три инструмента, которые сэкономят время при разработке диссектора.

Lua-консоль. Tools → Lua Console. Все вызовы print() в скрипте выводятся сюда. Добавьте print("plen=" .. buffer(5,2):uint()) в функцию dissector — при загрузке pcap увидите значения. Printf-отладка: примитивно, но работает.

Горячая перезагрузка. После правки .lua-файла нажмите Ctrl+Shift+L — Wireshark перезагрузит все Lua-скрипты и переприменит их к текущему pcap. Не нужно перезапускать программу и заново открывать дамп.

Справочник типичных ошибок:

Симптом Причина Что делать
Wireshark не запускается, ошибка в заголовке окна Синтаксическая ошибка в Lua-файле Wireshark укажет номер строки — исправьте и перезапустите
Диссектор загрузился, но пакеты не разбираются Неверный порт в DissectorTable или magic-проверка отсекает все пакеты Сверьте порт с реальным; временно уберите проверку magic
Поле показывает неверное значение Неправильное смещение или порядок байтов Сверьте buffer(offset, length) с hex-дампом; замените :uint() на :le_uint() для little-endian
Ошибка «attempt to call a nil value» Опечатка в имени функции или метода Lua чувствителен к регистру: Buffer и buffer — разные вещи
Поле не фильтруется ProtoField не добавлен в массив proto.fields Убедитесь, что все поля перечислены в sens.fields = { ... }

Практический совет: откройте pcap до написания диссектора, выберите один пакет и запомните hex-значения первых 8 байт. После подключения диссектора проверьте — значения полей в дереве Packet Details должны совпадать с тем, что вы видели в hex-дампе. Не совпадают — проблема в смещениях или порядке байтов.

Написать диссектор Wireshark: Lua vs C vs Scapy

Критерий Lua-диссектор C-диссектор Python + Scapy
Время разработки 20–60 минут Часы–дни (сборка Wireshark) 30–90 минут
Производительность Достаточна для 100k пакетов Максимальная Медленно на более чем 10k пакетов
Интеграция с GUI Нативная: фильтры, колонки, дерево Нативная Нет: отдельный скрипт
Порог входа Низкий: Lua прост, API документирован Высокий: C, система сборки Средний: Python + знание библиотеки
Когда использовать Анализ pcap, быстрый прототип, CTF Продакшн-диссектор для включения в основной Wireshark Автоматизация, скриптовая обработка, генерация пакетов
Когда НЕ использовать Pcap больше 1 ГБ, realtime-захват с высоким PPS Одноразовый разбор или CTF-задача Нужна визуализация в Wireshark GUI

Для задач ИБ-аналитика и пентестера Lua — оптимальный выбор в большинстве ситуаций. Быстро пишется, мгновенно интегрируется в рабочий инструмент, не требует внешних зависимостей и компиляции.

Python с scapy или construct лучше подходит для автоматизации: извлечь все C2-команды из pcap в CSV или сгенерировать тестовые пакеты для фаззинга. Но предварительный анализ протокола удобнее в Wireshark с Lua-диссектором — визуальная привязка полей к байтам ускоряет обнаружение ошибок в интерпретации формата.

Ограничения техники и когда диссектор не поможет

TCP-reassembly. Пример в статье работает с UDP, где каждый пакет — самостоятельное сообщение. С TCP сложнее: одно сообщение протокола может быть разбито на несколько TCP-сегментов, или один сегмент содержит несколько сообщений. Wireshark предоставляет Lua-функцию [dissect_tcp_pdus()](https://wiki.wireshark.org/Lua/Dissectors) для автоматической сборки, но корректная реализация требует обработки четырёх сценариев: неполное сообщение, несколько сообщений в одном сегменте, начало захвата посреди TCP-сессии и обрезанные пакеты. Каждый сценарий описан в документации Wireshark wiki (Lua/Dissectors → TCP reassembly) с примерами кода.

Шифрованный трафик. Если протокол шифрует payload (TLS, кастомный XOR или AES), диссектор покажет зашифрованные байты — он не занимается криптоанализом. Для TLS Wireshark умеет использовать pre-master secret log при наличии доступа к ключам. Для кастомного шифрования придётся сначала отреверсить алгоритм — и только потом добавлять расшифровку прямо в Lua-код диссектора.

Большие дампы. На pcap от 500 МБ Lua-диссектор заметно тормозит загрузку по сравнению со встроенными C-диссекторами. Если гигабайтные дампы — регулярная задача, используйте предварительную фильтрацию: tshark -Y "udp.port==9443" -r large.pcap -w filtered.pcap вырежет только нужные пакеты в отдельный файл.

Stateful-протоколы. Некоторые протоколы меняют структуру пакетов в зависимости от содержимого предыдущих (тип шифрования согласуется при handshake). Lua-диссектор вызывается для каждого пакета независимо — хранить состояние между вызовами можно через глобальные таблицы Lua, но Wireshark не гарантирует порядок вызова при повторном анализе. Это создаёт проблемы с воспроизводимостью результатов.


Большинство аналитиков, которых я видел на incident response, до сих пор разбирают неизвестный трафик глазами — пересчитывают смещения в hex-дампе, записывают значения в блокнот, сопоставляют пакеты вручную. Для пяти пакетов это работает. Для пятисот — потерянный день и пропущенные паттерны, которые были бы видны при фильтрации по полю за секунду.

Навык написания Lua-диссектора — один из тех, что отделяют «могу смотреть в Wireshark» от «могу анализировать трафик быстро». Порог входа — 30 минут и статья вроде этой. Отдача — сэкономленные часы при каждом следующем инциденте с нестандартным протоколом. Wireshark Lua API стабилен и обратно совместим уже много лет; меняются протоколы и малварь, но механика «вот байты → вот поля → вот фильтры» остаётся неизменной.

Есть мнение, что такие навыки «слишком нишевые» для начинающего. Не согласен: именно умение быстро разобраться в незнакомом протоколе отличает толкового джуниора от того, кто умеет только запускать готовые инструменты. Если хочешь пройти базу системно, а не собирать по статьям — на codeby.school есть курс IB Basics, который покрывает фундамент сетевого анализа и работы с инструментами за пару месяцев, без воды.

Эту тему и смежные навыки разбирают на практике в курсе «Аналитик SOC» Codeby Academy.