Привет, меня зовут Андрей Батутин, я Senior iOS Developer в DataArt. В предыдущей статье мы говорили, как можно сниффить трафик нашего мобильного приложения с помощью HTTPS-прокси. В этой обсудим, как обходить SSL Pinning. На всякий случай, рекомендую прочитать первую статью, если вы ее еще не читали: это понадобится для понимания приведенного ниже текста.
Собственно, на практике SSL Pinning применяют, чтобы описанный способ инспекции и модификации трафика мобильного приложения не был доступен плохим парням или любопытному шефу.
Что такое SSL Pinning
В предыдущей статье мы установили на мобильное устройство Charles Root Certificate, что позволило нашему Charles Proxy принимать, расшифровывать, показывать нам трафик, зашифровывать его обратно и отправлять на Dropbox.
Если я как разработчик мобильного приложения хочу, чтобы мой трафик мог инспектировать только мой сервер и никто другой, даже если этот другой установил на устройство свой SSL-сертификат, я могу воспользоваться SSL Pinning.
Его суть сводится к тому, что во время SSL-хендшейка клиент проверяет полученный от сервера сертификат.
В этой статье рассматривается самый простой в реализации способ SSL Pinning с помощью разрешенного списка сертификатов, зашитых в приложение (whitelisting).
Больше о типах SSL Pinning можно почитать здесь.
Реализация SSL Pinning в FoodSniffer
Полный код проекта лежит здесь. Вначале нам надо получить два сертификата в формате DER для двух хостов:
- www.dropbox.com;
- uc9b17f7c7fce374f5e5efd0a422.dl.dropboxusercontent.com.
Второй сервер хранит сам JSON со списком наших покупок.
Чтобы получить сертификаты в нужном формате, я использовал Mozila Firefox.
Открываем в браузере dropbox.com.
Нажимаем на символ замка в адресной строке.
Нажимаем More Information, выбираем Security -> View Certificate.
Затем выбираем Details и находим конечный сертификат в Certificate Hierarchy.
Нажимаем Export и сохраняем в формате DER.
Повторяем ту же процедуру для uc9b17f7c7fce374f5e5efd0a422.dl.dropboxusercontent.com.
Примечание
Для контент-сервера Dropbox (*.dl.dropboxusercontent.com) используется wildcard-сертификат. Значит, сертификат, который вы извлекли для uc9b17f7c7fce374f5e5efd0a422 сервера, будет подходить и для любых других *.dl.dropboxusercontent.com серверов Dropbox.
В результате у меня получилось два файла с сертификатами:
dropboxcom.crt,
dldropboxusercontentcom.crt,
которые я добавил в проект iOS-приложения FoodSniffer.
Затем я добавил extention для FoodListAPIConsumer-класса, в котором и проверяю полученный от сервера сертификат. Для этого я ищу его в списке разрешенных сертификатов, обрабатывая Authentication Challenge-делегат NSURLSessionDelegate-протокола:
extension FoodListAPIConsumer {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
if (validateTrustCertificateList(trust)) {
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
func validateTrustCertificateList(_ trust:SecTrust) -> Bool{
for index in 0..<SecTrustGetCertificateCount(trust) {
if let certificate = SecTrustGetCertificateAtIndex(trust, index){
let serverCertificateData = SecCertificateCopyData(certificate) as Data
if ( certificates.contains(serverCertificateData) ){
return true
}
}
}
return false
}
}
В массиве certificates у меня хранятся Data представления моих разрешенных сертификатов.
Теперь при работающем Charles Proxy приложение будет разрывать связь с ним по причине того, что Charles-сертификат не входит в список разрешенных. Пользователь будет видеть следующую ошибку:
Хакеры повержены!
Но теперь есть одна маленькая проблема — как мне-разработчику мониторить HTTPS-трафик своего же приложения?
Frida
Один из вариантов — отключить SSL Pinning с помощью dynamic code injection фреймворка Frida.
Идея в том, чтобы в процессе разработки приложения метод validateTrustCertificateList всегда возвращал true.
Этого, конечно, можно добится и без dynamic code injection, например, используя #if targetEnvironment(simulator) условие для отключения SSL Pinning на симуляторе, но это слишком просто.
С помощью Frida мы сможем написать скрипт на JavaScript (ловко, правда?), в котором подменим имплементацию validateTrustCertificateList на такую, что всегда возвращает true.
И этот скрипт будет впрыскиваться в приложение уже на этапе исполнения.
Как работает Frida на iOS, вы можете почитать здесь.
Установка Frida (взято отсюда).
sudo pip install frida-tools
Frida-скрипт
Непосредственный скрипт для подмены validateTrustCertificateList функции выглядит так:
// Are we debugging it?
DEBUG = true;
function main() {
// 1
var ValidateTrustCertificateList_prt = Module.findExportByName(null, "_T016FoodSnifferFrida0A15ListAPIConsumerC024validateTrustCertificateD0SbSo03SecG0CF");
if (ValidateTrustCertificateList_prt == null) {
console.log("[!] FoodSniffer!validateTrustCertificateList(...) not found!");
return;
}
// 2
var ValidateTrustCertificateList = new NativeFunction(ValidateTrustCertificateList_prt, "int", ["pointer"]);
// 3
Interceptor.replace(ValidateTrustCertificateList_prt, new NativeCallback(function(trust) {
if (DEBUG) console.log("[*] ValidateTrustCertificateList(...) hit!");
return 1;
}, "int", ["pointer"]));
console.log("[*] ValidateTrustCertificateList(...) hooked. SSL pinnig is disabled.");
}
// Run the script
main();
- Мы находим по полному имени функции указатель на validateTrustCertificateList в бинарнике приложения.
- Оборачиваем указатель в NativeFunction-обертку, указывая тип параметра и выходного значения функции.
- Заменяем имплементация функции validateTrustCertificateList такой, что всегда возвращает 1 (т. е. true).
Весь скрипт лежит в {source_root}/fridascrpts/killCertPinnig.js.
Одина из проблем — как было получено полное имя функции _T016FoodSnifferFrida0A15ListAPIConsumerC024validateTrustCertificateD0SbSo03SecG0CF
Для этого я использовал следующую технику.
- Создал в приложении дополнительный таргет FoodSnifferFrida.
- Подключил к нему библиотеку FridaGadget.dylib, которую взял здесь. Подробнее процедура подключения библиотеки описана здесь.
- Запустил на симуляторе приложение FoodSniffer.
- Использовал данную команду для поиска полного имени функции validateTrustCertificateList:
frida-trace -R -f re.frida.Gadget -i "*validateTrust*" - Получил его в виде:
А затем использовал его в killCertPinnig.js.
Почему такое «странное» имя вышло у функции в конечном итоге и что значат все эти T016 и 0A15, можно посмотреть здесь.
Убийство SSL Pinning
Теперь наконец запустим FoodSniffer с отключенным SSL Pinnig!
Запустим Charles Proxy.
Запустим таргет FoodSnifferFrida в Xcode-проекте в симуляторе. Мы должны увидеть просто белый экран. Приложение ждет, пока к нему подключится Frida.
Запустим Frida для исполнения killCertPinnig.js скрипта:
frida -R -f re.frida.Gadget -l ./fridascrpts/killCertPinnig.js
Дождемся подключения к iOS-приложению:
Продолжим работу приложения с помощью команды %resume:
Теперь мы должны увидеть список продовольствия в приложении:
И JSON в Charles Proxy:
Профит!
Вывод
Frida — это как Wireshark для бинарников. Она работает на iOS, Android, Linux, Windows-платформах. Этот фреймворк позволяет отслеживать вызовы методов и функций — и системных, и пользовательских. А еще подменять значения параметров, возвращаемых значений и имплементации функций.
Обход SSL Pinning в условиях процесса разработки с помощью Frida может показаться немного overkill. Меня он привлекает тем, что мне не надо иметь в самом приложении специфичной логики для отладки и разработки приложения. Такая логика загромождает код и при некорректной имплементации может просочиться в релизную версию сборки (макросы, привет вам!).
Кроме того, Frida применима и для Android. Что дает мне возможность облегчить жизнь всей своей команде и обеспечить плавный процесс разработки всей линейки продукта.
Frida позиционирует себя как black box process code injection tool. С ней возможно, не меняя непосредственный код iOS-приложения, добавлять в runtime логирование вызовов методов, что может быть незаменимо при отладке сложных и редких багов.
Комментарии (7)
DnV
27.09.2018 06:22В рантайме инжектить код лучше несчастного флага компилятора… чего только не услышишь. Кто не в теме, такие баловство программистов обычно дорого обходится пользователям как по производительности, так и по безопасности. Эппл, кстати, резонно запретила впредь публиковать в аппстор приложения с динамически изменяемым кодом.
house2008
27.09.2018 09:41Спасибо. Ранее не слышал про Frida. Но мне было бы жалко столько времени потратить на простую подмену возвращаемого значения. Я предпочитаю свизлинг, у меня всегда в проекте есть один ObjC файл именно для такого рода баловства и я знаю, что ничего не сломаю в продакшене, потому что он у меня никогда не коммитается.
NikolayJuly
27.09.2018 17:32Я очень скептически был настроен, когда открывал статью. Название — кликбэйт. Именовали бы честно: дебажим при использовании ssl pinning’а.
Использовать js, чтобы в дебаге отключить ssl pinning, тоже спорная затея. Выставляете флаг в настройках проекта, ну или прям DEBUG значение проверяете. Всё равно история с получением имени функции не должна сработать в релизе. Вы же функцию не публичной делали, а значит компилятор порежит этот символ. А ксли публичной… то плохие дяди и так её потом поменяют :)
aim17
К сожалению, предыдущая статья по ссылке не открывается. А очень хотелось почитать…
DataArt Автор
Загадка :( У нас все работает. Давайте попробуем поставить ссылку и сюда: habr.com/company/dataart/blog/419677.
andreymal
Она в черновиках habr.com/post/419677
DataArt Автор
Удивительная история! Спасибо!