multidisorder — TCP-сегментация в обратном порядке (zapret2 / nfqws2)
Файл: lua/zapret-antidpi.lua:597
nfqws1 эквивалент: --dpi-desync=multidisorder
Сигнатура: function multidisorder(ctx, desync)
multidisorder — функция TCP-сегментации в zapret2, которая берёт текущий payload (или reasm, или blob), разрезает его на несколько TCP-сегментов по заданным позициям и отправляет их в обратном порядке (от последнего к первому). Обратный порядок (disorder) эксплуатирует поведение TCP-стека: при получении внеочередных сегментов стек буферизирует их и собирает поток, а DPI зачастую не справляется с реассемблированием разупорядоченных данных. После успешной отправки выносит VERDICT_DROP, чтобы оригинальный пакет не ушёл.
Родственные функции: multisplit (прямой порядок), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок), hostfakesplit (по hostname), tcpseg (диапазон), oob (urgent byte), multidisorder_legacy (совместимый с nfqws1 алгоритм).
Общий для всех техник дурения порядок работы — восемь стадий от отсева чужого транспорта до вердикта — разобран в жизненный цикл desync-функции; здесь описано только то, чем multidisorder от этого скелета отличается.
Оглавление
- Зачем нужен multidisorder
- Быстрый старт
- Откуда берутся данные для нарезки
- Маркеры позиций (pos)
- Обратный порядок отправки — суть disorder
- seqovl — перезапись буфера сокета через перекрытие
- Полный список аргументов
- Порядок отправки сегментов
- Поведение при replay / reasm
- Автосегментация по MSS
- Место в общем скелете
- Псевдокод алгоритма
- Нюансы и подводные камни
- Отличия от других функций сегментации
- Миграция с nfqws1
- Практические примеры
Зачем нужен multidisorder
DPI анализирует TCP-поток, пытаясь собрать полный payload и найти в нём сигнатуры (hostname в HTTP, SNI в TLS). Разрезание payload на сегменты уже усложняет работу DPI (multisplit), но обратный порядок отправки создаёт дополнительную проблему:
- DPI не реассемблирует out-of-order сегменты: многие DPI работают потоково (stream-based) и ожидают сегменты в порядке sequence number. Получив сначала хвост потока, DPI может потерять контекст или не дождаться головы
- DPI не может сопоставить hostname: если разрез проходит через SNI/Host, а сегменты приходят задом наперёд, ни попакетный, ни потоковый DPI не найдёт сигнатуру
- seqovl в режиме disorder перезаписывает буфер: в отличие от multisplit, где seqovl работает через выход за TCP window, в disorder фейковые данные сначала попадают в буфер сокета, а затем перезаписываются реальными данными из последнего сегмента
Сервер при этом корректно собирает поток — TCP-стек буферизирует внеочередные сегменты и отдаёт данные приложению только после сборки непрерывной последовательности.
multidisorder vs multisplit: disorder работает за счёт разупорядочивания, а не за счёт линейного дробления. Для замешивания фейковых сегментов (отдельные пакеты с fooling) есть fakeddisorder.
Быстрый старт
Минимально (разрез по позиции 2, payload=known, dir=out):
--lua-desync=multidisorderТиповой TLS-разрез:
--payload=tls_client_hello --lua-desync=multidisorder:pos=midsldTLS с seqovl (маркер):
--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1HTTP с разрезом по hostname:
--payload=http_req --lua-desync=multidisorder:pos=host,midsld,endhostОткуда берутся данные для нарезки
Внутри multidisorder данные (data) выбираются в следующем порядке приоритетов:
1. blob_or_def(desync, desync.arg.blob) — если задан blob= и он существует
2. desync.reasm_data — если есть реассемблированные данные (multi-packet payload)
3. desync.dis.payload — текущий пакет (fallback)
Следствие: все маркеры pos, seqovl и прочие аргументы применяются именно к тем данным, которые реально выбраны. Если вы задали blob=myblob, маркеры вроде midsld будут работать только если myblob содержит валидный TLS/HTTP payload, который zapret может распознать.
Маркеры позиций (pos)
pos — главный аргумент multidisorder. Определяет где внутри payload будет произведён разрез. Задаётся как строка со списком маркеров через запятую.
Типы маркеров
| Тип | Описание | Пример |
|---|---|---|
| Абсолютный положительный | Смещение от начала payload, считая с 0. pos=N → первые N байт становятся отдельным сегментом | 1, 5, 100 |
| Абсолютный отрицательный | Смещение от конца payload. -1 = последний байт | -1, -10, -50 |
| Относительный | Логическая позиция внутри распознанного payload. Привязана к структуре протокола | midsld, host, sniext |
Относительные маркеры
| Маркер | Описание | Для каких payload |
|---|---|---|
method | Начало HTTP-метода (GET, POST, HEAD, PUT и т.д.). Обычно позиция 0, но может стать 1-2 при использовании http_methodeol | http_req |
host | Первый байт имени хоста (Host: в HTTP, SNI в TLS) | http_req, tls_client_hello |
endhost | Байт, следующий за последним байтом имени хоста. Т.е. host..endhost-1 = полный hostname | http_req, tls_client_hello |
sld | Первый байт домена второго уровня (SLD). Для www.example.com — это e в example | http_req, tls_client_hello |
endsld | Байт, следующий за последним байтом SLD. Для example.com — это . после example | http_req, tls_client_hello |
midsld | Середина SLD (самый популярный маркер). Для example (7 символов) — позиция 3-го или 4-го символа | http_req, tls_client_hello |
sniext | Начало поля данных SNI extension в TLS ClientHello. Extension состоит из type (2 байта) + length (2 байта) + данные — sniext указывает на начало данных | tls_client_hello |
extlen | Поле длины всех TLS extensions | tls_client_hello |
Арифметика маркеров
К любому маркеру можно прибавить (+) или вычесть (-) целое число:
midsld+1 — один байт ПОСЛЕ середины SLD
midsld-1 — один байт ДО середины SLD
endhost-2 — два байта до конца hostname
method+2 — два байта после начала метода (разрежет "GET " после "GE")
sniext+1 — один байт после начала SNI extension data
host+3 — три байта после начала hostname
-1 — последний байт payload (абсолютный, не относительный)
Арифметика работает и с абсолютными маркерами, хотя это избыточно (5+3 = 8).
Пример списка маркеров
pos=100,midsld,sniext+1,endhost-2,-10
Здесь 5 маркеров → payload разрежется максимум на 6 частей (если все маркеры успешно разрешатся и дадут различные позиции).
Как маркеры разрешаются в коде
Внутри multidisorder вызывается:
local pos = resolve_multi_pos(data, desync.l7payload, spos)Функция resolve_multi_pos:
- Разбивает строку
sposпо запятым - Для каждого маркера вызывает
resolve_pos(blob, l7payload_type, marker) - Если маркер не может быть разрешён (например,
midsldдляunknownpayload) — он молча пропускается - Результаты дедуплицируются и сортируются
- Возвращается массив уникальных позиций, уже переведённых в 1-based индексы Lua (маркер
1превращается в позицию2, маркер0— в позицию1)
Затем вызывается:
delete_pos_1(pos) -- удалить Lua-позицию 1, то есть маркер 0Важные нюансы pos
- Разрез в самом начале данных отбрасывается.
delete_pos_1удаляет Lua-позицию 1, которая соответствует маркеру0: первый сегмент вышел бы пустым. Считать маркеры нужно с нуля, поэтому под удаление попадаетpos=0, аpos=1(первый байт отдельным сегментом) работает и применяется часто. Частая жертва этого правила —pos=methodдля HTTP: метод обычно лежит на смещении 0, разреза не произойдёт; нужен сдвиг вродеpos=method+2 - Дублирующиеся позиции объединяются.
pos=5,5,5=pos=5 - Неразрешимые маркеры пропускаются. Если
midsldне разрешается (payload = unknown), он просто исчезает из списка. Если все маркеры не разрешились — multidisorder ничего не делает (логирует “no valid split positions”) - Позиции сортируются. Независимо от порядка записи,
pos=100,5,50будет обработано как5,50,100 - По умолчанию
pos=2. Еслиposне задан, payload делится на 2 части: первые 2 байта отдельным сегментом, весь остаток — вторым
Обратный порядок отправки — суть disorder
Ключевое отличие multidisorder от multisplit: сегменты отправляются от последнего к первому (в порядке убывания sequence number). Цикл в multidisorder_send:
for i=#pos,0,-1 domultisplit: отправка 1 -> 2 -> 3 -> 4 (прямой порядок)
multidisorder: отправка 4 -> 3 -> 2 -> 1 (обратный порядок)
Почему disorder ломает DPI
TCP-стек сервера спроектирован для работы с внеочередными сегментами — он буферизирует их и ждёт недостающие части. DPI, как правило, не имеет такой роскоши:
- Потоковые DPI обрабатывают данные по мере поступления. Получив последний сегмент первым, DPI видит данные без начала (без заголовков HTTP/TLS) и не может распознать протокол
- Попакетные DPI анализируют каждый пакет отдельно. В каждом отдельном сегменте нет полной сигнатуры
- DPI с реассемблированием теоретически могут собрать поток, но на практике многие имеют ограниченный буфер или timeout и не дожидаются всех частей
Ограничение: Windows-серверы
Техника seqovl в режиме disorder не работает с Windows-серверами. При перекрытии sequence numbers:
- Linux/BSD/macOS: более поздний сегмент перезаписывает ранее буферизированные данные
- Windows: сохраняет первые полученные данные, игнорируя перекрывающиеся
Это означает, что seqovl_pattern, записанный в буфер предпоследним сегментом, на Windows не будет перезаписан реальными данными из последнего сегмента — сервер получит мусор.
Без seqovl multidisorder работает нормально на всех системах — простое разупорядочивание не зависит от поведения при перекрытии.
seqovl — перезапись буфера сокета через перекрытие
В multidisorder техника seqovl работает принципиально иначе, чем в multisplit. Здесь seqovl эксплуатирует переписывание данных в буфере сокета при получении перекрывающихся сегментов.
Принцип работы seqovl в disorder
Рассмотрим payload из 2 частей: pos=100 (одна точка разреза).
Payload (300 байт): [ЧАСТЬ_1: 100 байт][ЧАСТЬ_2: 200 байт]
^pos=100
seqovl=midsld (разрешился в 50), ovl = 50 - 1 = 49
Порядок отправки (обратный, i от #pos до 0):
Шаг 1 (i=1): ЧАСТЬ_2 с seqovl!
К ЧАСТЬ_2 слева приписывается 49 байт PATTERN
part = [PATTERN(49)][ЧАСТЬ_2(200)]
seq = pos_start - 1 - ovl = 100 - 1 - 49 = 50
len = 249
Отправляется первым
Шаг 2 (i=0): ЧАСТЬ_1 без seqovl
part = [ЧАСТЬ_1(100)]
seq = 0
len = 100
Отправляется последним
Что происходит на сервере (Linux/BSD):
t1: приходит [PATTERN(49)][ЧАСТЬ_2(200)] seq=50 len=249
Буфер сокета:
позиции: 0 50 100 300
[ пусто ][PATTERN(49)][ЧАСТЬ_2(200)........]
^--- PATTERN занимает позиции 50..98
^--- ЧАСТЬ_2 начинается с позиции 99
Данные НЕ выдаются приложению — нет непрерывной последовательности от 0
t2: приходит [ЧАСТЬ_1(100)] seq=0 len=100
Буфер сокета (Linux/BSD — позднее ПЕРЕЗАПИСЫВАЕТ):
позиции: 0 100 300
[ЧАСТЬ_1(100)....................][ЧАСТЬ_2(200)........]
^--- ЧАСТЬ_1 занимает позиции 0..99
PATTERN ПЕРЕЗАПИСАН реальными данными!
Непрерывная последовательность 0..299 -> выдаётся приложению корректно
На Windows-сервере:
t2: приходит [ЧАСТЬ_1(100)] seq=0 len=100
Буфер сокета (Windows — СОХРАНЯЕТ старое):
позиции: 0 50 100 300
[ЧАСТЬ_1(50)][PATTERN(49)][ЧАСТЬ_2(200)........]
^--- только байты 0..49 из ЧАСТЬ_1 записались
^--- байты 50..98 остались от PATTERN (мусор!)
Приложение получает повреждённые данные!
Отличие seqovl от multisplit
| Аспект | seqovl в multisplit | seqovl в multidisorder |
|---|---|---|
| Механизм | Выход за левую границу TCP window | Перезапись буфера перекрывающимся сегментом |
| К какому сегменту | 1-й отправляемый (1-й в оригинале, i==0) | Предпоследний отправляемый (2-й в оригинале, i==1) |
| Тип значения | Только число | Маркер (число, имя маркера, маркер+арифметика) |
| Кто отбрасывает фейк | TCP-стек (данные за пределами window) | Перезапись следующим сегментом (последним отправляемым) |
| Работает на Windows | Да (TCP window — универсальный механизм) | Нет (Windows не перезаписывает буфер) |
seqovl как маркер
В отличие от multisplit, где seqovl принимает только число, в multidisorder seqovl разрешается через resolve_pos() — полноценный маркер:
seqovl = resolve_pos(data, desync.l7payload, desync.arg.seqovl)Это означает, что поддерживаются:
seqovl=5 — число (абсолютная позиция, как в multisplit)
seqovl=midsld-1 — маркер с арифметикой (типичное использование)
seqovl=host+2 — маркер с арифметикой
seqovl=sld — маркер без арифметики
seqovl=-10 — отрицательная позиция (от конца данных)
Типичный паттерн: pos=midsld + seqovl=midsld-1. Разрез по середине SLD, а seqovl перекрывает почти всю первую часть.
Если маркер не резолвится (например, seqovl=midsld-1 для unknown payload) — seqovl отменяется, но сегментация в обратном порядке всё равно происходит. В лог записывается “seqovl cancelled because could not resolve marker”.
Валидация seqovl: должен быть меньше pos[1]
Из multidisorder_send:
if seqovl>=pos[1] then
DLOG("multidisorder: seqovl cancelled because seqovl "..
(seqovl-1).." is not less than the first split pos "..(pos[1]-1))seqovl (разрешённое значение маркера) обязательно должен быть строго меньше pos[1] (первой позиции разреза). Если это условие не выполнено — seqovl отменяется (но сегментация всё равно происходит, просто без перекрытия).
Почему: seqovl приписывается к сегменту i==1 (ЧАСТЬ_2, от pos[1] до конца или до pos[2]-1). Перекрытие заползает назад на ovl байт. Если seqovl >= pos[1], перекрытие вылезло бы за начало данных первого сегмента, что не имеет смысла.
Пример:
pos=10,50 -> pos[1]=10
seqovl=midsld -> разрешился в 8 -> OK (8 < 10)
seqovl=midsld -> разрешился в 10 -> CANCELLED (10 >= 10, не строго меньше)
seqovl=midsld -> разрешился в 15 -> CANCELLED (15 >= 10)
Нюанс реализации: ovl = seqovl - 1
В коде multidisorder_send есть важная деталь:
ovl = seqovl - 1Реальный размер перекрытия на 1 байт меньше разрешённого значения маркера. Это связано с тем, что seqovl возвращается как позиция (1-based в Lua), а ovl используется как размер смещения (0-based). Для пользователя это означает:
seqovl=midsld (разрешился, скажем, в 50)
-> ovl = 49
-> К сегменту приписывается 49 байт pattern слева
-> TCP seq уменьшается на 49
Следствие: если seqovl разрешится в 1, то ovl = 0 — seqovl фактически не применяется. Минимальный эффективный seqovl — значение 2 (ovl = 1, 1 байт перекрытия).
seqovl_pattern
Паттерн, которым заполняется seqovl-область (ovl байт слева от реальных данных). По умолчанию — 0x00 (нули).
В multidisorder seqovl_pattern — это имя blob. Паттерн повторяется функцией pattern() до нужной длины ovl (= seqovl - 1).
# Inline hex blob (маскировка под начало TLS record)
--lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000
# Предзагруженный blob
--blob=tlspat:0x1603030100 \
--lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=tlspatЕсли optional задан и blob seqovl_pattern отсутствует — используется нулевой паттерн (операция не отменяется).
Полный список аргументов
Формат вызова:
--lua-desync=multidisorder[:arg1[=val1][:arg2[=val2]]...]
Все val приходят в Lua как строки. Если =val не указан, значение = пустая строка "" (в Lua это truthy), поэтому флаги пишутся просто как :optional, :nodrop, :tcp_ts_up.
A) Собственные аргументы multidisorder
pos
- Формат:
pos=<marker[,marker2,...]> - Тип: строка со списком маркеров через запятую
- По умолчанию:
"2" - Описание: Точки разреза. Каждый маркер определяет позицию, по которой payload будет разрезан. N маркеров → до N+1 сегментов. Сегменты отправляются в обратном порядке
- Примеры:
pos=2— разрез после 2-го байта (дефолт)pos=1— первый байт уходит отдельным сегментомpos=midsld— разрез посередине SLDpos=1,midsld— два разреза: после 1-го байта и посередине SLD → 3 сегментаpos=host,midsld,endhost-2,-10— четыре разреза → до 5 сегментовpos=method+2— после первых 2 символов HTTP-метода
seqovl
- Формат:
seqovl=<marker>(маркер, число, или маркер+арифметика) - Тип: маркер (в отличие от multisplit, где только число). Разрешается через
resolve_pos() - По умолчанию: не задан (нет seqovl)
- Описание: Применяется к сегменту
i==1(2-й в оригинальном порядке, предпоследний отправляемый). К данным этого сегмента слева добавляетсяseqovl-1байтseqovl_pattern, а TCPth_seqуменьшается наseqovl-1. Последний отправляемый сегмент (1-й в оригинале) перезаписывает фейковые данные в буфере сервера - Ограничение: разрешённое значение seqovl должно быть строго меньше pos[1], иначе seqovl отменяется
- Примеры:
seqovl=5— 4 байта фейка слева (ovl = 5-1)seqovl=midsld-1— типичное использование сpos=midsldseqovl=host+2— маркер с арифметикойseqovl=sld— маркер без арифметики
seqovl_pattern
- Формат:
seqovl_pattern=<blobName> - Тип: имя blob-переменной
- По умолчанию: один байт
0x00, повторяемый до длиныovl(= seqovl - 1) - Описание: Данные для заполнения seqovl-области. Blob повторяется функцией
pattern()до нужного размера - Поведение с
optional: еслиoptionalзадан и blob отсутствует — используется нулевой паттерн, seqovl не отменяется - Примеры:
seqovl_pattern=0x1603030000— inline hex (маскировка под TLS)seqovl_pattern=my_pattern_blob— предзагруженный blob
blob
- Формат:
blob=<blobName> - Тип: имя blob-переменной
- По умолчанию: не задан
- Описание: Заменить текущий payload/reasm на указанный blob и резать/слать его. Используется для отправки произвольных данных (фейковых payload, модифицированных ClientHello и т.д.)
- Примеры:
blob=fake_default_tls— стандартный TLS-фейкblob=0xDEADBEEF— inline hexblob=my_custom_ch— предзагруженный blob
optional
- Формат:
optional(флаг, без значения) - Описание: Мягкий режим:
- Если задан
blob=...и blob отсутствует → multidisorder ничего не делает (тихий skip, без ошибок) - Если задан
seqovl_pattern=...и blob отсутствует → используется нулевой паттерн (seqovl не отменяется)
- Если задан
- Использование: защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией)
nodrop
- Формат:
nodrop(флаг, без значения) - Описание: После успешной отправки сегментов не выносить
VERDICT_DROP(вместо этого вернутьVERDICT_PASS). Это означает, что оригинальный пакет тоже будет отправлен (наряду с нарезанными сегментами) - Использование: для отладки, для отправки произвольных данных без блокировки оригинала
- Предупреждение: в боевых профилях
nodropобычно нежелателен — оригинал ещё раз уйдёт, что создаст дублирование и может ухудшить обход
B) Standard direction
| Параметр | Значения | По умолчанию |
|---|---|---|
dir | in, out, any | out |
Фильтр по направлению пакета. multidisorder по умолчанию работает только с исходящими (out).
dir=out— только исходящие (от клиента к серверу)dir=in— только входящие (от сервера к клиенту)dir=any— оба направления
При первом вызове с указанным dir функция делает direction_cutoff_opposite — отсекает себя от противоположного направления.
C) Standard payload
| Параметр | Значения | По умолчанию |
|---|---|---|
payload | список типов через запятую | known |
Фильтр по типу payload на уровне Lua. Это дополнительный фильтр к --payload=... на уровне профиля.
payload=known— только распознанные протоколы (http_req,tls_client_hello,quic_initialи т.д.)payload=all— любой payload, включаяunknownpayload=tls_client_hello,http_req— конкретные типыpayload=~unknown— инверсия: всё кроме unknown
Важно: лучше ставить --payload=... на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.
D) Standard fooling
Модификации L3/L4 заголовков. В multidisorder применяются ко всем отправляемым сегментам (в отличие от fakeddisorder, где fooling идёт только на фейки).
| Параметр | Описание | Пример |
|---|---|---|
ip_ttl=N | Установить IPv4 TTL | ip_ttl=6 |
ip6_ttl=N | Установить IPv6 Hop Limit | ip6_ttl=6 |
ip_autottl=delta,min-max | Автоматический TTL (delta от серверного TTL) | ip_autottl=-2,40-64 |
ip6_autottl=delta,min-max | Аналогично для IPv6 | ip6_autottl=-2,40-64 |
ip6_hopbyhop[=HEX] | Вставить extension header hop-by-hop (по умолчанию 6 нулей) | ip6_hopbyhop |
ip6_hopbyhop2[=HEX] | Второй hop-by-hop header | ip6_hopbyhop2 |
ip6_destopt[=HEX] | Destination options header | ip6_destopt |
ip6_destopt2[=HEX] | Второй destination options | ip6_destopt2 |
ip6_routing[=HEX] | Routing header | ip6_routing |
ip6_ah[=HEX] | Authentication header | ip6_ah |
tcp_seq=N | Сместить TCP sequence (+ или -) | tcp_seq=-10000 |
tcp_ack=N | Сместить TCP ack (+ или -) | tcp_ack=-66000 |
tcp_ts=N | Сместить TCP timestamp | tcp_ts=-100 |
tcp_md5[=HEX] | Добавить TCP MD5 option (16 байт; по умолчанию случайные) | tcp_md5 |
tcp_flags_set=LIST | Установить TCP-флаги | tcp_flags_set=FIN,PUSH |
tcp_flags_unset=LIST | Снять TCP-флаги | tcp_flags_unset=ACK |
tcp_ts_up | Поднять TCP timestamp option в начало заголовка | tcp_ts_up |
tcp_nop_del | Удалить все TCP NOP опции | tcp_nop_del |
fool=<func> | Кастомная Lua-функция fooling | fool=my_fooler |
Заметка про tcp_ts_up: На Linux-серверах пакеты с инвалидным ACK стабильно отбрасываются только если TCP timestamp option идёт первой в заголовке. tcp_ts_up перемещает её в начало, обеспечивая корректную работу badseq-fooling.
E) Standard ipid
| Параметр | Описание | По умолчанию |
|---|---|---|
ip_id=seq | Последовательные IP ID | seq |
ip_id=rnd | Случайные IP ID | — |
ip_id=zero | Нулевые IP ID | — |
ip_id=none | Не менять IP ID | — |
ip_id_conn | Сквозная нумерация IP ID в рамках соединения (требует tracking) | — |
ip_id применяется к каждому отправляемому сегменту (включая под-сегменты при MSS-сегментации).
F) Standard ipfrag
IP-фрагментация поверх TCP-сегментации. Каждый TCP-сегмент дополнительно фрагментируется на уровне IP.
| Параметр | Описание | По умолчанию |
|---|---|---|
ipfrag[=func] | Включить IP-фрагментацию. Если без значения → ipfrag2 | — |
ipfrag_disorder | Отправить IP-фрагменты в обратном порядке | — |
ipfrag_pos_tcp=N | Позиция фрагментации TCP (кратно 8) | 32 |
ipfrag_pos_udp=N | Позиция фрагментации UDP (кратно 8). Для multidisorder бесполезно — он только TCP | 8 |
ipfrag_next=N | IPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы) | — |
G) Standard reconstruct
| Параметр | Описание |
|---|---|
badsum | Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
H) Standard rawsend
| Параметр | Описание |
|---|---|
repeats=N | Отправить каждый сегмент N раз (идентичные повторы) |
ifout=<iface> | Интерфейс для отправки (по умолчанию определяется автоматически) |
fwmark=N | Firewall mark (только Linux, nftables/iptables) |
Порядок отправки сегментов
multidisorder всегда отправляет сегменты в обратном порядке — от последнего к первому (в порядке убывания TCP sequence). Цикл в multidisorder_send: for i=#pos,0,-1.
Пример с 3 позициями разреза (без seqovl)
Payload (600 байт):
[AAA...100 байт...AAA][BBB...200 байт...BBB][CCC...150 байт...CCC][DDD...150 байт...DDD]
^pos=100 ^pos=300 ^pos=450
Оригинальный порядок Порядок отправки
------------------- ----------------
Сегмент 0 (i=0): [AAA...100] seq=0 4-й (последний)
Сегмент 1 (i=1): [BBB...200] seq=100 3-й
Сегмент 2 (i=2): [CCC...150] seq=300 2-й
Сегмент 3 (i=3): [DDD...150] seq=450 1-й (первым отправляется)
Реальная последовательность отправки:
1. [DDD...150] seq=450 len=150 (i=3, последний сегмент данных)
2. [CCC...150] seq=300 len=150 (i=2)
3. [BBB...200] seq=100 len=200 (i=1)
4. [AAA...100] seq=0 len=100 (i=0, первый сегмент данных)
Пример с seqovl (2 сегмента)
Payload (300 байт), pos=100, seqovl=midsld (разрешился в 50):
ovl = 50 - 1 = 49
Оригинальный порядок Порядок отправки
------------------- ----------------
Сегмент 0 (i=0): [ЧАСТЬ_1: 100 байт] seq=0 2-й (последний)
Сегмент 1 (i=1): [ЧАСТЬ_2: 200 байт] seq=100 1-й (первым)
Реальная последовательность отправки:
1. i=1: [PATTERN(49)][ЧАСТЬ_2(200)] seq=100-1-49=50 len=249 (seqovl!)
2. i=0: [ЧАСТЬ_1(100)] seq=0 len=100 (перезаписывает PATTERN)
ASCII-диаграмма: хронология буфера сокета на сервере
Время -->
t1: приходит [PATTERN(49)][ЧАСТЬ_2(200)] seq=50 len=249
Буфер сокета:
позиция: 0 50 99 100 299
[ пусто ][PATRN(49)][?? ][ЧАСТЬ_2(200)..............]
^^^^^^^^^^^^--- PATTERN записан в позиции 50-98
^^^--- ЧАСТЬ_2 в позициях 99-298
Данные НЕ выдаются приложению (нет непрерывности от 0)
t2: приходит [ЧАСТЬ_1(100)] seq=0 len=100
Linux/BSD (поздний ПЕРЕЗАПИСЫВАЕТ):
позиция: 0 100 299
[ЧАСТЬ_1(100)............][ЧАСТЬ_2(200)..............]
^--- ЧАСТЬ_1 перезаписала позиции 0-99
PATTERN уничтожен, данные корректны!
Windows (СОХРАНЯЕТ раннее):
позиция: 0 50 99 100 299
[ЧАСТЬ_1(50)][PATRN(49)][?? ][ЧАСТЬ_2(200)..............]
^--- только позиции 0-49 обновились
позиции 50-98 остались от PATTERN = МУСОР
Пример с 3 позициями + seqovl
Payload (600 байт), pos=100,300,450, seqovl=host (разрешился в 80):
ovl = 80 - 1 = 79
Порядок отправки:
1. i=3: [DDD...150] seq=450 len=150 (без seqovl)
2. i=2: [CCC...150] seq=300 len=150 (без seqovl)
3. i=1: [PATTERN(79)][BBB...200] seq=100-1-79=20 len=279 (seqovl! только i==1)
4. i=0: [AAA...100] seq=0 len=100 (перезаписывает PATTERN)
Обратите внимание: seqovl применяется только к i==1 (2-й сегмент в оригинальном порядке, предпоследний в порядке отправки), независимо от количества позиций разреза.
Поведение при replay / reasm
Многопакетный запрос (большой TLS ClientHello с post-quantum ключами и т. п.) nfqws2 придерживает, собирает в буфер reasm_data и перепроигрывает через desync-функции. multidisorder использует штатную развилку перепроигрывания из общего скелета desync-функций (жизненный цикл desync-функции, стадия 6) без изменений: нарезает и отправляет весь собранный буфер на первой части, а остальные части дропает, потому что их содержимое уже ушло в составе нарезки. Если отправка сорвалась, флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть.
Специфика multidisorder — в следствии этого поведения. Раз режется весь реасм целиком, обратный порядок распространяется на весь запрос, а не на отдельные пакеты: последний сегмент всего ClientHello уйдёт первым, даже если исходно он приехал третьим пакетом.
Отличие от multidisorder_legacy: legacy-версия развилку не использует вовсе — она обрабатывает каждую часть replay отдельно, нормализуя позиции разреза под границы конкретного пакета, как это делал nfqws1. Из-за этого порядок сегментов при многопакетных запросах у двух функций разный. Для полной совместимости с nfqws1 нужен именно legacy-вариант.
Автосегментация по MSS
О размерах TCP-сегментов думать не нужно. Отправкой занимается rawsend_payload_segmented из zapret-lib.lua, и она сама доводит куски до допустимого размера:
- MSS берётся из
desync.tcp_mss— движок отслеживает его для каждого TCP-соединения - Если кусок вместе с заголовками превышает MSS — он дополнительно режется по MSS
- Каждый под-сегмент отправляется с корректным TCP sequence
Пример: если seqovl разрешился в большое значение (скажем, 5000), это не вызовет ошибку. rawsend_payload_segmented отправит несколько TCP-сегментов общим размером, равным seqovl_pattern + данные сегмента.
Место в общем скелете
Тело multidisorder построено по тому же шаблону, что и все остальные функции дурения: восемь стадий от отсева чужого транспорта до вердикта. Разобраны они один раз в жизненный цикл desync-функции — здесь только отклонения:
| Стадия общего скелета | Что делает multidisorder |
|---|---|
| 1. Отсев транспорта | только TCP; на не-TCP делает instance_cutoff (кроме связанного ICMP) |
| 2. Направление | dir=out по умолчанию, отключается от входящего |
| 3. Аргументы | обязательных нет; optional + отсутствующий blob → тихий выход |
| 4. Данные | штатная цепочка blob → reasm → payload |
| 5. Гварды | штатные #data>0, direction_check, payload_check (по умолчанию known) |
| 6. replay | штатная развилка: нарезка на первой части, дроп остальных |
| 7. Своя техника | разрешение списка маркеров, нарезка, seqovl на втором по оригинальному порядку сегменте, отправка от последнего сегмента к первому — этому посвящена вся заметка |
| 8. Вердикт | VERDICT_DROP после успешной отправки, VERDICT_PASS при nodrop или сбое rawsend |
Отличие от multisplit сосредоточено ровно в одной стадии — седьмой. Всё остальное у двух функций совпадает буквально построчно, вплоть до общего хелпера multidisorder_send, который и разворачивает порядок отправки.
Псевдокод алгоритма
Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в жизненный цикл desync-функции:
function multidisorder(ctx, desync)
-- 1. Проверка: только TCP
if not desync.dis.tcp then
if not desync.dis.icmp then instance_cutoff_shim() end
return
end
-- 2. Cutoff противоположного направления
direction_cutoff_opposite(ctx, desync)
-- 3. Проверка optional blob
if optional and blob specified and blob not exists then
DLOG("blob not found. skipped")
return
end
-- 4. Выбор данных
data = blob_or_def(blob) or reasm_data or dis.payload
-- 5. Проверки: данные не пусты, направление OK, payload OK
if #data > 0 and direction_check() and payload_check() then
-- 6. Только первый replay
if replay_first() then
-- 7. Разрешение маркеров позиций
pos = resolve_multi_pos(data, l7payload, pos_arg or "2")
delete_pos_1(pos) -- удалить Lua-позицию 1 (маркер 0)
if #pos > 0 then
-- 8. Разрешение seqovl (МАРКЕР, не число!)
seqovl = nil
if arg.seqovl then
seqovl = resolve_pos(data, l7payload, arg.seqovl)
if not seqovl then
DLOG("seqovl cancelled: marker not resolved")
end
end
-- 9. multidisorder_send: цикл ОБРАТНЫЙ
for i = #pos, 0, -1 do -- <--- от последнего к первому!
pos_start = pos[i] or 1
pos_end = (i < #pos) and pos[i+1]-1 or #data
part = data:sub(pos_start, pos_end)
-- 10. seqovl только для i==1 (предпоследний отправляемый)
ovl = 0
if i == 1 and seqovl and seqovl > 0 then
if seqovl >= pos[1] then
DLOG("seqovl cancelled: not less than first split pos")
else
ovl = seqovl - 1 -- ВАЖНО: минус 1!
pat = seqovl_pattern_blob or "\x00"
part = pattern(pat, 1, ovl) .. part
end
end
-- 11. Отправка с автосегментацией
rawsend_payload_segmented(part, pos_start - 1 - ovl)
end
-- 12. Пометить как отправленное
replay_drop_set()
return nodrop and VERDICT_PASS or VERDICT_DROP
else
DLOG("no valid split positions")
end
else
-- 13. Не первый replay
DLOG("not acting on further replay pieces")
end
-- 14. Дропнуть если ранее успешно отправлено
if replay_drop() then
return nodrop and VERDICT_PASS or VERDICT_DROP
end
end
endНюансы и подводные камни
1. Работает только с TCP
Если текущий пакет не TCP (UDP, ICMP и т.д.), multidisorder делает instance_cutoff_shim — отключает себя для этого потока навсегда (кроме ICMP-ответов, для которых cutoff не вызывается).
2. Разрез в самом начале данных отбрасывается
delete_pos_1(pos) убирает из списка Lua-позицию 1 — она соответствует маркеру 0, то есть разрезу перед первым байтом, от которого первый сегмент вышел бы пустым. Если после этой чистки не осталось ни одной позиции, multidisorder не делает ничего.
Путаться тут легко из-за разной нумерации: в командной строке маркеры считаются от нуля, внутри Lua — от единицы. Поэтому под удаление попадает не pos=1, а pos=0. Самая частая реальная жертва — pos=method для HTTP: метод обычно лежит на смещении 0, маркер разрешается в удаляемую позицию, и разреза не происходит. Рабочий вариант — pos=method+2.
3. Все маркеры могут не разрешиться
Если вы указали pos=midsld,sniext для HTTP-payload, оба маркера (специфичные для TLS) не разрешатся. Multidisorder напишет в лог “no valid split positions” и ничего не сделает.
4. seqovl как маркер тоже может не разрешиться
В отличие от multisplit (где seqovl — число и всегда валидно), в multidisorder seqovl=midsld-1 может не разрешиться (например, для unknown payload). В этом случае seqovl отменяется (логируется), но сегментация в обратном порядке всё равно происходит.
5. seqovl НЕ работает на Windows-серверах
Если целевой сервер работает под Windows, seqovl бесполезен: Windows сохраняет первые полученные данные и не перезаписывает буфер при получении перекрывающихся сегментов. Результат — сервер получит мусор (seqovl_pattern) вместо реальных данных. Без seqovl multidisorder работает на всех ОС нормально.
6. ovl = seqovl - 1: размер перекрытия на 1 меньше
Из-за строки ovl = seqovl - 1 в multidisorder_send, если seqovl разрешился в 1, то ovl будет 0 — фактически seqovl не применяется. Минимальный эффективный seqovl — значение 2 (ovl = 1, 1 байт перекрытия).
7. seqovl >= pos[1] → отмена seqovl
Если разрешённое значение seqovl больше или равно первой позиции разреза, seqovl отменяется. Это частая ошибка при использовании маркеров, которые разрешаются в большие значения. Логируется как “seqovl cancelled because seqovl N is not less than the first split pos M”.
8. nodrop создаёт дублирование
С nodrop multidisorder отправляет нарезанные сегменты И пропускает оригинальный пакет. Сервер получит данные дважды. Используйте nodrop только для отладки или когда это осознанно нужно.
9. Fooling применяется ко ВСЕМ сегментам
В отличие от fakeddisorder, где fooling идёт только на фейки, в multidisorder все сегменты получают fooling. Если задать tcp_ack=-66000, все сегменты получат инвалидный ack — сервер их отбросит, и ничего не заработает. Fooling в multidisorder имеет смысл только для специфических вещей (например, tcp_ts_up, ip_id, IPv6 extension headers).
10. Алгоритм отличается от nfqws1
Из комментария в коде: “algorithm is not 100% the same as in nfqws1. multi-segment queries can produce different segment ordering.” nfqws2 работает с целым reasm, а nfqws1 — по отдельным частям replay. Для полной совместимости с nfqws1 используйте multidisorder_legacy.
11. Порядок инстансов важен
Если перед multidisorder стоит fake, он отправит фейк первым. Если после multidisorder стоит ещё один инстанс — он увидит VERDICT_DROP и не получит оригинальный payload.
Отличия от других функций сегментации
| Аспект | multisplit | multidisorder | fakedsplit | fakeddisorder |
|---|---|---|---|---|
| Количество позиций | Список (любое кол-во) | Список (любое кол-во) | Одна | Одна |
| Порядок отправки | Прямой (1→2→3) | Обратный (3→2→1) | Прямой | Обратный |
| Фейковые сегменты | Нет | Нет | Да (до 4 шт.) | Да (до 4 шт.) |
| seqovl тип | Только число | Маркер | Только число | Маркер |
| seqovl к какому сегменту | 1-й (первый отправляемый, i==0) | 2-й в оригинале (предпоследний отправляемый, i==1) | 1-й реальный | 2-й реальный |
| seqovl механизм | Выход за TCP window | Перезапись буфера | Выход за TCP window | Перезапись буфера |
| Windows-серверы (seqovl) | Работает | Не работает | Работает | Не работает |
| Fooling к | Всем сегментам | Всем сегментам | Только к фейкам | Только к фейкам |
| ipfrag | Да | Да | Нет | Нет |
| blob аргумент | Да | Да | Нет | Нет |
| reasm | Целый reasm на replay_first | Целый reasm на replay_first | По частям | По частям |
Миграция с nfqws1
Соответствие параметров
| nfqws1 | nfqws2 |
|---|---|
--dpi-desync=multidisorder | --lua-desync=multidisorder |
--dpi-desync-split-pos=midsld | :pos=midsld |
--dpi-desync-split-pos=1,midsld | :pos=1,midsld |
--dpi-desync-split-seqovl=5 | :seqovl=5 |
--dpi-desync-split-seqovl-pattern=0x1603030000 | :seqovl_pattern=0x1603030000 |
--dpi-desync-any-protocol | Не нужно; или payload=all в инстансе |
Простая миграция
# nfqws1:
nfqws --dpi-desync=multidisorder --dpi-desync-split-pos=midsld
# nfqws2:
nfqws2 --lua-desync=multidisorder:pos=midsldМиграция с fake + multidisorder
# nfqws1:
nfqws --dpi-desync=fake,multidisorder \
--dpi-desync-split-pos=1,midsld \
--dpi-desync-repeats=11 \
--dpi-desync-fooling=md5sig
# nfqws2:
nfqws2 \
--lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
--lua-desync=multidisorder:pos=1,midsldМиграция с seqovl
# nfqws1:
nfqws --dpi-desync=multidisorder \
--dpi-desync-split-pos=midsld \
--dpi-desync-split-seqovl=5 \
--dpi-desync-split-seqovl-pattern=0x1603030000
# nfqws2 (seqovl теперь может быть маркером!):
nfqws2 --lua-desync=multidisorder:pos=midsld:seqovl=5:seqovl_pattern=0x1603030000
# или с маркером (новая возможность nfqws2):
nfqws2 --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000Полная миграция: fake + disorder + seqovl + fooling
# nfqws1:
nfqws --dpi-desync=fake,multidisorder \
--dpi-desync-fooling=md5sig \
--dpi-desync-split-pos=1,midsld \
--dpi-desync-split-seqovl=5 \
--dpi-desync-split-seqovl-pattern=0x1603030000 \
--dpi-desync-fake-tls-mod=rnd,rndsni,dupsid
# nfqws2 (эквивалент):
nfqws2 \
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--payload=http_req \
--lua-desync=fake:blob=fake_default_http:tcp_md5 \
--payload=tls_client_hello,http_req \
--lua-desync=multidisorder:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000Замечание о совместимости: алгоритм multidisorder в nfqws2 не на 100% совпадает с nfqws1 при многопакетных запросах (multi-segment queries). nfqws2 работает с целым reasm, а nfqws1 — по частям. Для полной совместимости используйте multidisorder_legacy.
Практические примеры
1. Минимальный (дефолт: pos=2, dir=out, payload=known)
--lua-desync=multidisorderРазрезает payload после 2-го байта → 2 сегмента (первые 2 байта и остаток), отправляемых в обратном порядке.
2. TLS: разрез посередине SNI
--payload=tls_client_hello --lua-desync=multidisorder:pos=midsldSNI разрезан пополам, хвост отправлен первым — DPI не может собрать домен.
3. TLS: разрез + seqovl маркером (классическая комбинация)
--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1Разрез по midsld, seqovl перекрывает почти всю первую часть фейковыми данными. Типичный паттерн для disorder.
4. TLS: seqovl + кастомный паттерн
--payload=tls_client_hello \
--lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000seqovl-область заполнена данными, похожими на начало TLS record — DPI может принять за легитимный TLS-трафик.
5. HTTP: разрез после метода (disorder)
--payload=http_req --lua-desync=multidisorder:pos=method+2Для GET /path... разрежет после GE → хвост уйдёт первым, голова — вторым.
6. HTTP: несколько разрезов вокруг hostname
--payload=http_req --lua-desync=multidisorder:pos=host,midsld,endhostРазрезает: до hostname | первая половина | вторая половина | после hostname → 4 сегмента в обратном порядке.
7. Два разреза с seqovl
--payload=tls_client_hello --lua-desync=multidisorder:pos=2,midsld:seqovl=sld3 сегмента, seqovl применяется к сегменту i=1 (второй в оригинале). Маркер sld определяет размер перекрытия.
8. Произвольный blob вместо payload
--blob=mydata:@custom_payload.bin \
--lua-desync=multidisorder:blob=mydata:pos=10,100,-20Режет и отправляет произвольные данные из файла вместо реального payload, в обратном порядке.
9. Защита от отсутствующего blob
--lua-desync=multidisorder:blob=maybe_missing:optional:pos=midsldЕсли blob не существует — тихий пропуск, без ошибок и без VERDICT_DROP.
10. Отладка: не блокировать оригинал
--payload=http_req --lua-desync=multidisorder:pos=host,midsld:nodropОтправляет нарезанные сегменты в обратном порядке И пропускает оригинальный пакет (для экспериментов).
11. IP-фрагментация поверх TCP-сегментации (двойной disorder)
--payload=tls_client_hello \
--lua-desync=multidisorder:pos=1,midsld:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32Каждый TCP-сегмент дополнительно фрагментируется на IP-уровне в обратном порядке. Двойной disorder: TCP-сегменты в обратном порядке + IP-фрагменты каждого сегмента тоже в обратном порядке.
12. Комбинация: fake → multidisorder (типичный боевой профиль)
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--lua-desync=multidisorder:pos=1,midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000Сначала отправляется фейковый TLS ClientHello (с fooling), затем реальный — нарезанный на 3 сегмента в обратном порядке с seqovl.
13. Боевой пример для YouTube
--filter-tcp=443 --hostlist=youtube.txt \
--lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
--lua-desync=multidisorder:pos=1,midsld11 фейков подряд + реальный payload разрезан на 3 части в обратном порядке.
14. С TCP timestamp + IP ID
--payload=tls_client_hello --lua-desync=multidisorder:pos=1:tcp_ts_up:ip_id=seq:ip_id_conn15. Повторы отправки
--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:repeats=2Каждый сегмент отправляется 2 раза (бинарные повторы).
Источники:
lua/zapret-antidpi.lua:546-629,lua/zapret-lib.lua,docs/manual.md:4067-4097,docs/readme.mdиз репозитория zapret2.