Привет, Хабр!

Меня зовут Юрий Шабалин, как вы, наверное, уже знаете, я один из основателей компании Стингрей Технолоджиз, разработчика платформы анализа защищенности мобильных приложений iOS и Android.

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

Оглавление

Введение

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

Здесь я не буду глубоко погружаться в технические нюансы реализации различных протоколов и их историю. Мне бы хотелось дать более-менее целостную картину по сетевой активности и рассказать все простым и максимально доступным языком. Для тех, кто хочет копнуть поглубже, в тексте статьи приведены ссылки.

Немного про протоколы

HTTP

HTTP — широко распространённый протокол передачи данных, изначально предназначенный для передачи гипертекстовых документов (тех, которые могут содержать ссылки, позволяющие организовать переход к другим документам).

Аббревиатура HTTP расшифровывается как HyperText Transfer Protocol, «протокол передачи гипертекста». В соответствии со спецификацией OSI, HTTP является протоколом прикладного (верхнего, 7-го) уровня. Актуальная на данный момент версия протокола, HTTP 1.1, описана в спецификации RFC 2616.

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

Задача, которая традиционно решается с помощью протокола HTTP — обмен данными между пользовательским приложением, осуществляющим доступ к веб-ресурсам (обычно это веб-браузер), и веб-сервером. На данный момент именно благодаря протоколу HTTP обеспечивается работа Всемирной паутины.

API многих программных продуктов также подразумевает использование HTTP для передачи данных. Информация при этом может иметь любой формат, например, XML или JSON.

Как правило, передача данных по протоколу HTTP осуществляется через TCP/IP-соединения. Серверное программное обеспечение при этом обычно использует TCP-порт 80, но могут быть задействованы и другие порты. Если порт не указан явно, то обычно клиентское программное обеспечение по умолчанию использует именно 80-й порт для открываемых HTTP-соединений.

За описание протокола HTTP простыми словами спасибо вот этой статье.

HTTPS

Описание

Как известно, протокол HTTP передает данные в открытом виде, то есть любой человек в сети может прослушать трафик и прочитать все сообщения, которыми обмениваются стороны. Для того, чтобы предотвратить возможность чтения и модификации запросов, поверх обычного HTTP добавили SSL и появился протокол HTTPS, где буква S расшифровывается как Secure, то есть безопасный. При HTTPS соединении все данные зашифровываются и расшифровать их могут только те, кому они предназначены (отправляющая и принимающая стороны). Реализовано это с применением SSL/TLS уровней защиты (Secure Sockets Layer / Transport Level Security). Давайте посмотрим, что это такое, и чем они отличаются друг от друга.

История версий протоколов SSL/TLS
История версий протоколов SSL/TLS

SSL и TLS - это протоколы для защищенной передачи данных. TLS является прямым продолжением и, если можно так сказать, наследником SSL. Эти протоколы содержат в себе различные шифронаборы (Cipher Suites), которые можно использовать для осуществления операций шифрования, а также разнообразные правила коммуникации и многое другое. Первый протокол появился около 1995 года. От версии к версии он немного трансформировался: в него добавляли новые шифронаборы, убирали устаревшие, исправляли уязвимости и немного меняли сам протокол.

Шифронаборы

Что такое шифронаборы и почему они важны для нас?

Шифронабор - это некоторая строка вида “TLS_DH_RSA_WITH_AES_256_CBC_SHA256”, которая определяет, с использованием каких алгоритмов и по каким правилам будет осуществляться общение клиента и сервера. Давайте разберем по порядку, что за чем следует и что за что отвечает, - понимание этого потребуется нам в дальнейшем.

  • Protocol (TLS) – Протокол, по которому осуществляется соединение, а именно TLS или SSL.

  • Key Exchange (DH) – Алгоритм, по которому осуществляется обмен ключами. Это может быть RSA / DH (Диффи Хелман) / DHE (эфемерный Диффи Хелман) / ECDH (Диффи Хелман на эллиптических кривых) / ECDHE (эфемерный Диффи Хелман на эллиптических кривых) и другие. Чуть позже мы поймем, зачем я в обязательном порядке отметил эфемерные шифронаборы и дал каждому алгоритму расшифровку.

  • Authentication (RSA) – определяет, по какому алгоритму будет проходить аутентификация сторон (RSA / DSA / ECDSA / ...)

  • Stream encryption (AES_256_CBC) - определяет, по каким протоколам будет осуществляться шифрование потока. При этом указывается длина ключа и режим (RC4_128 / AES_256_CBC / 3DES_EDE_CBC / ...)

  • Message Authentication (SHA256)- определяет, каким алгоритмом будет осуществляться подпись сообщений (MD5 / SHA / SHA256 / ...)

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

Даже некоторые регуляторы и мировые стандарты рекомендуют обращать пристальное внимание на используемую версию протокола и применяемые шифронаборы. Если обратиться к PCI DSS, то можно найти рекомендацию по обязательному отключению SSL/early TLS и использованию “Strong Cryptography“.

Выдержка из PCI DSS
Выдержка из PCI DSS

При этом, включая только последние версии TLS с правильно настроенными шифронаборами, мы автоматически применяем свойство некоторых алгоритмов - Forward Secrecy (иногда Perfect FS). Его особенность в том, что компрометация сессионных ключей не происходит при компрометации одного из долговременных ключей. Работает эта опция только для эфемерных алгоритмов. То есть, речь идет про шифронаборы, начинающиеся с:

TLS_DHE_*         (EDH)  Ephemeral DH

TLS_ECDHE _*    (EECDH)  Ephemeral ECDH

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

TLS handshake (RSA)

Схематично можно изобразить TLS-рукопожатие (процесс установки защищенного соединения) в виде следующей схемы:

TLS-рукопожатие (RSA)
TLS-рукопожатие (RSA)

Разберемся простыми словами, что при этом происходит:

  1. Чтобы соединиться с сервером, клиент отправляет ему некоторый Hello-запрос. Условно, он говорит: “Привет сервер, я хотел бы начать с тобой общение“.

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

  3. Клиент на своей стороне вырабатывает предварительный секрет (большое случайное число), шифрует его на публичном ключе сервера, и этот зашифрованный секрет отправляет по сети серверу.

  4. Сервер, используя свой приватный ключ, расшифровывает предварительный секрет, и обе стороны независимо друг от друга вырабатывают Главный секрет, используя криптографическую магию.

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

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

TLS handshake (DHE/ECDHE)

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

TLS-рукопожатие (DHE/ECDHE)
TLS-рукопожатие (DHE/ECDHE)

И снова разберем, что происходит, и посмотрим на отличия:

  1. В первой части отличий никаких: клиент также отправляет запрос на подключение к серверу.

  2. Затем сервер так же возвращает клиенту свой публичный ключ из пары ключей вместе с сертификатом.

  3. А вот с этого момента применяются свойства эфемерных шифронаборов, а также немного криптографической магии. Вместо генерации предварительного секрета и его шифрования на публичном ключе, клиентская сторона вырабатывает свою пару ключей Диффи Хелмана, и открытый (публичный) ключ отправляет на сервер.

  4. Сервер на своей стороне также генерирует ключи с использованием эфемерного алгоритма и передает сгенерированный публичный ключ на сторону клиента.

  5. Вот тут происходит настоящая криптографическая магия, и на основе всей информации стороны независимо друг от друга могут сгенерировать Предварительный и Главный секреты.

  6. После этого на основе Главного секрета вырабатываются сеансовые ключи и начинается процесс передачи зашифрованных данных.

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

Подведем итоги. При использовании TLS_DHE/ECDHE_* сессионный ключ не передается по сети. Общий секрет вырабатывается на основе DH с временными ключами, действительными только для текущей сессии.

К слову, в iOS, при включенном и настроенном App Transport Security, приложение будет использовать PFS по умолчанию. Для детальной настройки реализована возможность управлять этой функцией с помощью ключа NSExceptionRequiresForwardSecrecy в plist файле. Выключение этого режима позволит использовать версии TLS, которые не поддерживают PFS.

“Man In The Middle”, или “Человек посередине“

Теперь, когда мы знаем, что такое HTTPS и как работает установка безопасного соединения, можно поговорить и о такой знаменитой атаке, как “Man In The Middle”. В русскоязычной литературе ее называют “Человек посередине“. Это атака на канал связи, при которой злоумышленник находится в одной сети с вами и обладает контролем над точкой доступа, или каким-то образом может перенаправить вас на свой прокси-сервер внутри сети. Злоумышленник для клиента представляется конечным сервером, а для сервера - клиентом. Он стоит “посередине“ между ними и способен читать и модифицировать трафик, проходящий через него. Но как это может быть реализовано? Ведь трафик зашифрован, и просто так прочитать его не получится. Проще всего это показать на небольшой схеме:

Процесс атаки "Человек посередине"
Процесс атаки "Человек посередине"

Если рассмотреть процесс подключения клиента (мобильного приложения) к своему серверу через подконтрольную точку доступа у злоумышленника, то он будет выглядеть так:

  1. Приложение обращается к своему серверу через точку доступа, подконтрольную злоумышленнику.

  2. На этой точке доступа развернут Proxy-сервер, через который проходит весь трафик.

  3. При запросе на соединение со своим сервером, Proxy “представляется” конечным адресатом, и в ответ на клиентский SSL-запрос возвращает свой собственный сертификат со своим публичным ключом.

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

  5. Таким образом, защищенное подключение устанавливается именно со “зловредным“ сервером и он, как принимающая сторона, видит весь трафик.

  6. С другой стороны, Proxy “представляется“ клиентом для целевого сервера и осуществляет с ним обмен данными, фактически, становясь “посредником“ между клиентом и сервером.

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

Напомним еще раз, что сертификат – это электронный документ, который позволяет проверить аутентичность его предъявителя (пользователя, сервиса, системы). Он обычно содержит публичный ключ владельца, сводную информацию (имя владельца, срок действия и т.д.) и данные о том, кто именно издал этот сертификат.

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

Давайте рассмотрим на практическом примере, как выглядит сертификат при осуществлении прямого соединения и соединения через Proxy. Для этого воспользуемся браузером и замечательным Proxy-сервером с практически неограниченным функционалом - Burp Suite.

Для начала, откроем какой-нибудь сайт по протоколу HTTPS и посмотрим на сертификат, который он нам отдаст. Допустим, мы переходим по адресу https://stingray-mobile.ru/ :

Сертификат сайта при подключении напрямую
Сертификат сайта при подключении напрямую

Мы видим, что сертификат имеет три шага, подписан промежуточным центром сертификации R3 (в свою очередь, подписанным корневым центром сертификации ISRG Root X1), выдан сроком на один год для доменного имени stingray-mobile.ru. Вместе с сертификатом к нам доставлен открытый ключ и его отпечаток. Обычно здесь содержится много информации: кому выдан, кем выдан, различные мета-данные самого сертификата и организаций, принимавших участие в его выдаче, и многое другое. Корневой центр сертификации ISRG Root X1 находится в списке доверенных внутри наших операционных систем. Это значит, что сертификаты, выданные данной компанией, валидные, и им можно доверять.

Теперь откроем наш прокси Burp Suite и пустим трафик от браузера через него. Вот что мы увидим:

Сертификат сайта при подключении через Proxy
Сертификат сайта при подключении через Proxy

Изменилось абсолютно всё. Корневой центр сертификации теперь некий “PortSwigger CA“, организация, выпустившая сертификат, теперь тоже “PortSwigger“, изменился открытый ключ. Неизменным осталось только имя сервера - stingray-mobile.ru. При этом, мы полностью увидим все данные, которые мы отправили на сервер и которые получили от него:

Дамп трафика в открытом виде
Дамп трафика в открытом виде

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

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

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

Как злоумышленник может использовать это для своих целей? Возможен такой сценарий: создается публичная точка доступа с популярным именем, например, METRO_FREE или что-то подобное. Далее, формируется Captive-портал с предупреждением о том, что “мы очень заботимся о вашей безопасности и хотим, чтобы вы всегда оставались защищенными”, ну и так далее. А в конце выводится кнопка, при нажатии на которую на подключившееся устройство скачивается сертификат и запускается мастер установки (для этого в запрос надо включить несколько специальных заголовков). Вот, собственно, и все - сертификат на устройстве, установлен, можно смотреть и слушать трафик!

Другой важный вопрос: как заставить пользователей подключиться к фейковой точке доступа? Здесь на помощь приходят сами устройства и их способ определения, к какой точке и как подключаться. Выбор сети для подключения внутри телефонов очень прост: точка доступа идентифицируется по SSID (имени сети) и паролю. То есть, телефон видит сеть со знакомым именем и пытается подключиться к ней с сохраненным паролем (если установлен флаг “автоматическое подключение“). Но, если точка открытая и пароля на ней нет, то ее идентификация происходит только по имени сети. Но что делать, если рядом - две точки с одинаковым именем? Здесь главное, что нужно злоумышленнику, - подключиться к той точке, у которой сигнал выше. Для этого он может сделать следующее: купить направленную антенну, какой-нибудь одноплатник вроде raspberry, настроить Captive-портал, пойти в публичное место с открытым Wi-Fi, назвать свою точку доступа аналогично и ловить подключения! И тогда трафик клиентов, установивших сертификат на свой телефон, станет доступен тому самому “человеку посередине“.

Похоже на сценарий какого-то фильма, далекий от реальности? А вот и нет. Однажды на одной конференции по безопасности в конце второго дня на сцену позвали участника и вручили ему приз - футболку, на которой были напечатаны адрес его электронной почты и пароль от неё. А на другом мероприятии на экран выводились логины и пароли, которые были пойманы в трафике (конечно, на второй день это заметили, и на экране стали выводить всякие неприличные рисунки в ASCII, и этот аттракцион прикрыли).

А вот и не про конференции. Недавно я обедал в достаточно большом ресторане и подключился к местному Wi-Fi. В ожидании заказа решил проверить, что у них там по адресу маршрутизатора. И каково же было мое удивление, когда перейдя по адресу 192.168.88.1, я попал в открытую админку роутера Mikrotik. Погуляв по настройкам, выяснил, что на нем не только развернута публичная точка доступа, но и подключено некоторое важное оборудование. А сколько таких вот открытых админок, наверное, только Shodan'у одному известно…

Так что подобные схемы - это вполне реальная история, и попасть под сниффинг трафика можно достаточно просто (особенно если вы любите посещать конференции).

Защита канала связи, или SSL Pinning

В качестве защиты мобильного приложения от подобных атак применяют механизм, который называется SSL Pinning. По поводу него всегда много споров (делать ли его, нужен ли он, как избежать “протухания“ сертификата в приложении и т.д.). Для начала, стоит понять, для чего вашему приложению нужен SSL Pinning. Как правило, есть три типа ответа на этот вопрос:

  1. Для защиты клиента, если он вдруг попал в недоверенную/скомпрометированную сеть, если кто-то пытается прослушать его трафик и т.д.

  2. Для того, чтобы нельзя было проанализировать backend и поискать ошибки, например, в бизнес-логике (классический Security through obscurity)

  3. Для того, чтобы быть уверенными, что запрос приходит от легитимного приложения или, другими словами, для защиты от ботов.

На мой взгляд, SSL Pinning призван решать одну единственную задачу - защищать клиента. По поводу двух оставшихся пунктов скажу кратко. Да, SSL Pinning может косвенно повлиять на них. То есть поможет и закрыть ваш API, и защитить от ботов (в теории), но это не серебряная пуля, а лишь способ обезопасить ваших клиентов.

Для того, чтобы избежать копирования ваших приложений и защититься от ботов, лучше всего использовать альтернативные варианты реализации, а не пиннинг. Так, можно применять подпись сообщений, то есть генерировать некоторую уникальную строку, которая бы зависела от состава передаваемой информации в запросе и могла бы высчитываться независимо на клиенте (для подписания) и на сервере (для проверки). Такая подпись сообщений является некоторым аналогом цифровой подписи. Нечто подобное было реализовано у Snapchat и очень долго никто не мог понять, как же генерируется этот токен для подписания. На эту тему вышло даже две статьи (часть первая и часть вторая), где можно попробовать подсмотреть кое-какие подходы. А можно воспользоваться готовыми сервисами, которые есть, например, у Cloudfare, или аналогичными. И тогда никто и ничто, кроме вашего приложения, не сможет подключиться к бэкенду.

А теперь немного о том, что же собственно такое SSL Pinning, как он устроен и как его применять. Суть этого метода в том, чтобы на этапе SSL Handshake после второго шага, когда сервер присылает нам свой сертификат с открытым ключом, приложение проверяло, что определенные параметры этого сертификата совпадают с тем, что ожидает получить приложение (то есть, некоторые данные, которые “зашиты“ в приложении и которые мы ожидаем получить от своего сервера). Схематично это можно представить в виде небольшой схемы:

Процесс SSL Pinning
Процесс SSL Pinning

Теперь посмотрим, что именно можно проверять и на каких этапах, и каким образом это можно реализовать.

Типы SSL Pinning

Certificate Pinning

Первая реализация - это Certificate Pinning. Проверяется непосредственно сам сертификат, включая метаданные (кому он выдан, срок окончания, данные владельца и т.д.). Такая реализация наиболее безопасна, поскольку даже небольшое изменение в сертификате вызовет несоответствие и приведет к невозможности установить соединение.

Но у сертификата есть срок действия, поэтому каждый раз, когда выпускается новый сертификат, должна выходить новая версия приложения.

Public key Pinning

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

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

Какие сертификаты возможно проверять

  1. Сертификат конечного сервера, с которым осуществляется соединение:

    1. Гарантирует с почти 100% уверенностью, что это ваш сертификат, даже если корневой центр был скомпрометирован;

    2. Если сертификат становится недействительным по какой-либо причине (либо по истечении срока действия, либо при компрометации), то осуществить соединение с сервером не получится, пока не выйдет обновление приложения;

    3. Позволяет использовать самоподписанные сертификаты, что может быть полезно при разработке.

  2. Сертификат промежуточного центра сертификации:

    1. Проверяя промежуточный сертификат, вы доверяете промежуточному центру сертификации;

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

  3. Сертификат центра сертификации (корневой сертификат, CA):

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

    2. Если корневой сертификат скомпрометирован, то соединение нельзя считать защищенным, и необходимо срочно менять все сертификаты.

  4. Вся цепочка сертификатов:

    1. Самая надежная проверка с точки зрения безопасности, поскольку проверяются все возможные изменения в любом из сертификатов;

    2. Это самая сложная проверка в поддержке, так как при изменении любого из сертификатов, участвующих в цепочке, необходимо обновлять приложение.

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

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

Еще хочется рассказать об интересном наблюдении из практики, нередко сертификат, с которым будет сравниваться отпечаток сервера, хранится в виде файла на файловой системе (в директории или ресурсах приложения). Мы автоматизировали поиск таких ключей/сертификатов, а после того, как собрали неплохой результат, прогнав через сканер несколько сотен приложений, захотелось посмотреть, а что еще хранят приложения, какие сертификаты или файлы ключей?

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

Такой подход позволяет контролировать появление нежелательных сертификатов в приложении, например, если по ошибке в сборку попадет приватный ключ (да, встречали и такое, когда по невнимательности в приложение попал ключ, который там ну никак не должен был быть):

Найденное хранилище ключей с приватными ключами
Найденное хранилище ключей с приватными ключами

Как проверять

Android Network Security Config

Для каждой из возможных библиотек будет своя собственная реализация. Для Android, например, существует встроенный механизм реализации Pinning на уровне системы, а именно конфигурация сетевого взаимодействия (XML-файл, в котором настраиваются параметры сетевой безопасности). Данная настройка задается специальным атрибутом android:networkSecurityConfig в AndroidManifest.xml.

Пример подключения:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application android:networkSecurityConfig="@xml/network_security_config"
                    ... >
        ...
    </application>
</manifest>

Network Security Config позволяет достаточно просто подключить механизм Certificate Pinning в приложение. Но при этом стоит учитывать определенные нюансы. Рассмотрим конфигурацию, которая с первого взгляда выглядит, как правильно настроенная, и разберем, как ее можно немного улучшить:

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set>
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

Этот пример имеет два небольших недостатка:

  1. Для отпечатка сертификата (pin-set) не установлен срок действия;

  2. Нет резервного сертификата.

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

Чтобы избежать такой паузы, стоит добавить следующий сертификат в настройках “резервных сертификатов“.

Вот пример наиболее корректного использования функционала Certificate Pinning:

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2021-01-01">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

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

iOS App Transport Security

Что касается iOS, то встроенный механизм проверок в AppTransport Security появился только начиная с iOS 14 и очень похож на то, что есть в Android:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSPinnedDomains</key>
    <dict>
        <key>example.org</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSPinnedCAIdentities</key>
            <array>
                <dict>
                    <key>SPKI-SHA256-BASE64</key>
                    <string>r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
                </dict>
            </array>
        </dict>
    </dict>
</dict>

В этом примере конфигурации мы указываем, что отпечаток открытого ключа связан с доменом example.org и его поддоменами, например test.example.org. Но вот поддомены третьего уровня и выше уже в эту проверку не попадают (например notinclude.test.example.org).

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

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSPinnedDomains</key>
    <dict>
        <key>example.net</key>
        <dict>
            <key>NSPinnedLeafIdentities</key>
            <array>
                <dict>
                    <key>SPKI-SHA256-BASE64</key>
                    <string>i9HaIScvf6T/skE3/A7QOq5n5cTYs8UHNOEFCnkguSI=</string>
                </dict>
                <dict>
                    <key>SPKI-SHA256-BASE64</key>
                    <string>i9HaIScvf6T/skE3/A7QOq5n5cTYs8UHNOEFCnkguSI=</string>
                </dict>
            </array>
        </dict>
    </dict>
</dict>

Более подробно о механизме AppTransportSecurity и о сертификатах в том числе, я расскажу в следующих статьях.

В нашем инструменте мы не только умеем выявлять отсутствие пиннинга, но и в обязательном порядке анализируем Network Security Config на предмет неправильной конфигурации и соответствия лучшим практикам безопасности:

Пример найденной небезопасной конфигурации Network Security Config
Пример найденной небезопасной конфигурации Network Security Config

И, конечно, конфигурация App Transport Security не остается без внимания:

Пример найденной небезопасной конфигурации App Transport Security
Пример найденной небезопасной конфигурации App Transport Security

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

OkHttp

При реализации в OkHttp можно воспользоваться классом CertificatePinner.

CertificatePinner certPinner = new CertificatePinner.Builder()
        .add("appmattus.com",
              "sha256/4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=")
        .build();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .certificatePinner(certPinner)
        .build();

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

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

Retrofit

Retrofit применяется поверх OkHttp, поэтому его использование похоже на аналогичные операции с OkHttpClient, как показано в примере выше.

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://appmattus.com")
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build();

HttpUrlConnection

Если используется HttpUrlConnection, рекомендуется пересмотреть подход в сторону OkHttp. Версия HttpUrlConnection, встроенная в Android, зафиксирована, поэтому с обновлениями могут возникнуть сложности.

В документе Android «Security with HTTPS and SSL» предлагаемая реализация основана на pinning сертификатов с помощью собственного TrustManager и SSLSocketFactory. Однако, как и в случае с другими API, в данной рекомендации будут примеры с использованием SPKI.

private void validatePinning(
        X509TrustManagerExtensions trustManagerExt,
        HttpsURLConnection conn, Set<String> validPins)
        throws SSLException {
    String certChainMsg = "";
    try {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        List<X509Certificate> trustedChain =
                trustedChain(trustManagerExt, conn);
        for (X509Certificate cert : trustedChain) {
            byte[] publicKey = cert.getPublicKey().getEncoded();
            md.update(publicKey, 0, publicKey.length);
            String pin = Base64.encodeToString(md.digest(),
                    Base64.NO_WRAP);
            certChainMsg += "    sha256/" + pin + " : " +
                    cert.getSubjectDN().toString() + "\n";
            if (validPins.contains(pin)) {
                return;
            }
        }
    } catch (NoSuchAlgorithmException e) {
        throw new SSLException(e);
    }
    throw new SSLPeerUnverifiedException("Certificate pinning " +
            "failure\n  Peer certificate chain:\n" + certChainMsg);
}
private List<X509Certificate> trustedChain(
        X509TrustManagerExtensions trustManagerExt,
        HttpsURLConnection conn) throws SSLException {
    Certificate[] serverCerts = conn.getServerCertificates();
    X509Certificate[] untrustedCerts = Arrays.copyOf(serverCerts,
            serverCerts.length, X509Certificate[].class);
    String host = conn.getURL().getHost();
    try {
        return trustManagerExt.checkServerTrusted(untrustedCerts,
                "RSA", host);
    } catch (CertificateException e) {
        throw new SSLException(e);
    }
}

Данная имплементация должна быть вызвана следующим образом:

TrustManagerFactory trustManagerFactory =
        TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
// Find first X509TrustManager in the TrustManagerFactory
X509TrustManager x509TrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
    if (trustManager instanceof X509TrustManager) {
        x509TrustManager = (X509TrustManager) trustManager;
        break;
    }
}
X509TrustManagerExtensions trustManagerExt =
        new X509TrustManagerExtensions(x509TrustManager);
...
URL url = new URL("https://www.appmattus.com/");
HttpsURLConnection urlConnection = 
        (HttpsURLConnection) url.openConnection();
urlConnection.connect();
Set<String> validPins = Collections.singleton
        ("4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=");
validatePinning(trustManagerExt, urlConnection, validPins);

В этом случае вызов urlConnection.connect() выполняет SSL Handshake, однако не передает никаких данных, пока не будет вызван urlConnection.getInputStream().

Volley

Стандартный способ использования библиотеки Volley - это Pinning сертификатов, как описывается в статье «Android Security Tip: Public Key Pinning with Volley Library». Проект Github Public Key Pinning with Android Volley library показывает, как можно настраивать SSLSocketFactory для привязки к SPKI.

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

Переопределить HostnameVerifier можно следующим образом:

RequestQueue requestQueue = Volley.newRequestQueue(appContext,
        new HurlStack() {
    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        HttpURLConnection connection = super.createConnection(url);

        if (connection instanceof HttpsURLConnection) {
            HostnameVerifier delegate =
                    urlConnection.getHostnameVerifier();
            HostnameVerifier pinningVerifier =
                    new PinningHostnameVerifier(delegate);

            urlConnection.setHostnameVerifier(pinningVerifier);
        }

        return connection;
    }
});
...
public static class PinningHostnameVerifier
        implements HostnameVerifier {
    private final HostnameVerifier delegate;

    private PinningHostnameVerifier(HostnameVerifier delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean verify(String host, SSLSession sslSession) {
        if (delegate.verify(host, sslSession)) {
            try {
                validatePinning(sslSession.getPeerCertificates(),
                        host, validPins);
                return true;
            } catch (SSLException e) {
                throw new RuntimeException(e);
            }
        }

        return false;
    }
}

Но не стоит забывать, что SSL Pinning - это не единственное, что может помочь в защите сетевого соединения. Другим немаловажным фактором является создание правильного протокола общения приложения с сервером, например, запрет на общение в открытом виде по HTTP или запрет на передачу чувствительной информации в параметрах GET-запросов. О последней проблеме хотелось бы поговорить чуть более подробно.

В случае, если чувствительная информация передается в URL-адресах, она может регистрироваться в самых разных местах, включая:

  • Web-сервер;

  • Любые прямые или обратные прокси-серверы между двумя конечными точками;

  • Заголовок Referrer;

  • Логи Web-сервера;

  • История браузера;

  • Кэш браузера.

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

Найденный пароль, передаваемый в параметрах GET-запроса
Найденный пароль, передаваемый в параметрах GET-запроса

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

Обнаружение уязвимостей отсутствия или некорректной реализации SSL Pinning

К сожалению, во многих приложениях, которые мы проверяем ежедневно, не реализован SSL Pinning. И это достаточно грустно, учитывая, что для его реализации не нужно так много усилий, как может показаться. А его реализация поможет обеспечить защиту ваших клиентов.

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

Выявленная уязвимость с указанием доменов, где был перехвачен трафик
Выявленная уязвимость с указанием доменов, где был перехвачен трафик

Заключение

Очень надеюсь, что эта статья нашла своего читателя и вам было интересно пройти со мной путь от основ HTTP и SSL до атак на канал связи и способов защиты от них.

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

Ссылки

  1. Простым языком об HTTP

  2. Network security configuration

  3. A Security Analyst’s Guide to Network Security Configuration in Android P

  4. https://github.com/square/okhttp/wiki/HTTPS#certificate-pinning

  5. Vulnerability in OkHttp’s Certificate Pinner

  6. Picasso 2 OkHttp 3 Downloader

  7. Pinning

  8. Android Security Tip: Public Key Pinning with Volley Library

  9. Public Key Pinning with Android Volley library

  10. Preventing Downgrade Attacks

  11. Identity Pinning: How to configure server certificates for your app

Комментарии (12)


  1. chuprun
    18.04.2022 09:31
    +1

    спасибо. Скажите, но ведь если мы имеем возможность использовать man in middle, а также исправить простейшую строчку приложения в XML ресурсах Network Security Config, прописав вручную там доверие только на наш поддельный сертификат, то слушать трафик все равно? Например, для поиска бекэнда и уязвимостей в приложении.

    Какие методы защиты могут быть в этом случае? Или же активный клиент со вшитым сертификатом может передавать fingerprint или часть сертификата, а мы его перехватить не сможем и передать дальше/


    1. Mr_R1p Автор
      18.04.2022 11:38

      Всё верно, есть защита в виде SSL Pinning и есть способы эту защиту сломать.

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

      Для того, чтобы таким образом защитить серверную часть от нелегитимных запросов можно использовать различные способы, например подпись каждого request (для проверки, что он не был изменен) или использование дополнительных сервисов, которые на основе обмена ключами позволят установить соединение только из своего приложения.

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


  1. pavelkann
    18.04.2022 10:15

    Очень круто! Спасибо за статью!


    1. Mr_R1p Автор
      18.04.2022 11:38

      Спасибо, я рад, что понравилось!


  1. Knutov_V
    18.04.2022 10:17

    Благодарю! Все по полочкам


    1. Mr_R1p Автор
      18.04.2022 11:39

      Спасибо! Старался описывать все простым языком, так как сам это понимаю)


  1. Elbbazer
    18.04.2022 11:21
    +1

    Так как же обходить корректно настроенный SSL Pinning?


    1. Mr_R1p Автор
      18.04.2022 11:58

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

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

      Есть более интересные подходы, например, попробовать той же Frida отлавливать и сохранять сессионные ключи и потом расшифровывать трафик (тоже много-много нюансов, но подход тоже имеет право на жизнь).


      1. Elbbazer
        18.04.2022 12:44

        Я не сталкивался с разработкой под Android, в Windows я бы сделал перехват API фунций Wininet/WinHttp. В Android что то подобное возможно?


        1. Fi5t
          18.04.2022 13:01

          Фактически так и есть когда речь идет о Frida. Это инструмент для runtime хуков на нужные методы.


  1. skozharinov
    18.04.2022 19:31

    Актуальная на данный момент версия протокола, HTTP 1.1, описана в спецификации RFC 2616

    Уже HTTP/3 на подходе
    История версий протоколов SSL/TLS

    На картинке TLS 1.3 upcoming тоже устарело


    1. Mr_R1p Автор
      18.04.2022 19:33

      Спасибо за внимательность и комментарии!

      Поправлю и в тексте и на картинке!