Атаки по сторонним каналам
Почему шифр можно взломать, ничего не зная о ключе, — просто измеряя, сколько времени работает код.
Атака по стороннему каналу (side-channel) — извлечение секрета не из самих данных, а из побочных физических проявлений вычисления: времени работы, энергопотребления, электромагнитного излучения, звука.
Криптография обычно доказывает стойкость в идеальном мире, где у атакующего есть только вход и выход алгоритма. В реальности у программы есть ещё и поведение: она тратит время, потребляет ток, обращается к кэшу. Если это поведение зависит от секрета, секрет частично утекает наружу. Именно поэтому корректный по математике AES можно скомпрометировать, не атакуя сам шифр.
Зачем это знать защитнику
Side-channel — это класс уязвимостей, который не виден в обычном код-ревью и не ловится функциональными тестами: код даёт правильный ответ, проходит все проверки, но при этом «нашёптывает» секрет тому, кто умеет слушать. Защитник обязан понимать механизм, чтобы выбирать реализации, не зависящие от секретных данных по времени, и чтобы грамотно оценивать риск (например, серверный API, отвечающий на запросы из интернета, уязвимее изолированной офлайн-системы).
Тайминг-атаки: утечка через время
Классический пример — наивное сравнение секрета (токена, MAC, пароля) побайтно с ранним выходом. Как только встретился первый несовпадающий байт, функция возвращает false. Значит, для «почти правильного» значения, у которого совпадает больше первых байтов, проверка длится чуть дольше. Многократно измеряя это время, атакующий может подбирать секрет по одному байту, превращая перебор из астрономического в линейный.
Посмотрите на разницу между опасным и безопасным сравнением. Наивная версия выходит раньше, постоянная по времени — всегда проходит всю длину:
import secrets
# Наивно: ранний выход на первом несовпадении -> время зависит от секрета
def naive_equal(a: bytes, b: bytes) -> bool:
if len(a) != len(b):
return False
for x, y in zip(a, b):
if x != y:
return False # утечка: разное число итераций
return True
# Постоянное по времени: копим разницу через XOR, обходим всю длину
def constant_time_equal(a: bytes, b: bytes) -> bool:
if len(a) != len(b):
return False
result = 0
for x, y in zip(a, b):
result |= x ^ y
return result == 0
real = b"super-secret-token"
print("naive верный токен: ", naive_equal(real, b"super-secret-token"))
print("naive неверный токен:", naive_equal(real, b"wrong-token-aaaaaaa"))
print("CT верный токен: ", constant_time_equal(real, b"super-secret-token"))
print("CT неверный токен: ", constant_time_equal(real, b"wrong-token-aaaaaaa"))
# В реальном коде берите готовое:
print("secrets.compare_digest:", secrets.compare_digest(real, real))
Вывод:
naive верный токен: True naive неверный токен: False CT верный токен: True CT неверный токен: False secrets.compare_digest: True
Функционально обе версии эквивалентны — отличается лишь профиль времени. Поэтому глазами баг не заметить: нужно знать о нём заранее и сразу выбирать безопасный примитив.
Атаки по энергопотреблению
На уровне железа (смарт-карты, аппаратные токены, IoT-устройства) применяют ещё более тонкие методы. Простой анализ потребления (SPA) различает операции по форме графика тока, а дифференциальный анализ (DPA) статистически коррелирует тысячи измерений с гипотезами о битах ключа. Аналогично работают электромагнитные и акустические каналы. Здесь защита — задача в первую очередь производителя устройства (маскирование, выравнивание, экранирование), но разработчику прикладного ПО важно понимать: если атакующий имеет физический доступ к устройству, угроза реальна.
Важная оговорка про модель угроз: не всякий тайминг-канал одинаково опасен. Различие в наносекунды через сеть тонет в шуме задержек, тогда как локальный атакующий на той же машине (другой процесс, виртуальная машина по соседству, разделяемый кэш) измеряет гораздо точнее. Но полагаться на «сеть зашумит утечку» нельзя: техники усреднения и статистики научились вытаскивать сигнал даже из удалённых измерений, а облачные среды сближают атакующего и жертву физически. Поэтому правильный подход — закрывать канал в коде, а не надеяться на внешний шум.
Где это встречается на практике
Тайминг-уязвимости живут в самых обыденных местах: проверка HMAC-подписи вебхука, сверка API-ключа или CSRF-токена, валидация кода сброса пароля, сравнение хеша сессии. Везде, где код сопоставляет присланное пользователем значение с секретом и где-то отвечает «да/нет», стоит спросить: «сравниваю ли я это constant-time?». Особенно коварны самописные «проверки подписи» в интеграциях — частый источник реальных утечек, потому что выглядят они безобидно.
Как это работает под капотом
Корень проблемы — ветвления и обращения к памяти, зависящие от секрета. Процессор выполняет if secret_bit: за разное число тактов в зависимости от значения бита; кэш отвечает быстрее на адрес, который недавно читали (отсюда кэш-тайминг-атаки на табличные реализации шифров). Любая величина, которая физически зависит от секрета и при этом наблюдаема снаружи, — это канал утечки. Цель constant-time-программирования — разорвать эту зависимость: время и шаблон доступа к памяти не должны меняться от секретных данных.
Как защититься
- Используйте constant-time-примитивы. Сравнивайте секреты через
secrets.compare_digest(Python),hmac.compare_digest,crypto.timingSafeEqual(Node.js),subtle.ConstantTimeCompare(Go). Никогда не сравнивайте токены и MAC обычным==. - Не ветвитесь по секрету. Избегайте
if, раннихreturnи индексации массива по секретным значениям в криптокоде. Маскируйте выбор арифметикой/битовыми операциями вместо переходов. - Берите проверенные библиотеки. libsodium, BoringSSL, реализации из
cryptography(Python) уже написаны как constant-time. Свой AES «для скорости» почти наверняка получится уязвимым. - Снижайте observability. На сервере выравнивайте время ответа на ошибочные и успешные запросы, добавляйте rate-limiting — это поднимает стоимость измерений для атакующего.
- Учитывайте модель угроз. Если возможен физический доступ (смарт-карты, токены) — нужны аппаратные контрмеры. Выбирайте сертифицированное железо там, где это критично.
Правовое напоминание: исследовать side-channel допустимо только на собственных или явно разрешённых системах. Несанкционированный доступ и нарушение работы чужих систем наказуемы (УК РФ ст. 272–274).
Итоги
- Стойкий алгоритм не гарантирует стойкой реализации: секрет может утекать через время, ток, кэш.
- Тайминг-атаки превращают перебор секрета в линейный, если сравнение выходит раньше на несовпадении.
- Корень проблемы — ветвления и доступы к памяти, зависящие от секретных данных.
- Защита: constant-time-сравнение, отказ от ветвлений по секрету, проверенные библиотеки, выравнивание времени ответа.