Апокриф 04

Пакет из ниоткуда: подделка пакетов и атаки на TCP/IP

Пакет из ниоткуда: подделка пакетов и атаки на TCP/IP

Сетевой уровень устроен так, что отправитель указывает в заголовке всё, что получатель потом использует: адрес источника, длину, идентификатор, контрольную сумму 1. Ни одно из этих полей не проверяется криптографически. Пока пакет идёт по сети, единственная защита от подделки — фильтрация на входе у провайдера, и она включается по доброй воле.

Отсюда два класса приёмов. Первый — подделка источника: пакет выглядит так, будто его отправил кто-то другой, и ответы уходят жертве или в никуда. Второй — атаки на состояние: TCP хранит номера последовательностей и окна, и если угадать или подменить их, можно вклиниться в чужое соединение, не видя его целиком.

Тема 07 основной программы разбирала заголовок IP, фрагментацию и ICMP; тема 12 — TCP и сокеты; тема 13 — перегрузку и SYN cookies. Здесь те же механизмы рассматриваются как поверхность подделки. Границы: сканирование — тема 01, DNS — тема 05.

Тень базового курса: поля, которым доверяют

В теме 07 заголовок IPv4 разбирался по полям: версия, длина, идентификатор, флаги, смещение, TTL, протокол, контрольная сумма, адреса. Каждое поле существует, чтобы получатель мог обработать пакет; ни одно не удостоверяет отправителя.

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

TCP добавляет состояние: номера последовательностей, подтверждения, окно 3. Это состояние — единственное, что мешает вклиниться в соединение. Поэтому атаки на TCP сводятся к одному: узнать или угадать номер, который ожидает получатель.

Подделка пакета

Raw-сокеты и AF_PACKET

Обычный сокет SOCK_STREAM или SOCK_DGRAM не даёт контролировать IP-заголовок: ядро само заполняет адреса и контрольные суммы. Для полной подделки нужен SOCK_RAW с IP_HDRINCL или AF_PACKET, который работает ниже IP и позволяет собрать кадр целиком.

import socket, struct

def checksum(data: bytes) -> int:
    if len(data) % 2:
        data += b"\x00"
    s = sum(struct.unpack("!%dH" % (len(data) // 2), data))
    s = (s >> 16) + (s & 0xffff)
    s += s >> 16
    return ~s & 0xffff

src = socket.inet_aton("192.0.2.66")
dst = socket.inet_aton("192.0.2.10")
ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + 8, 0x1234, 0, 64, 1, 0, src, dst)
ip = ip[:10] + struct.pack("!H", checksum(ip)) + ip[12:]
icmp = struct.pack("!BBHHH", 8, 0, 0, 0x0001, 0) + b"hello"
icmp = icmp[:2] + struct.pack("!H", checksum(icmp)) + icmp[4:]

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
s.sendto(ip + icmp, ("192.0.2.10", 0))

Разбор: IPPROTO_RAW с IP_HDRINCL означает «заголовок уже собран»; контрольная сумма IP считается по заголовку с обнулённым полем; ICMP-сумма — по всему сообщению. Ядро не исправит подделанный адрес источника, потому что не считает его своим делом.

scapy делает то же короче и с автоматическим расчётом сумм, но под капотом — те же сокеты:

from scapy.all import IP, ICMP, send
send(IP(src="192.0.2.66", dst="192.0.2.10") / ICMP() / b"hello")

Контрольные суммы и TTL

Неверная контрольная сумма — не ошибка, а приём. Пакет с испорченной суммой отбрасывается стеком, но доходит до межсетевого экрана и IDS, которые могут его залогировать. Это позволяет проверять правила фильтрации, не получая ответа от цели: экран отвечает ICMP или молчит в зависимости от правила.

TTL используется иначе. Отправив пакет с TTL, равным расстоянию до цели, можно получить ICMP Time Exceeded от нужного маршрутизатора. Это основа firewalk из темы 01 и способ определить, где именно стоит фильтр.

BCP 38 и почему он не везде

RFC 2827 (BCP 38) требует, чтобы провайдер отбрасывал пакеты с адресом источника, не принадлежащим его клиенту 6. Если бы это правило выполнялось повсеместно, подделка источника за пределами своей сети стала бы невозможной.

На практике фильтрация включена не везде: её нужно настраивать, она добавляет нагрузку, а в некоторых сетях ломает легитимные сценарии. Поэтому подделка работает, и защита строится на предположении, что источник может быть любым: uRPF на маршрутизаторах, rp_filter на хостах, лимиты на входе.

sysctl net.ipv4.conf.all.rp_filter
sysctl -w net.ipv4.conf.all.rp_filter=1

rp_filter=1 (strict) отбрасывает пакет, если обратный маршрут к его источнику не совпадает с интерфейсом, на который он пришёл. Это ломает асимметричную маршрутизацию, поэтому в сложных сетях используют 2 (loose).

Фрагментация как оружие

Перекрывающиеся фрагменты и teardrop

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

from scapy.all import IP, UDP, fragment, send

payload = b"A" * 64
pkts = fragment(IP(dst="192.0.2.10") / UDP(dport=53) / payload, fragsize=16)
send(pkts)

Перекрытие создаётся вручную: первый фрагмент объявляет длину больше, чем несёт, второй начинается внутри первого. Историческая атака teardrop использовала именно это: смещения подбирались так, что сборка ломала стек. Современные ядра устойчивы, но промежуточные устройства — нет.

Правило RFC 1858 описывает две атаки на фрагментацию: tiny fragment (первый фрагмент слишком мал, чтобы содержать TCP-заголовок) и перекрытие 7. Обе обходят фильтры, которые смотрят только первый фрагмент.

Tiny fragment и stateless-ACL

Stateless-фильтр проверяет порты в TCP-заголовке. Если первый фрагмент содержит только часть заголовка, фильтр не может принять решение и часто пропускает пакет целиком, надеясь на сборку позже. Сборка может не произойти вовсе, если фрагменты идут в разном порядке и часть отбрасывается.

Защита: собирать фрагменты до фильтрации, ограничивать их количество и размер, отбрасывать перекрытия. В Linux это net.ipv4.ipfrag_high_thresh и поведение conntrack; в промышленных экранах — отдельная функция defragmentation.

PMTUD-чёрная дыра как отказ в обслуживании

Path MTU Discovery полагается на ICMP «Fragmentation needed»: отправитель уменьшает размер, получив сообщение. Если ICMP заблокирован, крупные пакеты молча теряются, и соединение «зависает» на передаче файла или TLS-рукопожатии.

ping -M do -s 1472 -c 1 203.0.113.10
ping -M do -s 1400 -c 1 203.0.113.10

Первая команда отправляет пакет 1500 байт с DF=1; если путь уже, ответа не будет. Вторая — 1428 байт, проходит. Так находится реальный MTU. Атакующий может использовать ту же механику наоборот: заблокировать ICMP на пути, чтобы уронить крупные сессии, не отправляя ни одного пакета от своего имени.

ICMP как инструмент

ICMP — не только диагностика 2. Redirect сообщает хосту, что маршрут надо изменить; поддельный redirect от имени шлюза переключает трафик на атакующего. Destination Unreachable с кодом «fragmentation needed» управляет размером пакетов; поддельный unreachable обрывает соединение.

sysctl net.ipv4.conf.all.accept_redirects
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0

ICMP-туннель — отдельный приём: данные кодируются в эхо-запросах и ответах, и трафик выглядит как диагностика. Обнаружение — по размеру и частоте, а не по содержимому.

TCP: последовательности и доверие

Генерация ISN и предсказуемость

Начальный номер последовательности выбирается не случайно в буквальном смысле: RFC 6528 требует, чтобы он был трудно предсказуем и при этом монотонно рос 4. Формула использует секрет, адрес и порт, а также таймер. Если секрет известен или энтропия мала, следующий ISN предсказуем, и появляется возможность слепой инъекции.

sysctl net.ipv4.tcp_syncookies
ss -ti

ss -ti показывает для каждого соединения rtt, cwnd, retrans; по этим числам видно, как стек реагирует на потери. Для атаки важнее другое: номер последовательности, который виден в захвате, если атакующий находится на пути.

SYN-флуд и SYN cookies

SYN-флуд эксплуатирует обязательность состояния: получив SYN, сервер выделяет структуру и ждёт ACK. Тысячи SYN с поддельными адресами заполняют очередь, и новые соединения не принимаются 5.

SYN cookies убирают состояние: сервер кодирует параметры соединения в ISN ответа и восстанавливает их из ACK. Очередь перестаёт быть узким местом, но теряются некоторые TCP-опции.

sysctl net.ipv4.tcp_syncookies
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn

Включение cookies — компромисс: под нагрузкой они спасают доступность, но ломают оконное масштабирование и SACK, если параметры не помещаются в номер.

RST-инъекция и слепая подделка

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

Окно приёма в 32-битном пространстве порядковых номеров TCP
Слепой RST должен попасть в узкое окно приёма, пока оно не сдвинулось
from scapy.all import IP, TCP, send
send(IP(src="192.0.2.10", dst="192.0.2.20") /
     TCP(sport=49152, dport=80, flags="R", seq=123456789))

Современные стеки применяют RFC 5961: RST принимается только в узком окне последовательностей, что снижает эффективность слепой инъекции 8. Но и это не абсолютно: атака на пути по-прежнему работает, а вместе с ней — hijacking, при котором атакующий не обрывает соединение, а вставляет данные.

Окно, SACK и PAWS

Масштабирование окна позволяет получателю объявить окно больше 64 КиБ. SACK сообщает о пропущенных блоках, ускоряя восстановление. PAWS защищает от старых дубликатов по меткам времени. Все три механизма расширяют состояние, которое нужно подделать, и одновременно дают атакующему больше полей для наблюдения.

Обнаружение и защита

ПриёмПризнакКонтрмера
Подделка источникаадрес не принадлежит сетиBCP 38, uRPF, rp_filter
Tiny fragmentпервый фрагмент короче заголовкасборка до фильтрации
Перекрытие фрагментовпротиворечивые смещенияотбрасывание перекрытий
PMTUD-чёрная дыракрупные пакеты теряются, ICMP нетразрешить ICMP type 3 code 4
ICMP redirectсмена маршрута по ICMPaccept_redirects=0
SYN-флудрост полуоткрытых соединенийSYN cookies, backlog
RST-инъекцияRST с неверным seqRFC 5961, подпись TCP (TCP-AO)
Слепая инъекциявсплеск RST/данныхэнтропия ISN, шифрование

Правило nftables, отбрасывающее крошечные первые фрагменты и весь фрагментированный трафик целиком:

nft add table inet frag_guard
nft add chain inet frag_guard input '{ type filter hook input priority -150; policy accept; }'
# крошечный первый фрагмент: MF установлен, смещение 0, нагрузки меньше 8 байт
nft add rule inet frag_guard input "ip frag-off & 0x2000 != 0 ip frag-off & 0x1fff == 0 ip length < 28" drop
# «хвостовые» фрагменты: политика не собирать фрагментированный трафик вовсе
nft add rule inet frag_guard input "ip frag-off & 0x1fff != 0" drop

Имя frag для таблицы не подходит: в nftables это ключевое слово заголовка фрагментации, и nft add table inet frag завершается синтаксической ошибкой. Перекрытие фрагментов таким правилом не ловится в принципе: чтобы увидеть, что два фрагмента претендуют на одно смещение, нужен сборщик, который помнит уже принятые фрагменты, — ядро или Suricata. Статическое правило может только отбросить фрагменты целиком.

Итоги темы

Подделка пакета возможна потому, что заголовок не аутентифицирован. Контрольная сумма защищает от ошибки, а не от злонамеренности; BCP 38 мог бы закрыть подделку источника, но включён не везде. Фрагментация и ICMP дают обходные пути для фильтров, которые смотрят только на первый фрагмент или только на TCP.

TCP держится на состоянии: номерах последовательностей и окнах. Кто знает номер, тот может оборвать соединение; кто угадывает — может попытаться. SYN cookies защищают доступность ценой части функциональности, RFC 5961 сужает окно для RST, но не устраняет атаку на пути.

Следующая тема переходит к именам — и показывает, что DNS доверяет ответу почти так же, как ARP доверяет объявлению.

Литература

  1. Postel J. Internet Protocol. — RFC 791, IETF, 1981. https://www.rfc-editor.org/rfc/rfc791
  2. Postel J. Internet Control Message Protocol. — RFC 792, IETF, 1981. https://www.rfc-editor.org/rfc/rfc792
  3. Eddy W. Transmission Control Protocol (TCP). — RFC 9293, IETF, 2022. https://www.rfc-editor.org/rfc/rfc9293
  4. Jacobson V., Braden R., Borman D. Computing TCP's Initial Sequence Number. — RFC 6528, IETF, 2012. https://www.rfc-editor.org/rfc/rfc6528
  5. Eddy W. Defenses Against TCP SYN Flooding Attacks. — RFC 4987, IETF, 2007. https://www.rfc-editor.org/rfc/rfc4987
  6. Ferguson P., Senie D. Network Ingress Filtering. — RFC 2827, IETF, 2000. https://www.rfc-editor.org/rfc/rfc2827
  7. Ziemba G., Reed D., Traina P. Security Considerations for IP Fragment Filtering. — RFC 1858, IETF, 1995. https://www.rfc-editor.org/rfc/rfc1858
  8. Ramaiah A., Stewart R., Dalal M. Improving TCP's Robustness to Blind In-Window Attacks. — RFC 5961, IETF, 2010. https://www.rfc-editor.org/rfc/rfc5961