Обход 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.

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





    Нажимаем 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();
    

    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*"
    • Получил его в виде:

    А затем использовал его в 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 логирование вызовов методов, что может быть незаменимо при отладке сложных и редких багов.
    • +12
    • 4,5k
    • 9

    DataArt

    117,00

    Технологический консалтинг и разработка ПО

    Поделиться публикацией

    Похожие публикации

    Комментарии 9
      +1
      К сожалению, предыдущая статья по ссылке не открывается. А очень хотелось почитать…
      0
      В рантайме инжектить код лучше несчастного флага компилятора… чего только не услышишь. Кто не в теме, такие баловство программистов обычно дорого обходится пользователям как по производительности, так и по безопасности. Эппл, кстати, резонно запретила впредь публиковать в аппстор приложения с динамически изменяемым кодом.
        0
        Спасибо. Ранее не слышал про Frida. Но мне было бы жалко столько времени потратить на простую подмену возвращаемого значения. Я предпочитаю свизлинг, у меня всегда в проекте есть один ObjC файл именно для такого рода баловства и я знаю, что ничего не сломаю в продакшене, потому что он у меня никогда не коммитается.
          0

          Я очень скептически был настроен, когда открывал статью. Название — кликбэйт. Именовали бы честно: дебажим при использовании ssl pinning’а.
          Использовать js, чтобы в дебаге отключить ssl pinning, тоже спорная затея. Выставляете флаг в настройках проекта, ну или прям DEBUG значение проверяете. Всё равно история с получением имени функции не должна сработать в релизе. Вы же функцию не публичной делали, а значит компилятор порежит этот символ. А ксли публичной… то плохие дяди и так её потом поменяют :)

            +1
            SSL Pinning применяют, чтобы описанный способ инспекции и модификации трафика мобильного приложения не был доступен плохим парням
            Что за бред? Плохим парням ничего не помешает на своем устройстве засниффать и модифицировать трафик.
              0
              >Такая логика загромождает код и при некорректной имплементации может просочиться в релизную версию сборки
              Подключенная Frida как-бы тоже рискует просочиться, но это уже дело вкуса.

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

              Самое читаемое