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

WARNING!

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

Предисловие

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

Если обратиться к истории, то можно увидеть, что банки одними из первых стали использовать гражданскую криптографию — шифрование, электронную цифровую подпись, криптографические протоколы и специализированную аппаратуру. Разработчики систем дистанционного банковского обслуживания (или просто ДБО), таких как онлайн-банки для физических лиц или банк-клиент для юридических лиц, как правило, неплохо знакомы с криптографией и умеют хорошо решать основные задачи в применении к банковским реалиям: обеспечение безопасной передачи данных, обеспечение неотказуемости банковских операций (то есть цифровой аналог собственноручной подписи) и аутентификацию, соответствие требованиям государственных регулирующих органов (регуляторов, в нашем случае — Банка России), защиту морально устаревших систем, которые нельзя оперативно обновить. Так что если такие системы аккуратно разработаны программистами, которые хорошо знают основы криптографии и умеют их применять, используют признанные алгоритмы с хорошо изученной криптостойкостью, то их сложно сломать, потому что в этом случае придется искать уязвимости, к примеру, в реализации RSA или научиться эффективно факторизовать числа…

А вот и нет. Все так красиво и безопасно только на словах. Даже в военной сфере, авиации и прочих областях разработки ПО, где надежность жизненно важна и где в процессы разработки интегрированы методы формальной верификации, в программах время от времени находят ошибки. Финансовые приложения существенно менее критичны в плане надежности (от сбоя ДБО, скорее всего, никто не умрет), и формальная верификация — нечастый гость даже в процессах разработки современных финансовых приложений, не говоря уже о старых системах, разработанных лет двадцать назад или еще раньше.

Каждый банк, который хочет предоставлять доступ к финансовым транзакциям через интернет, вынужден выбирать один из двух вариантов реализации:

  • использовать типовое решение одного из хорошо известных на рынке вендоров (например, BSS, Bifit) и «заточить» его под себя;
  • разработать самостоятельно (или заказать у стороннего разработчика) собственную систему.

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

Рис. 1. Шифрование, обеспечение неотказуемости операций и аутентификация в ДБО
Рис. 1. Шифрование, обеспечение неотказуемости операций и аутентификация в ДБО

О втором варианте как раз и пойдет сегодня речь. Мы рассмотрим современные системы дистанционного банковского обслуживания, работающие поверх обычных интернет-соединений (рис. 1). И предложим подход к поиску уязвимостей проектирования в этих финансовых приложениях, на примере клиент-банка одного из крупных европейских банков. Данный подход особенно актуален и интересен тем, что в России довольно много банковских приложений реализуют именно такую архитектуру ДБО (рис. 1).

Забегая немного вперед, скажем: по нашему опыту, самая «вкусная» часть таких приложений — специализированный протокол взаимодействия компонентов между собой. Действительно, криптосервер должен передавать серверу приложений результаты проверки ЭЦП из запроса пользователя. Сервер приложений при этом должен доверять этим результатам, так как сам не использует криптографические примитивы (в этом была вся идея!). Фактически методика взлома такой схемы состоит из обратной инженерии протокола взаимодействия с тем, чтобы понять, как серверу приложений передается результат валидации запроса пользователя, а потом научиться подделывать и передавать эти идентификационные данные.

Предлагаемый подход

Для начала сделаем пару утверждений общего характера на уровне здравого смысла о процессе разработки приложений в банковской сфере и оставим их без доказательства; они помогут нам в поиске уязвимостей проектирования.

Утверждение А: нельзя так просто взять и создать криптографический протокол уровня приложений с нуля.

Когда разработчик пытается изобрести «на коленке» безопасный специализированный протокол с использованием криптографических примитивов без предварительных шагов типа разработки спецификации, формального доказательства свойств протокола, использования безопасного процесса разработки, у него, скорее всего, ничего не выйдет.

Утверждение Б: нельзя так просто взять и реализовать HTTP-клиент или HTTP-сервер с нуля.

Когда программист пытается создать свой новый клевый HTTP-клиент или HTTP-сервер, с блек-джеком и всеми теми фичами безопасности, которых ему так хочется, и при этом он не Google или Microsoft в смысле бюджета, количества рабочей силы и плана выпуска продукции, у него, скорее всего, также ничего не выйдет. А теперь представим, что мы имеем дело с результатами обоих утверждений в одном месте. Это означает кучу сделанных «на коленке» парсеров, поверх которых работает прикладной протокол, «защищенный» криптографическими примитивами. Все это дает высокую вероятность появления уязвимости после интеграции всех частей в единое решение. Что касается любого нарушителя, то изначально он уже будет обладать следующими возможностями:

  • заходить в систему в качестве легитимного пользователя (мы всегда можем стать клиентом атакуемого банка — например взяв у него кредит :));
  • иметь полный доступ к клиентскому ПО (и аппаратуре), что позволяет осуществлять обратную инженерию произвольного «толстого» клиента и протокола, по которому он обменивается данными с сервером, мониторить работу с аппаратными ключами и так далее.

Чтобы определиться с условиями, под успешным хаком ДБО будем понимать получение доступа к счетам других клиентов с возможностью генерировать запросы на выполнение платежей от их имени, которые корректно проводятся сервером. Мы планируем сделать это, обойдя проверки ЭЦП за счет использования различий в обработке протокола HTTP элементами многозвенной архитектуры банковского решения. Но прежде необходимо детально изучить «пациента». Три основных шага обратной инженерии ДБО, которые помогут нам справиться с задачей, включают в себя:

  • реверсинг деталей реализации клиента;
  • фингерпринтинг серверной части;
  • реверсинг и анализ протокола взаимодействия.

Основной задачей первого шага является реверсинг криптографического протокола, реализованного на клиентской стороне криптосистемы. Криптосистему можно представить черным ящиком, который внедряется между браузером пользователя и сервером приложений и делает следующее:

  1. Устанавливает шифрованный туннель (SSL/TLS или специализированный VPN — без разницы).
  2. Подписывает каждый исходящий HTTP-запрос на клиенте. Эта часть может быть реализована (и часто так и делают) как веб-прокси, который слушает на локальном порту клиента.
  3. Проверяет целостность и аутентичность HTTP-запросов, покидающих туннель на серверной стороне криптосистемы.
  4. Передает проверенные запросы серверу приложений, к которым прикрепляет метаданные, содержащие идентификатор пользователя и результаты криптографической обработки запроса («Все OK», «Ключ устарел» и так далее).

Реверсинг криптопротокола на стороне клиента — не высшая математика, достаточно воспользоваться API-монитором и любимым отладчиком и с их помощью получить ответы на следующие вопросы:

  1. Какой HTTP-клиент (и парсер) используется на стороне клиента (Windows API, Java HTTP Client, …)?
  2. Какие элементы GET-запроса подписываются ЭЦП? Весь запрос или только URL? Какие запросы поддерживаются: POST, GET, HEAD, TRACE?
  3. Какие элементы подписываются у POST-запроса? Весь запрос, только тело или тело и URL?
  4. Какая служебная информация передается с запросом? Как передается ЭЦП? Как передается идентификатор ключа? Например, это могут быть кастомные заголовки, такие как X-Client-Key-Id.

После этого можно переходить к изучению функциональности криптосервера. Здесь предстоит сделать несколько вещей: проанализировать особенности парсера HTTP на криптосервере, проанализировать сам веб-сервер криптосервера и, наконец, проанализировать взаимодействие криптосервера и сервера приложений.

Анализ HTTP-парсера криптосервера

Основные проверки, которым следует подвергнуть криптосервер, представляют собой следующий список:

  • Как HTTP-парсер криптосервера обрабатывает дублирующиеся имена параметров в GET- и POST-запросах? Какое значение будет использовано: первое или последнее? Как насчет одинакового имени параметра в URL POST-запроса и в его теле (да-да, HTTP Parameter Pollution)?
  • Как криптосервер обрабатывает дублирующиеся заголовки? Какое значение будет использовано: первое или последнее?
  • Какие символы используются для разделения заголовков (CRLF, CR или что-то еще)?

Цель этого этапа — обнаружить различия в обработке HTTP на стороне криптосервера, где выполняются проверки подписи, и на сервере приложений, где выполняется непосредственно обработка запроса, используя которые мы могли бы реализовать идею атаки вида «XML signature wrapping attack», только для HTTP, при помощи все тех же известных методов: protocol smuggling и parameter pollution (в примере ниже ты увидишь, как конкретно это работает на реальном приложении).

Анализ сервера HTTP

Следующий этап анализа позволит выяснить детали обработки протокола HTTP на сервере. Здесь нам нужны ответы на следующие вопросы:

  • Какие версии протокола HTTP поддерживаются? Поддерживается ли HTTP/0.9?
  • Поддерживаются ли множественные HTTP-запросы через одно соединение?
  • Как криптосервер обрабатывает некорректные или дублирующиеся заголовки Content-Length?
  • Какие HTTP-методы разрешены?
  • Поддерживает ли криптосервер multipart-запросы или чанки?

Анализ протокола взаимодействия

Напомним, мы бы хотели уметь форджить осмысленные запросы, которые будут обрабатываться сервером приложений как доверенные. Наиболее очевидный и простой способ передачи метаданных от криптосервера к серверу приложений — через HTTP-заголовки, добавляемые к запросам клиента. Соответственно, знание «секретных» управляющих заголовков может предоставить тебе полную власть в банковских приложениях.

Действительно, криптосервер должен каким-то образом передавать серверу приложений идентифицирующую информацию о пользователе, который сделал запрос. Грубо говоря, криптосервер должен вместо вороха пришедшей к нему метаинформации, являющейся продуктом криптографических преобразований, передать серверу приложений просто идентификатор пользователя, который сделал запрос. Сервер приложений же должен доверять полученной информации. Это свойство (то есть протокол взаимодействия между криптосервером и сервером приложений, который отделен от криптографических примитивов) становится неотъемлемой частью архитектуры, когда разработчики решают использовать в качестве фронтенда стороннее криптографическое решение.

Все это хорошо, но как мы узнаем названия этих управляющих заголовков? Тут доступны следующие варианты:

  • угадать/сбрутфорсить;
  • прочитать документацию на криптосервер; есть надежда, что названия управляющих заголовков не поменялись в установленной версии;
  • атака методом социальной инженерии на разработчиков криптографического решения; можно притвориться заказчиком и спросить, как будет их криптосервер передавать результаты проверки запросов твоему бэкенду;
  • прочитать заголовки в ответе от самого приложения (см. отладочные интерфейсы, подробные сообщения об ошибках, метод TRACE);
  • обратная инженерия криптоклиента или криптографических библиотек; возможна ситуация, когда метаданные, присоединенные к исходящим запросам на стороне клиента, будут только валидироваться криптосервером, но никак не изменяться. Действительно, зачем менять клевые названия заголовков?

Пример взлома

Ну а теперь, собственно, о самом интересном. Все началось как обычно. Большой европейский банк с филиалом в России попросил проанализировать безопасность их ДБО, защищенного криптоалгоритмами семейства ГОСТ. Почти сразу после начала анализа мы нашли несколько типовых веб-уязвимостей, которые позволяли, например, получать список всех пользователей и менять им пароли. Плюс обнаружили отладочный интерфейс, который распечатывал полностью HTTP-запрос, полученный веб-приложением (так что с именами управляющих заголовков мы разобрались довольно быстро).

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

Сам клиент представлял собой прокси уровня приложений, работающий на стороне пользователя между браузером и криптосервером. Пристальный взгляд на клиентскую часть позволил выявить криптографические примитивы, реализованные в поставляемых вместе с клиентом разделяемых библиотеках, и с помощью API Monitor нам удалось установить хуки на вызовы API, проанализировать полученные трассы и вытащить оттуда нужную информацию.

Рис. 2. Трасса, собранная с помощью API Monitor
Рис. 2. Трасса, собранная с помощью API Monitor

Изучение буфера с пользовательскими данными раскрыло структуру запроса от клиента, в частности, в него были добавлены специальные заголовки Certificate_number, Form_data, Signature (рис. 3), а также было видно, какие данные из запроса подписывались с помощью ЭЦП (рис. 4).

Рис. 3. В буфере мы видим структуру запроса от клиента, добавлены специальные заголовки к запросу
Рис. 3. В буфере мы видим структуру запроса от клиента, добавлены специальные заголовки к запросу
Рис. 4. и какая информация подписывается ЭЦП — Form_data
Рис. 4. …и какая информация подписывается ЭЦП — Form_data

Для нас наиболее интересен тут заголовок Certificate_number, который, очевидно, содержит идентификатор ключа клиента, а также заголовки Form_data и Signature, которые содержат параметры запроса (в данном случае строку запроса) и ЭЦП соответственно.

В результате клиентский запрос, который в оригинале выглядит так:

GET /login?name=value HTTP/1.1
Host: 10.6.28.19

после обработки криптоклиентом становится таким:

GET /login?name=value HTTP/1.1
Host: 10.6.28.19
Certificate_number: usr849
Form_data: name=value
Signature: 6B8A57A3EA9C25D77C01F4E957D5752C69F61D3451E87DD18046C51DC9A9AD63C7718708159B7ECF5FC8EDF4424F813DB65EF5E2D21D2F389E03319CA25D7003

Играясь с методами и параметрами запросов, мы заметили, что прокси подписывает только строку запроса в случае GET-запросов и только тело в случае POST-запросов.

Стало понятно, что криптосервер для каждого запроса выполняет примерно такое предписание:

  1. Проверить, что заголовок Form_data отражает строку запроса или тело, в зависимости от типа запроса.
  2. Проверить, что значение заголовка Certificate_number указывает на того же пользователя, который устанавливал безопасное соединение с помощью своего сертификата.
  3. Проверить, что заголовок Signature содержит корректную подпись заголовка Form_data, используя ID ключа из Certificate_number.

Выглядит солидно, не так ли?

Обход механизма обеспечения неотказуемости операций

Что ж, сначала мы посвятили немного времени анализу и фингерпринтингу. И вот что мы нашли:

  1. Прокси на стороне клиента не добавлял заголовки Form_data и Signature к запросам HEAD, они передавались без подписи и каких-либо проверок, клиентская часть только добавляла номер сертификата клиента.
  2. Прокси на стороне клиента не учитывал, что POST-запросы могут содержать не только параметры в теле запроса, но и строку запроса. Для POST-запросов подписывались только параметры из тела, а строка запроса передавалась серверу приложений без изменений. Теперь можно было попробовать HTTP parameter pollution (HPP) и передавать параметры с одним и тем же именем в теле и в строке запроса. Что и было сделано. В результате мы обнаружили, что криптопрокси подписывает только параметры в теле запроса и передает строку запроса без изменений, криптосервер также проверяет подпись только для тела (см. заголовки Form_data и Signature на рис. 5) и передает строку запроса серверу приложений в неизменном виде.
Рис. 5. Как запрос POST выглядит после прохождения криптопрокси: подписано только значение из тела запроса, при этом значение из строки запроса осталось неизменным
Рис. 5. Как запрос POST выглядит после прохождения криптопрокси: подписано только значение из тела запроса, при этом значение из строки запроса осталось неизменным

Убедившись, что сервер приложений отдает предпочтение параметрам, получаемым в строке запроса, а не в теле, мы смогли обойти механизм обеспечения неотказуемости при помощи такого вектора: нужные нам значения параметров запроса ставим в строку запроса, а в теле оставляем те значения, которым криптопрокси будет доверять. При этом, как ты помнишь, сервер приложений первым делом смотрит в строку запроса, а значит, мы удачно обошли все проверки, и при этом незаметно для всех компонентов системы.

Рис. 6. Итоговый вектор атаки на механизм обеспечения неотказуемости - атакующее значение передаем в строке запроса, а в теле оставляем доверенные значения
Рис. 6. Итоговый вектор атаки на механизм обеспечения неотказуемости – атакующее значение передаем в строке запроса, а в теле оставляем доверенные значения

Обход авторизации

Все это хорошо, только кого волнует эта неотказуемость, если механизмы аутентификации остаются неприступными? Поэтому идем копать дальше. Напомним, что к этому моменту мы умеем просматривать список всех пользователей ДБО, менять им пароли и, кроме того, отправлять серверу приложений запросы таким образом, что криптосервер считает их корректно подписанными, а сервер приложений использует параметры без подписи.

Предположим, что мы хотим атаковать пользователя с идентификатором ID=0x717 и уже установили ему новый пароль. Теперь мы бы хотели залогиниться под ним. В обычной ситуации аутентификационный запрос выглядит как на рис. 7.

Рис. 7. Обычный запрос на авторизацию
Рис. 7. Обычный запрос на авторизацию

С передачей этого запроса есть две проблемы: во-первых, прокси на стороне клиента удаляет все управляющие заголовки из запроса от браузера. В нашем случае будет удален заголовок Certificate_number. С этим можно справиться, реализовав свой собственный криптоклиент, который связывается с криптосервером и передает все, что нам нужно, с правильными заголовками. Вторая проблема заключается в том, что криптосервер сравнивает параметр Certificate_number из заголовка, полученного в HTTP-запросе, с номером сертификата, который был использован для установки шифрованного туннеля. Вот в этом-то вся загвоздка. Чтобы продвинуться дальше, нужен был очередной трюк. И мы его нашли.

Помнишь, мы научились отправлять HEAD-запросы со строкой параметров без какой-либо подписи? Вдобавок к этому мы заметили, что каждый раз через одно TCP-соединение криптоклиент передает только один HTTP-запрос, после чего криптосервер разрывает соединение.

И мы подумали: а что, если отправить два HTTP-запроса в одном TCP-соединении один за другим? Оказалось, что криптосервер будет считать их одним большим HTTP-запросом. Бинго, protocol smuggling!

Вот как криптоклиент обрабатывал два последовательных запроса, первый из которых – HEAD:

  • клиент парсил их как одно HTTP-сообщение со строкой запроса, заголовками и телом;
  • удалял все управляющие заголовки из первого запроса (ну да, второй-то он считает телом первого);
  • добавлял корректный заголовок Certificate_number в первый запрос;
  • как было показано выше, он, без добавления заголовков Form_data или Signature, отправляет HEAD-запрос (с телом, без проверки) в криптотуннель.

А вот как криптосервер обрабатывал полученные запросы:

  • он также считал их единым HTTP-сообщением;
  • сервер проверял значение заголовка Certificate_number на совпадение с параметрами установленного криптотуннеля;
  • для HEAD-запроса криптосервер не проверял отсутствующие заголовки Form_data и Signature, а сразу передавал результат серверу приложений, добавив к нему необходимые управляющие заголовки.

Сервер приложений, в свою очередь, корректно обрабатывал полученные данные как два отдельных HTTP-запроса, причем обрабатывал оба. Вот так мы смогли обойти авторизацию (см. рис. 8 и 9).

Рис. 8. Криптоклиент и криптосервер считают два последовательных HTTP-запроса в одном соединении одним большим запросом
Рис. 8. Криптоклиент и криптосервер считают два последовательных HTTP-запроса в одном соединении одним большим запросом

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

Рис. 9. Итоговый вектор атаки на аутентификацию
Рис. 9. Итоговый вектор атаки на аутентификацию

После того как два запроса (рис. 9) будут обработаны клиентской частью криптосистемы, у нас окажется два корректных HTTP-запроса, первый от имени нашего пользователя, а второй от имени произвольного пользователя системы. Шах и мат!

Потому что ничто никогда не меняется…

Если напрячь извилины, то можно вспомнить изрядное число недавних публикаций и выступлений с похожими техниками обхода механизмов защиты:

  • XML Signature Wrapping;
  • On Breaking SAML: Be Whoever You Want to Be bit.ly/Rwg0Gk;
  • Analysis of Signature Wrapping Attacks and Countermeasures bit.ly/10HtJPW;
  • CWE–347: Improper Verification of Cryptographic Signature и связанные с ней CVE bit.ly/12nEdXN;
  • погугли по запросу — куча разных статей;
  • CWE–444: Inconsistent Interpretation of HTTP Requests и связанные с ней CVE bit.ly/1472VJt;
  • Web App Cryptology: A Study in Failure bit.ly/13ub1tE;
  • Разное: небезопасные генераторы случайных чисел, некорректные реализации PKI как примеры некорректного использования криптографии.

Заключение

В результате мы достигли возможности отправлять полностью доверенные запросы от имени «зловредного» клиента к серверу банка, как если бы они были сгенерированы легитимным клиентом. Мы полагаем, что подобный подход к анализу систем «сверху вниз» можно использовать практически для любой специализированной криптографической системы, потому что ключевое значение имеет человеческий фактор (в нашем случае это приняло форму неверного представления о современных протоколах уровня приложений, о сложных веб-фреймворках и их внутренней кухне). Кроме того, разобранный пример анализа ДБО может оказаться полезным для других исследователей защищенности программных продуктов.

В качестве послесловия процитируем слова Ади Шамира из его недавнего выступления на RSA Conference 2013: «Я действительно верю, что значимость криптографии снижается. Даже самые защищенные компьютерные системы в самых физически изолированных местах успешно взламывались в последние пару лет в результате APT (Advanced Persistent Threat, целенаправленная атака на конкретную систему) и других продвинутых атак».

By Ruslan Novikov

Интернет-предприниматель. Фулстек разработчик. Маркетолог. Наставник.