Шифруем CoreML +6


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

мое лицо, когда я находил незашифрованную модель
мое лицо, когда я находил незашифрованную модель

В 2020 году Apple представила удобный способ деплоя и шифрования CoreML моделей с помощью Apple Cloud. Это довольно мощный инструмент, который дает нам следующие возможности:

  • Independent development. Возможность обновлять модели на девайсах юзеров без апдейта приложения

  • Model collections. Возможность объединять модели в коллекции и гарантировать их консистентное обновление. Удобно для случая, когда для одной фичи используется несколько моделей.

  • Targeted deployments. Возможность поставлять разные ML модели в зависимости от правил: девайса, версии iOS и т.д.

  • Model encryption. Возможность поставлять модель в зашифрованном виде

Apple Encryption

Остановимся подробней на последнем пункте. Рассмотрим шаги для шифрования ML модели, которую мы будем поставлять вместе с бандлом приложения.

  1. Сначала нам нужно сгенерировать ключ для нашей модели и сохранить его на диске. Для в Project Navigator выбираем нашу модель. Открываем вкладку Utilities и нажимаем Create Encryption Key. В появившемся окне нужно выбрать именно ту команду, под которой мы будем релизить приложение. Нажимаем Continue. Ключ будет сгенерирован и сохранен на диске.

  1. Далее нам нужно указать для Xcode, что мы хотим во время сборки нашего приложения при компиляции модели зашифровать ее с нашим ключом. Для этого выбираем таргет нашего приложения, идем Build Phases ->  Compile Sources и добавляем для нашей модели в Compiler Flags: --encrypt $KeyPathOnDisk.

  1. Готово. Модель зашифрована. Теперь осталось обратиться к ней в коде таким образом, чтобы мы смогли ее дешифровать. Для этого вместо init метода для создания MLModel нужно использовать асинхронный метод load, который появился в iOS 14. Ключ, который мы использовали для шифрования, в момент создания сохраняется в Apple Cloud. В момент первого обращения к load он скачивается и сохраняется локально на девайсе. Поэтому очень важно, чтобы в момент первого обращения был доступ к сети. При вызове load модель дешифруется и загружается в память. На диске же по-прежнему остается зашифрованная ML модель.

MLModel.load(contentsOf: modelURL) { result in   
  switch result {   
  case .success(let loadedModel):       
    print("Successfully loaded model \(loadedModel).")       

    // Use the loaded model for predictions.       
    // ...   
  
  case .failure(let error):       
    print("Error loading model: (error).")   
  }
}

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

Custom Encryption

Для большинства случаев решение от Apple отлично работает, но бывают исключения, когда приходится задуматься над кастомным решением. Основная причина для этого - кастомный деплой ML моделей, когда нам нужно поставлять ML модели с наших серверов. Несколько возможных причин для этого:

  1. Необходимость  более тонкой настройки таргетирования моделей

  2. В компании существует централизованный процесс хранения и апдейта ресурсов приложения, а также требование ему следовать.

  3. A/B тестирование моделей.

  4. Внутренние политики безопасности компании, не позволяющие хранить контент, представляющий интеллектуальную ценность, на серверах Apple.

  5. Поддержка iOS 13 и ниже.

Технически можно пойти по пути, когда мы шифруем модель, чтобы ее забандлить, но по факту не бандлить ее, а доставать зашифрованную, скомпилированную модель и загружать ее на наши сервера. Однако при таком решении, помимо отсутствия возможности автоматизировать процесс деплоя ML модели, значительно повышается риск ошибки, в силу того, что ключ мы будем хранить в Apple Cloud, а саму модель - на наших серверах. 

Давайте рассмотрим возможное решение, если перед нами встала задача кастомного шифрования.

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

  2. Далее нам нужно зашифровать наш файл. Для этого можно использовать популярный sha256 либо любой другой алгоритм.

  3. Зашифрованную модель бандлим в приложение или заливаем на бэкенд.

  4. Далее нам нужно придумать способ доставки ключа на клиент. Тут возможны варианты:

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

      https://github.com/pjebs/Obfuscator-iOS или https://github.com/UrbanApps/UAObfuscatedString

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

      [UPD 25.11.22] @house2008сделал хорошее замечание, что для этого метода есть риск того, что на Jailbreak девайсе можно отключить ssl pining и прочитать ключ из ответа бэкенда.

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

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

  6. Инициализируем модель и загружаем ее в память c помощью init метода MLModel

  7. Удаляем временную папку.

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

Итого

Мы рассмотрели два подхода к шифрованию CoreML моделей. Какой способ выбрать и стоит ли вообще тратить время на шифрование как водится каждый решает для себя исходя из задач. Всем добра.

Ссылки

Скриншоты взяты из презентации Apple




Комментарии (6):

  1. fralik
    /#24947078 / +2

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

    • albrom
      /#24947494 / -1

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

  2. house2008
    /#24947968 / +1

    Спасибо, очень интересно) А если сделать jailbreak и отключить ssl pining, ваш ключ для расшифровки будет виден в трафике ?

    • albrom
      /#24949800 / +1

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

      https://github.com/pjebs/Obfuscator-iOS 

      https://github.com/UrbanApps/UAObfuscatedString

  3. S-trace
    /#24954588

    дешифруем модель, релизим из памяти ключ, разархивируем модель и сохраняем во временную папку, например:  NSTemporaryDirectory()/YourFolder/

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

    Ну или просто запустить ваше приложение под чем-то типа strace и оттрассировать все вызовыopen() и write() а затем отфильтровать их по пути, получив только вызовы связанные с временной директорией, а затем повторить их и получить полностью дубликат вашей временной директории с расшифрованной моделью.

    Инициализируем модель и загружаем ее в память c помощью init метода MLModel

    Ещё одно место которое можно немножко попячить (к примеру вставив в init что-то типа sleep(10000) или заменить вызов init на exit() и спокойненько вычитать за время сна (или после завершения приложения) всю временную директорию с расшифрованной моделью.

    Удаляем временную папку.

    Или просто заменить все вызовы удаления файлов на nop - первый же запуск приложения и временная директория с расшифрованной моделью останется.

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

  4. S-trace
    /#24954980

    Далее нам нужно зашифровать наш файл. Для этого можно использовать популярный sha256 либо любой другой алгоритм.

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

    Может всё-таки лучше для шифрования выбрать что-то вроде AES256?