Представьте себе сервис (или веб-приложение), который выдаёт вам сообщение вида «пятый символ введённого вами пароля неверный» в ответ на вашу попытку аутентификации. Выглядит абсурдно, не так ли? Предоставляя потенциальному злоумышленнику информацию подобного рода, мы попросту даём ему шанс «сбрутить» (подобрать, методом перебора) пароль от сервиса.
В то же время — это практически то самое событие, которое происходит, когда мы, например, используем наипростейший механизм сравнения строкового типа данных во время сверки паролей или токенов для аутентификации.
Сама по себе «тайминговая атака» или «атака по времени» — это нападение на систему по открытому каналу доступа, когда атакующий пытается скомпрометировать систему с помощью анализа времени, затрачиваемого на исполнение алгоритмов. Каждая операция (особенно математическая, будь то сложение, вычитание, возведение в степень и т.д.) требует определённого времени на исполнение, и это время может различаться в зависимости от входных данных. Располагая точными измерениями времени, которое расходуется на эти операции, злоумышленник может восстановить данные, необходимые для входа в систему.
Кое-что о JavaScript
Возвращаясь к вступлению – механизм сравнения строк в JavaScript, основанный на операторе «===», работает как обычная итерация для строк одинаковой длины, сравнивая строки друг с другом простейшим перебором символов, идя при этом вперёд, если сравниваемая пара символов идентична, и неожиданно останавливаясь, если один из символов в паре различается.
function isAuthenticated(user, token) {
var correctToken = FetchUserTokenFromDB(user);
return token === correctToken;
}
Пример нежелательного кода
Эта операция – быстрая, но в то же время и небезопасная. Исследования показывают, что злоумышленнику не составляет труда измерять отрезки времени от 15 до 100 микросекунд через Интернет и 100 наносекунд через локальную сеть. Иными словами, злоумышленники способны использовать технику столь крошечных временных задержек, словно подсказку – какой символ подошёл, а какой — нет. Чтобы предотвратить развитие подобных сценариев, нам следует реализовать механизм обработки строк таким образом, чтобы его отработка занимала один и тот же промежуток времени, вне зависимости от заданного пароля.
Например, применяя логическую операцию xor для двух паролей, получая при этом на выходе 0.
var mismatch = 0;
for (var i = 0; i <a.lenght; ++i) {
mismatch | = (a.charCodeat(i)) ^ b.charCodeAt(i));
}
return mistmatch;
Что касается Node.js
Сам по себе Node.js был сконструирован как масштабируемый и асинхронный фреймворк. Когда какая-либо часть кода Node.js желает совершить вызов того или иного блокирующего действия (например, открытие файла или запись в сетевой сокет) – она регистрирует функцию обратного вызова (колбэк), запускает соответствующее действие и затем завершает работу. Сам по себе колбэк вызывается при помощи механизма, носящего название “цикл событий” (event loop).
Цикл событий – это то, что позволяет Node.js выполнять неблокирующие операции ввода/вывода (даже несмотря на то, что JavaScript является однопоточным) путём выгрузки операций в ядро системы (когда это возможно). Поскольку большинство современных ядер являются многопоточными, они могут обрабатывать несколько операций, выполняемых в фоновом режиме. Когда одна из этих операций завершается, ядро сообщает Node.js, что соответствующая этой операции функция обратного вызова может быть добавлена в очередь опроса, чтобы в конечном итоге быть выполненной.
Модель, построенная на управлении событиями, сама по себе очень сверхмасштабируема, так как потоки (threads), представленные в ней, никогда не находятся в состоянии ожидания. Но в то же время это заявлено как проблема, так как та или иная функция вынуждена брать себе для исполнения длительный период времени, прежде чем она завершится.
Поскольку изначально сервер, базирующийся на Node.js, запускает по одному потоку на ядро (по умолчанию) – какая-нибудь одна «долгоиграющая» функция может занять целиком всё ядро процессора, заставляя при этом другие функции простаивать в режиме ожидания. Именно по приведенной выше причине в браузере мы иногда можем увидеть сообщение с ошибкой – «script taking too long to run». На сервере же в это время происходит «простой» приходящих запросов.
Разбираем на примере
Предположим, что у нас есть небольшой сервис example.com, который предоставляет нам пару эндпойнтгов:
Исходник сервиса, который вы можете использовать для локального запуска, вы можете скачать по ссылке.
Запрос на эндпойнт /info будет отображать нам информацию о нашем ip-адресе и количестве наших запросов.
"you are 172.25.20.157 request count on your IOLoop: 1"
А запрос на /check в соответствующем виде, будет показывать нам, смогли ли мы подобрать нужную комбинацию символов или нет.
curl "http://example.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
"you are 172.25.20.157 - At least one value is wrong!"
Что нам заранее известно о системе, которую мы будем «атаковать»?
- Чтобы получить доступ к системе, нам нужно предоставить корректную последовательность символов val0=1&val1=1&val2=1&val3=1&val4=1
- Последовательность чисел предположительно находится в промежутке от 1 до 100.
- Время исполнения алгоритма, скорее всего, варьируется от входных данных, которые он принимает.
- Сервер постоянно ожидает не менее 3-х секунд до того, как дать ответ, чтобы предотвратить тайминговую атаку.
Что нам неизвестно — это то, что корректные данные для входа в систему выглядят вот так:
val0=4&val1=12&val2=77&val3=98&val4=35
Итак, поскольку нам известно, что на обработку одного запроса сервер тратит около 3-х секунд, то на подбор комбинации 100^5 при помощи банального перебора могут уйти тысячи лет. Счётчик цикла событий увеличивается в любом случае, но информацию о состоянии счётчика мы можем получить лишь при вызове эндпойнта /info.
$ curl "http://example.com/info"
you are 172.25.50.175 request count on your IOLoop: 1
$ curl "http://example.com/info"
you are 172.25.50.175 request count on your IOLoop: 2
$ curl "http://example.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 172.25.20.157 - At least one value is wrong!
$ curl "http://example.com/info"
you are 172.25.50.175 request count on your IOLoop: 4
Посылка запросов на /check с различными комбинациями чисел не выдаст нам никаких существенных различий во времени исполнения. Алгоритм, по всей видимости, занимает меньше, чем 3 секунды, чтобы отработать. Также мы заметили, что отправка запроса на /info в то время, как отрабатывает запрос на /check, предоставляет нам зацепку, которой мы посвятили данное исследование. Тут нам как раз и приходит на помощь понятие цикла событий, которое мы описывали выше.
Пока отрабатывает вызов эндпойнта /check, цикл событий не перехватывает управление, заставляя при этом подвисать вызов эндпойнта /info. В противоположность к этому функция setTimeout попросту регистрирует запланированное событие и отдаёт контроль, позволяя запросам контроллеру /info обрабатываться очень быстро. Как же нам теперь применить тайминг? Очень просто. Мы пошлём запрос на /check и, не дожидаясь ответа, тут же вышлем запрос на /info, замерив при этом время отклика.
$ curl "http://example.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 172.25.20.157 - At least one value is wrong!
$ curl "http://example.com/info"
you are 172.25.50.175 request count on your IOLoop: 2
По сути, во время таких запросов происходит следующее:
Как мы видим – измерение покажет нам, что при установке val0=4 – система ответит нам с небольшой задержкой. Таким образом, опираясь на подобные задержки, мы можем в конечном итоге восстановить всю нужную нам последовательность кода.
Способы защиты:
- Поддержание константного времени во время исполнения рискованных запросов (например, аутентификации);
- Более доскональное тестирование систем на уязвимости (например, Nanown или Time_trial);
- Технологии для создания слепых подписей (если речь идёт о защите криптографических систем);
- Алгоритмы, основанные прежде всего на логических операциях, а не на арифметических.
Заключение
Тайминговые атаки на сегодняшний день могут представлять собой самую настоящую угрозу как для небольших приложений, так и для крупных. Практика показывает, что даже из небольших различий во времени выполнения запросов к системе можно почерпнуть достаточно информации, которая может быть использована злоумышленниками. В частности, с точки зрения Node.js – может быть использован цикл событий.
Ссылки
При написании поста я использовал этот материал.
Плюс некоторые материалы из Википедии.
Автор: serpentcross