Атаки по времени: почему ваш код может раскрывать секреты

Вы когда-нибудь слышали об атаках по сторонним каналам? Это категория атак, в которых злоумышленники вместо прямого взлома криптографии анализируют непреднамеренную информацию, "утекающую" из реализации системы во время её обычной работы.
Атаки по сторонним каналам опасны — даже идеально реализованная криптография может быть уязвимой из-за утечки информации из физической реализации криптографических систем. Они могут быть незаметны для стандартных аудитов безопасности, поскольку для их понимания и предотвращения требуются междисциплинарные знания.
Существует несколько типов таких атак: атаки по времени, атаки по времени кэша, атаки на основе анализа энергопотребления и другие.
Атаки по времени являются одними из самых распространенных и доступных атак на сторонние каналы, потому что:
- Их можно выполнять удаленно через сети.
- Они не требуют физического доступа к оборудованию.
- Их можно осуществить с помощью стандартных программных инструментов.
- Они часто напрямую затрагивают веб-приложения и API.
Понимание атак по времени
Вместо того чтобы атаковать математическое решение ваших алгоритмов шифрования, эти атаки нацелены на то, как ваш код выполняется в реальных условиях, позволяя злоумышленникам извлекать пароли, ключи и другие конфиденциальные данные, просто измеряя время отклика. Атаки по времени создают уникальные проблемы, потому что могут полностью обойти традиционные меры безопасности.
В 2003 году исследователи Дэвид Брамли и Дэн Боне продемонстрировали извлечение полных 1024-битных приватных ключей RSA с SSL-серверов по сети, используя только анализ времени, что потребовало около миллиона запросов в течение двух часов. Их атака сработала несмотря на джиттер в сети и доказала, что атаки по времени представляют реальную угрозу для реальных систем.
Основной принцип обманчиво прост: время выполнения часто коррелирует со свойствами секрета, такими как значение и длина.
Например, рассмотрим это условие:
javascript1'HELLO' === 'AAAA';
Технически оно уязвимо, потому что движки JavaScript (V8, SpiderMonkey) реализуют сравнение строк с ранним выходом для повышения производительности.
Сравнение строки "AAAA" со строкой "HELLO" завершается неудачей на первом символе (самый быстрый вариант), "HAAA" — на втором (немного медленнее), а "HEAA" — на третьем (еще медленнее). Злоумышленник может определять правильный символ за символом, сокращая пространство поиска с экспоненциальной до линейной сложности.
Это важно — это не практический пример, потому что:
- Разница во времени крошечная — наносекунды, а не миллисекунды.
- Сетевой джиттер заглушает эти небольшие различия.
- Никто не сравнивает пароли напрямую (я надеюсь).
Рассмотрим более практические примеры.
Реальные атаки по времени используют операции, которые занимают миллисекунды, а не наносекунды. Когда поиск в базе данных или криптографическая операция создают измеримые задержки, злоумышленники могут извлекать секреты даже через зашумленные сети.
Системы аутентификации под ударом
Системы аутентификации представляют собой самую ценную цель для атак по времени в веб-приложениях. Когда аутентификация занимает разное количество времени в зависимости от того, существует ли имя пользователя, или когда ваше сравнение паролей завершается раньше при первом неверном символе, вы транслируете конфиденциальную информацию через временные паттерны.
javascript1function authenticate(username, password) {2 const user = database.getUser(username);34 if (!user) {5 return false; // Быстрый возврат — серьезная утечка времени6 }78 return bcrypt.compare(password, user.hashedPassword); // Медленная операция9}
Когда предоставляется неверное имя пользователя, функция немедленно возвращает результат. Для действительных имен пользователей выполняется дорогостоящая операция хеширования bcrypt.compare()
, создавая разницу во времени, которую легко обнаружить через сетевые соединения.
Злоумышленники могут составить полные списки действительных имен пользователей перед запуском целенаправленных атак на пароли.
Ограничение частоты запросов API и анализ времени
Системы ограничения частоты запросов (rate limiting), предназначенные для предотвращения злоупотреблений, часто сами становятся векторами атак по времени. Обнаружение ограничения частоты запросов на основе времени позволяет злоумышленникам определять пороги и разрабатывать стратегии обхода:
javascript1// Ограничение частоты запросов создает временные паттерны2app.use(3 rateLimit({4 windowMs: 15 * 60 * 1000, // 15 минут5 max: 100, // Лимит запросов на окно6 handler: (req, res) => {7 res.status(429).send('Too many requests'); // Немедленный ответ8 },9 })10);1112// Обычный запрос: ответ за 200-500 мс13// Запрос с превышением лимита: ответ за <10 мс (утечка времени)
Запросы, превысившие лимит, обычно немедленно возвращают код ошибки, в то время как обычные запросы проходят полную обработку. Эта разница во времени позволяет злоумышленникам обнаруживать активацию ограничения и соответствующим образом корректировать свои схемы атак.
Что делать с атакой на системы ограничения частоты запросов? Вы можете встретить предложения скрыть лимиты. Потому что видимые лимиты позволяют злоумышленникам:
- Определять точные пороги (например, «Сделать ровно 99 запросов, прежде чем сработает лимит»).
- Оптимизировать схемы атак, чтобы оставаться чуть ниже лимитов.
- Более эффективно переключаться между разными IP-адресами/сессиями.
- Идеально подгадывать время своих атак к сбросу окон.
Идея в том, что неопределенность заставляет злоумышленников быть более осмотрительными. Однако это обычно считается «безопасностью через неясность» и не очень эффективно — злоумышленники все равно могут обнаружить лимиты путем проб.
Я бы предложила рассматривать лимиты как публичную информацию. Они просто должны быть разумными, но не обязательно секретными.
Больше о системах ограничения запросов вы можете узнать в статье Системы ограничения запросов на бэкенде
Современные стратегии защиты и контрмеры
Защита от атак по времени требует многоуровневого подхода, сочетающего программирование с постоянным временем выполнения, архитектурные защиты и системы мониторинга. Основной принцип заключается в том, чтобы время выполнения оставалось независимым от секретных данных.
Функции сравнения с постоянным временем выполнения
Функции сравнения с постоянным временем выполнения являются краеугольным камнем защиты от атак по времени. Современные платформы предоставляют встроенные функции безопасного по времени сравнения.
Трюк с фиктивным хешем для паролей: Сделайте так, чтобы ваша точка входа для логина всегда вызывала медленную проверку пароля, независимо от того, существует ли имя пользователя или нет, чтобы злоумышленник не мог с помощью времени определить, «существует ли этот пользователь?».
javascript1// Предварительно вычислите фиктивный хеш один раз при запуске:2const DUMMY_HASH = '$2b$12$K9QhMWzVN5YbRz5sXl0ueODhk0PtAyp7cVt2hx6Vj7XOx0JPTWm6W';34// В вашем обработчике входа всегда выполняйте bcrypt.compare:5async function login(username, password) {6 const user = await db.findUser(username);7 // Если пользователь отсутствует, сравниваем с фиктивным хешем8 const hashToCompare = user ? user.hash : DUMMY_HASH;9 const passwordValid = await bcrypt.compare(password, hashToCompare);1011 if (!user || !passwordValid) {12 return { error: 'Invalid credentials' };13 }14 return { token: generateToken(user) };15}
Метод .compare()
из bcrypt уже разработан для выполнения за постоянное время для каждого хеша, но если вы когда-либо сравниваете «сырые» секреты (HMAC, токены, ключи API и т. д.), вы никогда не должны использовать ===
. Вместо этого сделайте что-то вроде этого:
javascript1const { timingSafeEqual } = require('crypto');23function safeCompare(a, b) {4 const bufA = Buffer.from(a);5 const bufB = Buffer.from(b);67 // Если длины различаются, сравните bufA с самим собой (постоянное время)8 // чтобы предотвратить утечку информации о длине секрета9 if (bufA.length !== bufB.length) {10 timingSafeEqual(bufA, bufA);11 return false;12 }1314 // настоящее сравнение с постоянным временем выполнения15 return timingSafeEqual(bufA, bufB);16}
Архитектурные защиты
Архитектурные защиты — это шаблоны проектирования систем, которые устраняют векторы атак по времени путем реструктуризации обработки конфиденциальных операций в вашем приложении. Вместо того чтобы исправлять утечки времени в коде, вы перепроектируете систему так, чтобы информация о времени становилась бессмысленной для злоумышленников.
Ключевые архитектурные паттерны:
- Разделение запроса и ответа
- Стратегия кэширования ответов
- Унифицированные конвейеры обработки
- Сегрегация сервисов
Рассмотрим подробнее разделение запроса и ответа:
javascript1// app/api/auth/forgot-password/route.js2import { addToQueue } from '@/lib/queue';34export async function POST(request) {5 const { email } = await request.json();67 // Поместить задачу в очередь вместо немедленной обработки8 addToQueue('password-reset', {9 email,10 requestId: crypto.randomUUID(),11 timestamp: Date.now(),12 });1314 // Немедленно вернуть ответ15 return Response.json({16 message: 'Если учетная запись существует, вы получите инструкции по сбросу',17 });18}1920/** Фоновый воркер обрабатывает очередь (выполняется отдельно) **/21// lib/workers/password-reset.js22export async function processPasswordReset({ email }) {23 const user = await findUserByEmail(email);2425 if (user) {26 await sendPasswordResetEmail(user);27 }28 // Никакая информация о времени не утекает к исходному запрашивающему29}
Системы мониторинга
Системы мониторинга — это инструменты обнаружения и анализа, которые выявляют попытки атак по времени, анализируя паттерны запросов, время отклика и поведенческие аномалии. В отличие от систем ограничения частоты запросов, которые предотвращают запросы, системы мониторинга предназначены для обнаружения. Они наблюдают и оповещают о подозрительных паттернах, не блокируя трафик, что позволяет анализировать потенциальные атаки.
Начните защищать свой код уже сегодня
Защита от атак по времени начинается с трех простых изменений в вашей кодовой базе:
- Переключите все сравнения секретов на функции, безопасные по времени.
- Реализуйте фиктивную обработку для неудачных попыток аутентификации.
- Используйте библиотеки со встроенной защитой от атак по времени.
Атаки по времени коварны, но реальны. Они не проявятся в ваших юнит-тестах или сканированиях безопасности. Лучшая защита — это понимание того, как они работают, и встраивание защиты в ваш код с самого начала путем учета атак по времени при принятии архитектурных решений, в процессах разработки и в системах мониторинга.
Заключение
Хотя фундаментальные принципы атак не изменились со времен работы Пола Кохера в 1996 году, сегодня атаки по времени затрагивают гораздо более широкий спектр приложений — от JavaScript в браузере до бэкенд-сервисов.
Успех в защите от атак по времени требует сочетания нескольких уровней защиты: реализации с постоянным временем выполнения на уровне кода, архитектурных защит, таких как ограничение частоты запросов и нормализация ответов, всестороннего мониторинга для обнаружения атак и регулярных оценок безопасности для выявления новых уязвимостей. Систематически внедряя эти меры защиты, команды разработчиков могут создавать приложения, устойчивые даже к сложным атакам по времени, сохраняя при этом производительность и функциональность, которые ожидают пользователи.