Как перехватить защищенный трафик

перехватить защищенный трафик ssl

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

Немного об HTTP и о том, при чем здесь OpenSSL

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

За время своего существования HTTP оброс кучей разных фич и наворотов, устраняющих недостатки его изначальной «вебовой» версии: Cookies для устранения Stateless, Keep-Alive и Long polling для имитации непрерывности соединения, концепция REST, бинарный HTTP/2 с повсеместным сжатием и многое другое. Забегая вперед, отмечу, что HTTP/2 вообще разрабатывался с учетом использования вместе с TLS 1.2+.

РЕКОМЕНДУЕМ:
Сетевые атаки и защита от них

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

Параллельно с HTTP шло развитие SSL, а их синтез — HTTPS — постепенно захватывал долю трафика в мировом интернете, пока в один прекрасный момент в конце января 2017 года не превысил половину. Сейчас его доля стремится к 80%. Многие увидели смысл в шифровании данных, а тех, кто не успел, стали поторапливать в том числе и разработчики браузеров. В итоге все снова приняли это (что хорошо!) и начали повсеместно применять в своих приложениях, многие из которых и так уже работали по привычному HTTP.

В Mozilla публикуют статистику на основе телеметрии Firefox, где видно, какой процент веб-страниц загружают по HTTPS. На сайте LetsEncrypt можно посмотреть графики, основанные на этих данных.

Развитие SSL, а впоследствии и TLS (SSL 3.0+) во многом определялось развитием опенсорсного проекта OpenSSL. Он медленно, но верно впитывал в себя все новые и новые спецификации RFC. Как известно, велосипеды изобретать никто не любит, поэтому библиотека OpenSSL стала де-факто стандартом при имплементации защищенного транспорта для HTTP, и теперь ее следы можно обнаружить в бессчетном количестве софтверных проектов.

Как работает классический HTTPS

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

TLS Handshake
TLS Handshake

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

Для проверки используются центры сертификации, которые бывают корневыми и промежуточными. Публичных корневых центров мало, и обо всех них знает каждый клиент SSL/TLS. А промежуточных центров может быть очень много — единственное их отличие от корневых в том, что выпускаемые ими сертификаты подписываются приватным ключом вышестоящего CA. Например, если посмотреть на путь сертификации google.com на рисунке ниже, то корневым будет сертификат, выданный GlobalSign, а промежуточным — Google Internet Authority.

Перехват трафика. Цепочка сертификатов Google
Цепочка сертификатов Google

Объяснение хендшейка TLS 1.2 байт за байтом

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

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

Исследуем принципы работы OpenSSL на подопытном приложении

Библиотека OpenSSL «в вакууме» по умолчанию не доверяет никому, а чтобы инициировать процесс появления этого доверия, библиотеке необходимо сообщить о том, кому, собственно, мы намерены доверять. Если рассмотреть пример классического клиента TLS, приведенный в wiki.openssl, то можно там заметить загрузку доверенных сертификатов непосредственно перед осуществлением запроса HTTP:

Кроме функции SSL_CTX_load_verify_locations существует еще несколько похожих способов загрузки проверенных сертификатов внутрь OpenSSL. Наверняка разработчики, которые писали свои клиенты TLS, не стеснялись подсматривать подобные общедоступные примеры.

Попробуем отследить поведение представленного классического клиента TLS на каком-нибудь реальном приложении. Для начала возьмем что-нибудь простое, что могло бы быть связано с TLS, — например, библиотеку OkHttp. Она обеспечивает коммуникации по HTTP/S в бесчисленном количестве современных приложений для Android.

Заранее замечу, что OkHttp написана на Java и работает на JVM, поэтому сама по себе она не интересна, так как является своеобразной оберткой более интересной составляющей — низкоуровневой имплементации OpenSSL для Android. Вот ей-то мы и займемся.

Готовим тестовое приложение

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

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

UI подопытного приложения для Android
UI подопытного приложения для Android

Совершенно стандартный код, кочующий по Ctrl-C и Ctrl-V из одного проекта в другой, подвергаясь при этом несущественным изменениям. Итак, запускаем новоиспеченное приложение и сразу же заглядываем в разметку его памяти.

Поиск следов OpenSSL в приложениях для Android
Поиск следов OpenSSL в приложениях для Android

В поисках истины

Становится очевидно, что в Android есть библиотека, которая ведет к OpenSSL. Но загрузилась она не из-за использования HTTPS, а задолго до появления на экране основного приложения — на этапе «конструирования» процесса приложения ( fork из app_process) и проверки его подлинности. Это связано с особенностями распространения приложений для Android, в которые мы погружаться не будем.

Чтобы убедиться, что найденная библиотека связана с OpenSSL, можно посмотреть ее таблицу символов и затем сравнить с примитивами эталонной openssl. Получить ее можно, например, компиляцией исходников из официального репозитория OpenSSL. Самое время убедиться, не используется ли рутина libssl.so, когда пользователь нажимает кнопки в нашем приложении.

Библиотека libssl на Android — это не OpenSSL в чистом виде. Эта ОС сделана в Google, а у Google почти на все есть свой форк. Для OpenSSL — это BoringSSL.

Наконец мы подошли к реализации потенциала нашего приложения — обнаружению в используемом им клиенте (OkHttp) на более низком уровне следов «классического клиента TLS».

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

Полный стек вызовов получился внушительным. Зато он полностью покрывает одиночный запрос HTTPS из OkHttp. Сейчас с включенным прокси-сниффером приложение вылетает с ошибкой:

Что не удивительно. На полученном фрагменте стека мы видим, что какие-то шаги классического клиента были пройдены, а какие-то — нет. Но самое главное, что до хендшейка не было загрузки доверенных сертификатов! А как известно, без этого успешного исхода хендшейка не добиться.

РЕКОМЕНДУЕМ:
Какую информацию сливают китайские Android смартфоны

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

Одна из таких функций — SSL_CTX_set_cert_verify_callback, которая засветилась в стеке вызовов. Если верить документации, то сделать этот колбэк сможет очень многое. По сути, он целиком делегирует приложению процедуру проверки подлинности сертификата сервера. Если отследить колбэк, то это, возможно, даст ответы на вопросы об источнике доверенных сертификатов в Android.

Настал момент воспользоваться преимуществом открытых исходников OpenSSL и посмотреть на функцию SSL_CTX_set_cert_verify_callback изнутри. Поиск по коду OpenSSL на GitHub быстро выведет нас к файлу ssl_lib.c:

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

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

Поиск места использования делегата по проверке сертификата в OpenSSL
Поиск места использования делегата по проверке сертификата в OpenSSL

Место использования делегата в OpenSSL — это часть функции ssl_verify_cert_chain. Точно такое же место можно обнаружить в BoringSSL (который и входит в Android в виде libssl) в коде функции ssl_crypto_x509_session_verify_cert_chain.

Что же в этом коде интересного? Здесь (и только здесь) передается управление пользовательской колбэк-функции. Но самое главное: стало ясно, что при отсутствии таковой (например, если не было явного вызова SSL_CTX_set_cert_verify_callback) управление передается стандартной функции верификации из OpenSSL, она называется X509_verify_cert.

Давай теперь посмотрим, какой именно код получит управление в том случае, если в результате работы клиента OkHttp будет установлен колбэк. Нам в этом поможет любой отладчик — например тот, что встроен в IDA Pro.

Callback в Android, получающий управление при проверке сертификата сервера
Callback в Android, получающий управление при проверке сертификата сервера

Колбэк находится в библиотеке libjavacrypto. Это, как потом выяснилось, воплощение Java Security Provider (JCP) для Android под названием Conscrypt c имплементацией Java Cryptography Extension (JCE). Приятная новость, ведь буквы OSP в AOSP обозначают Open Source Project!

Определяем источники сертификатов

Так же, как и в случае с OpenSSL, поищем исходники Conscrypt в AOSP и место, похожее по структуре на то, которое нам удалось найти с помощью встроенного отладчика. Таким местом оказывается функция cert_verify_callback в файле native_crypto.cc.

Посмотри, насколько структура этой функции похожа на псевдокод, сгенерированный плагином Hex-Rays:

Что делает найденная нативная функция? Она, получив ссылку на объект SSL, олицетворяющий экземпляр безопасного соединения, получает через него цепочку сертификатов сервера и передает ее в Java-метод с кричащим названием verifyCertificateChain.

Если этот метод в процессе своей работы не бросает исключения, то результат верификации после передачи управления колбэку ( result) будет успешным ( ssl_verify_ok). У метода verifyCertificateChain в свою очередь есть ряд реализаций в Conscrypt, и все они очень-очень похожи:

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

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

Теперь мы точно знаем об источнике доверенных сертификатов, которые OpenSSL получает при использовании клиента OkHttp. А также обо всей цепочке вызовов, происходящих в ходе установки безопасного соединения в реальной среде, эксплуатирующей OpenSSL, — Android.

HTTPS как средство защиты от пользователя

Подавляющее большинство «классических» клиентов HTTPS использует системное хранилище сертификатов. Главный недостаток такого подхода — в полном доверии сертификатам из папки устройства. Это значит, что какие бы сертификаты ни находились в хранилище у пользователя, они пройдут проверку подлинности и соединение будет успешно установлено. Таким сертификатом может быть сертификат прокси-сервера, что приведет классическую схему к несколько другому виду: она будет с «двойным рукопожатием».

Схема установки SSL-соединения через «доверенный» прокси-сервер
Схема установки SSL-соединения через «доверенный» прокси-сервер

По идее для защиты от MITM пользователь должен самостоятельно заботиться о содержимом своих системных хранилищ. Но что, если он сам пожелает посмотреть, чем приложение обменивается с сервером? Здесь на помощь приходит технология SSLPinning.

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

Механизм такого доверия предполагает, что часть информации о сертификате сервера понадобится зашить в код проверки. Обычно это хеш SHA-сертификата, именуемый «пином». В нашем тестовом приложении использовался хеш SHA-256, который передается при инициализации OkHttp.

РЕКОМЕНДУЕМ:
Что такое и как работает IPsec

Есть ряд популярных подходов к реализации SSLPinning в приложениях. Один из них, как мы знаем, реализуется библиотекой OkHttp. Для большинства этих способов есть контрприемы: в виде скриптов Frida, системных модулей или, на худой конец, просто описаний алгоритмов обхода.

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

Обходим SSLPinning

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

Способ первый: глушим проверки OpenSSL

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

Нам уже известен «толстый» callback, который пользователь устанавливает для переопределения процедуры проверки цепочки сертификатов. За это отвечает функция SSL_CTX_set_cert_verify_callback, а используется этот колбэк в ssl_verify_cert_chain следующим образом:

Как мы это можем использовать? Самым правильным вариантом будет «глушение» пользовательского колбэка. Для этого достаточно нарушить условие:

То есть сделать так, чтобы app_verify_callback был NULL. Чтобы это произошло, нужно отменить эффект вызова SSL_CTX_set_cert_verify_callback, причем желательно внутри самой функции.

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

Далее остается решить, что делать с частью else. Когда происходит вызов X509_verify_cert, управление передается в x509_vfy.c. Функция производит много действий, часть из которых полагается на то, что OpenSSL будет проинициализирован хранилищем сертификатов.

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

Здесь же выясняется, что успешное с точки зрения проверки цепочки сертификатов возвращаемое значение X509_verify_cert — это 1 (true). В ARM возврат значения происходит через регистры R0-R3. В нашем же случае все сводится к регистру R0, поскольку тип возвращаемого значения прост. Попробуем перезаписать этот код имитацией успешной отработки X509_verify_cert (заодно не забываем и про устранение аргумента функции):

Для реализации этого нужно напрямую записать в память 8 байт машинного кода (два раза по 4 байта, поскольку 4 байта — фиксированный размер инструкции в ARM). Конвертацию в машкод произведем с помощью rasm из пакета утилит radare2:

Прибегаем к Frida для завершения операции глушения:

И в итоге добиваемся успешного прохождения всех низкоуровневых проверок в ssl_verify_cert_chain (их глушения). Два наших действия в результате привели к тому, что теперь у нас есть возможность подслушивать HTTPS, работающий в «штатном» режиме (без защиты SSLPinning) и без импорта сертификатов из системного хранилища. Нажатие на первую кнопку тестового приложения теперь приводит не к исключительной ситуации SSLHandshakeException, а к тому, что ты видишь на картинке.

Пара простых операций на нижнем уровне OpenSSL дала определенные плоды
Пара простых операций на нижнем уровне OpenSSL дала определенные плоды

Однако при нажатии на вторую кнопку тестового приложения («Do Https with pin!») перехватывать трафик пока все еще не получается, что наводит на мысль о дополнительных проверках. Давай же узнаем, что это за проверки!

Какая-то часть приложения нам все еще не доверяет
Какая-то часть приложения нам все еще не доверяет

Поиск места дополнительной проверки сертификатов

Если продолжить дампить стек вызовов функций OpenSSL после нажатия на кнопку приложения, то можно понять, что на уровне libssl все проверки прошли успешно. Это спровоцировало дальнейшую последовательность действий:

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

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

Функция возвращает ссылку на кастомный тип данных — STACK_OF(X509). OpenSSL использует Stack API для удобного взаимодействия с коллекциями объектов. Цепочка сертификатов является, по сути, стеком. Если посмотреть представление этой структуры данных в памяти, то можно обнаружить, что указатель на стек сертификатов находится во втором поле этой структуры (размер указателя составляет 8 байт):

По указателю друг за другом хранятся ссылки на экземпляры структуры X509 (в цепочке два сертификата):

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

Однако в ходе исследования я нашел одну интересную особенность работы OpenSSL, которая позволяет существенно упростить подделку цепочки. Дело в том, что после установки и закрытия соединения структура данных STACK_OF(X509) не удаляется из памяти (память не очищается, хотя ссылки на нее могут и исчезнуть). Это позволяет заново использовать ее в последующих соединениях, пока занятая ранее память не будет выделена под что-то еще.

РЕКОМЕНДУЕМ:
Как убить защитный драйвер в Windows

Заново использовать память мы будем следующим образом. Первое соединение по HTTPS будет установлено без прокси-сниффера, а последующие — через него. Это позволит получить валидную цепочку сертификатов при первом соединении, запомнить ссылку на нее и затем возвращать эту ссылку во всех последующих соединениях.

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

Устранение «лишних» перепроверок сертификата

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

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

Теперь у нас достаточно информации, чтобы реализовать задуманное. Снова обращаемся за помощью к фреймворку Frida для модификации памяти. Следующий фрагмент описывает завершающий пазл глушения OpenSSL:

Результат не заставляет себя долго ждать: выполняем первый запрос и включаем Burp. Весь остальной трафик теперь совершенно беспрепятственно будет проходить перед нашими глазами.

SSLPinning bypassed!
SSLPinning bypassed!

Интересно, что SSLPinning на уровне введенного в Android 7 Network security configuration легко обходится и без подмены цепочки сертификатов — достаточно заглушить callback.

Способ второй: игнорируем проверки OpenSSL

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

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

Забираем полезные данные до отправки

Продолжим дальше изучать пример демонстрационного клиента TLS, взятого в wiki.openssl. Замечаем, что после успешного рукопожатия с сервером клиент начинает отправку полезных данных:

Функция BIO_puts в какой-то степени говорит сама за себя. Но все не так просто, как кажется. Это своего рода абстракция OpenSSL над I/O. Если немного погуглить, то выясняется, что таких функций целая группа, синонимичная функциям на C. Одна из них — BIO_write, она выполняет в целом то же, что и BIO_puts. Кроме функций абстракции BIO в OpenSSL есть и другие функции для выполнения I/O. Самые интересные для нас — это пара SSL_write/ SSL_read.

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

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

А вот пример снятия трафика в SSL_write после успешного хендшейка c google.com (по бинарному HTTP/2) под защитой SSLPinning:

И ответ из SSL_read:

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

Забираем полезные данные до шифрования

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

Это приводит к тому, что в собранных приложениях присутствует семейство функций I/O из OpenSSL, но используются они по минимуму — либо не используются вовсе. Но есть и приятная новость: если разработчики, внедряющие в свои проекты OpenSSL, частенько и избегают прямого использования сетевой логики в виде SSL_write, то без криптографической составляющей OpenSSL обойтись уже сложнее. Как минимум потому, что алгоритмы шифрования остаются неизменными даже при имплементации собственного сетевого стека. Разработка же собственного криптографического алгоритма чревата появлением дополнительного источника ошибок.

По ряду причин весь OpenSSL разделен на две большие части: уже рассмотренная libssl, где реализована рутина для протоколов SSL/TLS, и libcrypto — с криптографическими алгоритмами. Первая зависит от второй, а вот вторая библиотека полностью автономна. Если заглянуть в таблицу символов not-stripped-версии libcrypto, то можно увидеть много интересного.

Внутренности криптографической составляющей OpenSSL
Внутренности криптографической составляющей OpenSSL

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

Алгоритм для симметричного шифрования и метод выработки общего ключа определяется на этапе хендшейка: сервер и клиент обмениваются друг с другом наборами криптографических возможностей, которые они поддерживают. Такие наборы называются СipherSuites. Они бывают разными, ознакомиться с их вариантами можно в официальной документации OpenSSL.

Узнать о выбранном CipherSuite всегда можно путем перехвата пакетов TCP, например с помощью Wireshark. Однако иногда встречаются экзотические шифронаборы, которые не удается определять в автоматическом режиме. Тут поможет таблица их кодов.

Зачем нам знать о CipherSuites? Дело в том, что от выбранного набора зависит диапазон используемых функций libcrypto.

Например, если сервер и клиент договорились (последнее слово тут за сервером) использовать для шифрования AES в режиме CBC, то в названиях функций из libcrypto обязательно будут буквы aes и cbc. На практике наиболее часто встречается шифронабор TLS_RSA_WITH_AES_128_GCM_SHA256: AES в режиме GCM с длиной ключа в 128 бит, за работу которого в libcrypto отвечает такая связка:

Перехват буфера in в первом случае и out во втором позволит получить то же содержимое, что мы перехватили при стандартном использовании OpenSSL — с помощью SSL_write и SSL_read. Главная причина здесь в том, что с момента, как буфер попал в SSL_write/ SSL_read, он никак не меняется до шифрования или дешифровки в BIO_read/ BIO_write.

Во многих других шифронаборах при симметричном шифровании внутри libcrypto используется высокоуровневый интерфейс криптографических функций OpenSSL — EVP (EnVeloPe). Через него можно получать доступ к криптографии, абстрагируясь от конкретного алгоритма и режима его функционирования. При таком сценарии, вероятнее всего, будут сделаны вызовы EVP_DecryptUpdate/ EVP_EncryptUpdate. Их перехват приведет к аналогичному результату:

Где универсальность?

Теперь мы знаем принципы работы OpenSSL и можем пользоваться тем, что тысячи разработчиков по всему миру не стесняются зашивать части этой библиотеки в свои проекты. А значит, наши возможности по обходу SSLPinning существенно расширяются! Самое сложное теперь — это успешно идентифицировать нужные кусочки OpenSSL в чужих программах.

Ищем следы OpenSSL по строкам и функциям

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

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

Попробуем отыскать, например, ssl_client в статически скомпилированной библиотеке libmonochrome_base.so, которая используется в Chrome.

Поиск нужной функции из OpenSSL по строке с помощью radare2
Поиск нужной функции из OpenSSL по строке с помощью radare2

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

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

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

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

Ищем следы OpenSSL с помощью сигнатурного анализа

Для примера возьмем нативную реализацию стека HTTP компании Facebook — libliger. В Facebook не скрывают, что используют «наработки сообщества», что для нашего случая может означать и OpenSSL.

Для генерации сигнатур воспользуемся встроенным в radare2 движком Zignatures. Для начала скомпилируем все необходимые библиотеки (BoringSSL и OpenSSL) для нужной архитектуры и убедимся, что их действительно можно сопоставить с исследуемым бинарником для сигнатурного анализа:

Генерируем с помощью Zignatures сигнатуры для каждого файла .so и сохраняем их для дальнейшего использования:

Затем запускаем процедуру поиска сигнатур внутри libliger:

И спустя некоторое время смотрим на результаты поиска:

Успешно удалось найти только одну из трех рассмотренных функций, пригодных в роли точек перехвата. Что ж, этого хватит для наших методов SSLUnPinning! Кстати, найти *ssl_verify_cert_chain в libliger удалось предыдущим методом — поиском по строке ssl_client. Ничто не мешает сочетать способы.

Движок Zignatures придется по душе не всем, поэтому в качестве дополнения можно воспользоваться технологией FLIRT из HexRays.

FLIRT изначально задумывался как инструмент, который позволял бы разделить исследуемое приложение на «известную часть», исходники которой были написаны непосредственным разработчиком и которые можно найти на просторах интернета, и «неизвестную часть» — собственно, уникальный код.

Можно пойти одним из двух путей: поискать необходимые для FLIRT сигнатуры либо создать их самостоятельно. Во многих сборниках сигнатур FLIRT уже присутствуют в том числе сигнатуры для OpenSSL: например, для Ubuntu и Windows, а также для OpenSSL, скомпилированной в MSVC разных версий.

Кроме того, можно самостоятельно сгенерировать сигнатуры FLIRT из unstripped-бинарника либо архива .ar с помощью утилит pelf/sigmake из пакета FLAIR tools, после чего загрузить их в IDA Pro и посмотреть на количество совпадений по итогам сканирования.

РЕКОМЕНДУЕМ:
Анализ исполняемых файлов в IDA Pro

Заключение

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

Понравилась статья? Поделиться с друзьями:
Добавить комментарий