Пакет из ниоткуда: подделка пакетов и атаки на 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-инъекция — самый дешёвый способ оборвать соединение: достаточно отправить пакет с правильными адресами, портами и номером последовательности. Если атакующий на пути, номер известен из захвата; если вне пути, его нужно угадать.
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 | смена маршрута по ICMP | accept_redirects=0 |
| SYN-флуд | рост полуоткрытых соединений | SYN cookies, backlog |
| RST-инъекция | RST с неверным seq | RFC 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 доверяет объявлению.
Литература
- Postel J. Internet Protocol. — RFC 791, IETF, 1981. https://www.rfc-editor.org/rfc/rfc791
- Postel J. Internet Control Message Protocol. — RFC 792, IETF, 1981. https://www.rfc-editor.org/rfc/rfc792
- Eddy W. Transmission Control Protocol (TCP). — RFC 9293, IETF, 2022. https://www.rfc-editor.org/rfc/rfc9293
- Jacobson V., Braden R., Borman D. Computing TCP's Initial Sequence Number. — RFC 6528, IETF, 2012. https://www.rfc-editor.org/rfc/rfc6528
- Eddy W. Defenses Against TCP SYN Flooding Attacks. — RFC 4987, IETF, 2007. https://www.rfc-editor.org/rfc/rfc4987
- Ferguson P., Senie D. Network Ingress Filtering. — RFC 2827, IETF, 2000. https://www.rfc-editor.org/rfc/rfc2827
- Ziemba G., Reed D., Traina P. Security Considerations for IP Fragment Filtering. — RFC 1858, IETF, 1995. https://www.rfc-editor.org/rfc/rfc1858
- 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