Эта статья посвящена развитию систем защиты на аппаратных ключах, видам атак, с которыми пришлось столкнуться за последние годы, и тому, как им удалось противостоять. Рассмотрены возможности электронных ключей, которые напрямую влияют на степень защиты программного обеспечения, и основные проблемы, присущие данной технологии.
Современные электронные ключи подразделяются на ключи с симметричной криптографией, ключи с асимметричной криптографией и ключи с загружаемым кодом. Последний тип устройств требует более детального анализа, выходящего за рамки данной статьи.
Рассмотрим защиту Windows-приложений, разработанных с использованием компилируемых языков программирования (C, C++, Pascal, Delphi, Fortran и т.д.).
Обмен данными между программой и электронным ключом
Основным способом построения надёжной защиты является использование библиотеки API для работы с электронным ключом. Как правило, API поставляется в виде статической и динамической библиотеки. Статические библиотеки, в отличие от динамических, присоединяются (линкуются) к исполняемой программе в процессе сборки. Их использование наиболее предпочтительно, т.к. исключает возможность простой подмены файла. Далее будем рассматривать защиту приложений, использующих именно статическую библиотеку.
Программа обменивается данными с электронным ключом через библиотеку API, которая напрямую взаимодействует с драйвером электронного ключа. Типовая схема обмена между защищённой программой и электронным ключом изображена на рисунке 1.
Атаки в такой схеме нацелены на взаимодействие между различными модулями защиты. Перехват запросов к электронному ключу на уровне драйвера электронного ключа (1) или драйвера USB-шины (2) не требует повторной модификации каждой новой версии приложения, в отличие от варианта с перехватом вызовов статической библиотеки API (3).
Атака на драйвер электронного ключа
Данный вид атаки является наиболее простым. Оригинальный драйвер электронного ключа заменяется на драйвер-эмулятор. Данные, передаваемые в ключ и возвращаемые обратно, перехватываются и сохраняются в файле на диске. Затем оригинальный ключ извлекается из компьютера и программа начинает взаимодействовать с эмулятором, продолжая искренне верить в то, что общается с ключом.
Переходя к частностям, хотелось бы остановиться подробнее на защите от программных эмуляторов в электронных ключах Guardant. Драйверы электронных ключей Guardant содержат электронную подпись (ЭП). При вызове функций Guardant API защищенное приложение автоматически проверяет подпись драйвера в оперативной памяти. Поскольку закрытый ключ не известен, создать драйвер-эмулятор с правильной подписью невозможно. Убрать проверку мешает виртуальная машина (псевдокод), с помощью которой защищены исполняемые файлы драйверов и библиотеки Guardant API. Схема проверки изображена на рисунке 2.
После внедрения такой защиты на практике, программные эмуляторы сместились на уровень USB-шины (2-й вариант атаки).
Атака на драйвер USB-шины
Создание такого эмулятора потребовало изучение протокола обмена между драйвером электронного ключа и драйвером USB-шины. Как это ни парадоксально звучит, но в такой ситуации более надёжными оказались LPT-ключи, т.к. драйвер электронного ключа напрямую взаимодействует с LPT-ключом через порты ввода/вывода компьютера, минуя промежуточный драйвер.
К сожалению, полностью избавиться от программных эмуляторов на уровне USB-шины, используя ключи с симметричной криптографией, невозможно. Тем не менее, хорошая защита, построенная на постоянном обмене с электронным ключом, может потребовать не один день для записи всех возможных посылок и ответов к ключу и сработать у нелегального пользователя в самый неподходящий момент. Взломанные программы, перестающие работать по непонятным причинам, лишь тому подтверждение.
Отдельно хочется упомянуть о защитах, когда разработчики ограничиваются простой проверкой наличия электронного ключа. Это грубая ошибка. Используя дизассемблер, «независимость» такой программе можно подарить за 15 минут.
В электронных ключах с асимметричной криптографией обмен данными между защищённой программой и электронным ключом шифруется на сеансовых ключах. По этой причине единственно возможным является третий вариант атаки.
Атака на перехват вызовов статической библиотеки API
Данная атака является наиболее логичной для ключей с асимметричной криптографией. Она целесообразна только в случае, если производитель электронных ключей позаботился о защите своих библиотек, иначе гораздо проще атаковать саму библиотеку.
С помощью современного дизассемблера можно быстро распознать функции API в программе. А раз так, то все вызовы функций легко перехватить. Это означает, что коды доступа, запросы и ответы по-прежнему остаются уязвимы. И если сам разработчик программы не защитил вызовы функций API, то это наилучшее место для атаки.
Для исключения возможности перехвата вызовов статической библиотеки API в защищённой программе, необходимо ключевые функции приложения и функции API объединить в единое целое. Только тогда анализировать параметры, перехватывать вызовы или модифицировать запросы и ответы к функциям API будет действительно сложно. На практике разработкой защиты начинают заниматься уже после того как написан основной код приложения. Очень часто на глубокую интеграцию взаимодействия программы и электронных ключей не хватает времени, а возможно, и желания. Наименее трудоёмкий способ повысить качество защиты, это дополнительно использовать программные средства, усложняющие анализ кода. В этом случае, взаимодействие функций программы и функций статической библиотеки API становится трудным для понимания.
Возвращаясь к электронным ключам Guardant, скажем, что все библиотеки Guardant API защищены от анализа и модификации с помощью виртуальной машины. Под виртуальной машиной подразумевается псевдокод, полученный из оригинального бинарного кода программы, и соответствующий ему интерпретатор. Инструкции псевдокода могут быть выполнены только на том интерпретаторе, для которого он был сгенерирован. Интерпретатор отвечает за защиту инструкций псевдокода и самого себя от модификации при помощи множественного контроля целостности. Все параметры инструкций, константы, адреса переходов расшифровываются на хэшах фрагментов псевдокода и самого интерпретатора. Для затруднения анализа интерпретатор псевдокода защищён с помощью полиморфной обфускации. Перечисленные характеристики реализуются уникальным образом в каждой копии виртуальной машины.
Технология защиты псевдокодом доступна всем желающим с помощью сервиса Guardant Online. У такого подхода есть ряд преимуществ: инструмент защиты постоянно обновляется и недоступен для изучения. При этом особую выгоду получают разработчики, использующие электронные ключи Guardant. Сервис позволяет защищать функции программы одновременно с функциями статической библиотеки Guardant API (технология Guardant Monolith). Наличие библиотеки Guardant API внутри приложения определяется автоматически, поэтому никаких дополнительных перекомпиляций приложения не требуется. В результате, при каждой защите приложения создаётся уникальная копия виртуальной машины со своими инструкциями, константами, обфускацией, в которой логика работы программы тесно связана с библиотекой электронного ключа. Теперь для перехвата вызовов статической библиотеки или атаки на саму библиотеку придётся разбираться с каждой защитой индивидуально.
Рассмотрим это на примере небольшого тестового приложения, фрагмент которого приведён на рисунке 4. Данный пример очень ярко характеризует уязвимость вызовов любой статической библиотеки внутри приложения. Проблема касается всех производителей электронных ключей.
Предположим, что функция приложения MyLogicAndVerifyDongle содержит некоторые программные вычисления с использованием электронного ключа. Работа без данной функции невозможна, т.к. она содержит часть ключевой логики приложения. При этом вызов функции осуществляется из менее важного участка кода программы. Для обращения к электронному ключу используются вызовы статической библиотеки Guardant API. В данном примере это вызовы функций GrdRead и GrdCrypt. В начале каждой функции находится лишь переход в защищённую область VM_Start. Тем не менее, сама функция MyLogicAndVerifyDongle остаётся незащищённой и может быть подвержена атаке.
Теперь проведём защиту функций API и ключевых функций приложения с помощью сервиса Guardant Online. Фрагмент защищённого приложения приведён на рисунке 5.
После защиты функция MyLogicAndVerifyDongle содержит лишь переход в защищённую область Guardant_VirtualMachine. По адресам, где ранее располагался исполняемый код функции, оказываются мусорные инструкции. Сама функция транслируется в псевдокод и исполняется на той же самой виртуальной машине, что и функции Guardant API. Больше нет возможности ставить точки останова на вызовы функций API, т.к. переходы спрятаны внутри виртуальной машины. Это означает, что для взлома программы придётся раскручивать всю логику работы виртуальной машины, а это на порядок сложнее, чем простой перехват функций API.
Выводы
Ряд рекомендаций для построения надёжной защиты.
- Необходимо использовать статические библиотеки API в программе. Вызовы библиотеки API должны быть тесно интегрированы с логикой защищаемой программы – это гарантия создания стойкой защиты.
- Лучше использовать ключи с асимметричной криптографией, т.к. для них не существует простого способа создания табличных эмуляторов.
- Код приложения и статическую библиотеку API необходимо защитить от анализа с помощью программных средств защиты, где отсутствует простая возможность перехвата вызовов функций API в программе. Для разработчиков, использующих технологии Guardant, идеальным решением будет Guardant Monolith.