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

На внутреннем пентесте 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— только пакеты с типом datasens.sensor_id == 42— только пакеты от датчика с ID 42sens.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.