Обход SSL Pinning в iOS-приложении

28 сентября
Андрей Батутин
Обход SSL Pinning в iOS-приложении
Привет, меня зовут Андрей Батутин, я 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 для двух хостов:

Второй сервер хранит сам JSON со списком наших покупок.

Чтобы получить сертификаты в нужном формате, я использовал Mozila Firefox.

Открываем в браузере dropbox.com

Нажимаем на символ замка в адресной строке.

screen dropbox

Нажимаем More Information, выбираем Security -> View Certificate.

screen dropbox

Затем выбираем Details и находим конечный сертификат в Certificate Hierarchy.

screen dropbox

Нажимаем Export и сохраняем в формате DER.

screen dropbox


Повторяем ту же процедуру для uc9b17f7c7fce374f5e5efd0a422.dl.dropboxusercontent.com.

Примечание

Для контент-сервера Dropbox (*.dl.dropboxusercontent.com) используется wildcard-сертификат. Значит, сертификат, который вы извлекли для uc9b17f7c7fce374f5e5efd0a422 сервера, будет подходить и для любых других *.dl.dropboxusercontent.com серверов Dropbox.

В результате у меня получилось два файла с сертификатами:

dropboxcom.crt,
dldropboxusercontentcom.crt,

которые я добавил в проект iOS-приложения FoodSniffer.

screen tree

Затем я добавил 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-сертификат не входит в список разрешенных. Пользователь будет видеть следующую ошибку:

phone screen

Хакеры повержены!

Но теперь есть одна маленькая проблема — как мне-разработчику мониторить 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();


  1. Мы находим по полному имени функции указатель на validateTrustCertificateList в бинарнике приложения.
  2. Оборачиваем указатель в NativeFunction-обертку, указывая тип параметра и выходного значения функции.
  3. Заменяем имплементация функции validateTrustCertificateList такой, что всегда возвращает 1 (т. е. true).

Весь скрипт лежит в {source_root}/fridascrpts/killCertPinnig.js.

Одина из проблем — как было получено полное имя функции _T016FoodSnifferFrida0A15ListAPIConsumerC024validateTrustCertificateD0SbSo03SecG0CF

Для этого я использовал следующую технику.

  • Создал в приложении дополнительный таргет FoodSnifferFrida.
  • Подключил к нему библиотеку FridaGadget.dylib, которую взял здесь. Подробнее процедура подключения библиотеки описана здесь.
  • Запустил на симуляторе приложение FoodSniffer.
  • Использовал данную команду для поиска полного имени функции validateTrustCertificateList:
    frida-trace -R -f re.frida.Gadget -i "*validateTrust*"
  • Получил его в виде:
screen

А затем использовал его в killCertPinnig.js.

Почему такое «странное» имя вышло у функции в конечном итоге и что значат все эти T016 и 0A15, можно посмотреть здесь.

Убийство SSL Pinning

Теперь наконец запустим FoodSniffer с отключенным SSL Pinnig!

Запустим Charles Proxy.

Запустим таргет FoodSnifferFrida в Xcode-проекте в симуляторе. Мы должны увидеть просто белый экран. Приложение ждет, пока к нему подключится Frida.

screen

Запустим Frida для исполнения killCertPinnig.js скрипта:

frida -R -f re.frida.Gadget -l ./fridascrpts/killCertPinnig.js

Дождемся подключения к iOS-приложению:

screen

Продолжим работу приложения с помощью команды %resume:

Теперь мы должны увидеть список продовольствия в приложении:

screen


И JSON в Charles Proxy:

screen tree

Профит!

Вывод

Frida — это как Wireshark для бинарников. Она работает на iOS, Android, Linux, Windows-платформах. Этот фреймворк позволяет отслеживать вызовы методов и функций — и системных, и пользовательских. А еще подменять значения параметров, возвращаемых значений и имплементации функций.

Обход SSL Pinning в условиях процесса разработки с помощью Frida может показаться немного overkill. Меня он привлекает тем, что мне не надо иметь в самом приложении специфичной логики для отладки и разработки приложения. Такая логика загромождает код и при некорректной имплементации может просочиться в релизную версию сборки (макросы, привет вам!).

Кроме того, Frida применима и для Android. Что дает мне возможность облегчить жизнь всей своей команде и обеспечить плавный процесс разработки всей линейки продукта.

Frida позиционирует себя как black box process code injection tool. С ней возможно, не меняя непосредственный код iOS-приложения, добавлять в runtime логирование вызовов методов, что может быть незаменимо при отладке сложных и редких багов.