Skip to content

side channel

Alexander Borzunov edited this page Mar 12, 2018 · 16 revisions

Правительство Курляндии потратило 1 млрд долларов на разработку системы аутентификации

  • Категория: Web
  • Стоимость: 800
  • Автор: Александр Борзунов
  • Репозиторий

Условие

Правительство Курляндии потратило 1 миллиард бюджетных долларов на разработку «самой защищённой системы аутентификации в мире». Система будет использоваться внутри страны для доступа к базам данных полиции, спецслужб и военных.

Сообщается, что разработчики системы после нескольких лет исследований отказались от использования таких биометрических данных, как отпечаток пальца или снимок сетчатки глаза (по их словам, для этого пришлось бы установить дорогостоящие сенсоры на огромное число устройств). Вместо этого система проверяет пароль, вводимый пользователями от руки.

«Согласно проведённым исследованиям, средний житель Курляндии тратит на 10% меньше времени, если пишет пароль стилусом, а не печатает его на сравнительно маленькой клавиатуре современных телефонов или планшетов», — заявил глава проекта доктор Сигмунд Мортмен.

Разработчики выслали исходный код системы целому ряду фирм, известных своей деятельностью в сфере информационной безопасности. Они уверены, что в системе нет серьёзных изъянов, но за любые найденные недочёты готовы платить соразмерное вознаграждение.

Решение

В условии дана ссылка на сайт с системой аутентификации и её исходный код. Для входа система требует написать пароль с помощью рукописного ввода (мышкой):

Проанализируем код системы. Строка в начале файла подсказывает, что пароль состоит из 10 цифр:

assert re.fullmatch(r'\d{10}', PASSWORD) is not None

Сравнение введённых цифр с паролем происходит в цикле:

recognized = ''
correct = True
for actual, expected in zip_longest(find_digits(img), PASSWORD):
    if actual is not None:
        recognized += actual
    if actual != expected:
        correct = False
        break

Если очередная цифра не совпала с требуемой, цикл будет остановлен оператором break. В подобных случаях иногда можно провести тайминг-атаку, ведь чем больший префикс вашей строки совпал с правильным паролем, тем дольше будет выполняться код, генерирующий ответ на запрос, на сервере.

Теперь заметим, что find_digits, используемая в цикле, является не функцией, возвращающей список распознанных цифр, а генератором, использующим оператор yield и возвращающим цифры по одной. Особенность генераторов заключается в том, что они выполняются лениво - код от одного yield до следующего выполнится только тогда, когда следующее значение последовательности будет запрошено в цикле, вызывающем генератор. Если же этот цикл закончится заранее, не дойдя до конца последовательности (как в случае выше, если выполнится break), код, распознающий оставшиеся цифры, не будет выполняться. Так, разработчики системы могли применить генератор, чтобы сэкономить ресурсы, не распознавая ненужные цифры.

В данном случае распознавание одной цифры - сравнительно долгая операция, включаящая в себя поиск границ цифры и запуск нейросети. Значит, посмотрев на время генерации ответа, мы наверняка сможем отличить ситуации, когда правильными были первые K цифр пароля (поэтому распознавалась K + 1 цифра) и когда правильными были первые K + 1 цифра (поэтому распознавались K + 2).

Наконец, ещё одним намёком на тайминг-атаку является то, что, кроме сообщения о правильности пароля, сервер возвращает время, потраченное на генерацию ответа:

elapsed_ms = round((time.time() - start_time) * 1000)

app.logger.info('recognized = "{}"  correct = {}  elapsed_ms = {}'.format(
    recognized, correct, elapsed_ms))
return jsonify({'status': status, 'elapsed_ms': elapsed_ms})

Этот факт облегчит написание эксплоита: колебания времени, которое мы получим из значения elapsed_ms, не будут зависеть от сетевых задержек.

Сначала эксплоит может сравнивать время ответа для изображений, содержащих строки 00, 10, 20, ..., 90. То из них, для которого сервер генерирует ответ дольше всего (потому что распознаёт две цифры, а не одну) содержит правильную первую цифру. После этого по очереди можно восстановить и оставшиеся 9 цифр пароля.

Чтобы написать надёжный эксплоит, нужно учесть следующие особенности:

  1. Время генерации ответа может колебаться в зависимости от текущей загрузки сервера. Поэтому имеет смысл делать запрос с одним и тем же изображением, к примеру, 3 раза, а потом брать медианный результат.

  2. Нейросеть для распознавания цифр обучена на известной базе данных с образцами рукописного написания цифр MNIST. Несмотря на то, что на этом наборе данных она достигает высокой точности, она необязательно будет способна распознать цифры, написанные компьютерным шрифтом, или цифры, которые нарисованы в Paint'е мышкой. Чтобы использовать в переборе цифры, которые распознаются правильно, можно запустить систему на своём компьютере и проверить по логам, какое из написаний интересующей вас цифры распознаётся верно.

    Другой способ - отправлять на сервер изображение с цифрами из самого набора MNIST. Для надёжности можно отправлять несколько написаний для одной цифры. Этот подход реализован в авторском решении.

  3. (не обязательно) В качестве последней цифры (самой правой связной области) можно отправлять большой чёрный прямоугольник:

    Оказывается, дольше всего при распознавании цифры работает поиск в ширину, который ищет её границы (большой объём пикселей обрабатывается в циклах на медленном языке Python). Если все цифры, кроме последней, верны, сервер запустит поиск в ширину для большой связной области, который замедлит генерацию ответа на примерно 0,9 секунд!

Авторское решение, где реализованы все эти техники, можно посмотреть здесь.