Безопасность IOS-приложений (часть 20) – исследование методов хранения данных на устройстве (NSUserDefaults, CoreData, Sqlite, Plist-файлы)

Безопасность IOS-приложений (часть 20) – исследование методов хранения данных на устройстве (NSUserDefaults, CoreData, Sqlite, Plist-файлы)

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

Автор: Пратик Джианчандани (Prateek Gianchandani)

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

Некоторые наши эксперименты будут выполняться над тестовым приложением, которое можно скачать отсюда. Для исследования CoreData вы можете загрузить тестовое приложение отсюда.

Кроме того, мы будем запускать приложения в IOS-симуляторе, используя Xcode и, следовательно, весь анализ будет происходить на компьютере, а не на самом устройстве. Все это будет делаться лишь с той целью, чтобы продемонстрировать, что вы можете использовать методы, описанные в предыдущих статьях, и над приложением, запущенным через Xcode. При желании, вы можете запустить приложение на устройстве, используя способ, описанный в седьмой статье из этой серии.

NSUserDefaults

Один из наиболее распространенных способов сохранения пользовательских настроек и свойств приложения – использование NSUserDefaults. Информация, хранимая в NSUserDefaults, сохраняется даже после закрытия и повторного запуска приложения. К примеру, в NSUserDefaults может храниться статус пользователя в системе, и при повторном открытии приложения в зависимости от этого статуса, пользователю будет показываться соответствующая информация и внешний вид интерфейса. Некоторые приложения используют NSUserDefaults для хранения конфиденциальной информации (например, пользовательский токен доступа), которая при повторном запуске приложения используется для повторной аутентификации в системе.

Загрузите и запустите тестовое приложение. Появится окно (см. рисунок ниже). Введите любую информацию в текстовом поле, которое относится к NSUserDefaults и нажмите на кнопку с надписью Save in NSUserDefaults, после чего информация сохранится в NSUserDefaults.

Рисунок 1: Окно, появляющееся после запуска тестового приложения

Многие не понимают того, что информация, хранимая в NSUserDefaults, не шифруется и может быть легко получена из пакета приложения (application bundle), поскольку хранится в plist-файле с именем bundle Id конкретного приложения. Для начала нам необходимо найти пакет для нашего приложения. Поскольку мы запускаем приложение на компьютере, то все приложения можно найти в папке /Users/$username/Library/Application Support/iPhone Simulator/$ios version of simulator/Applications/. В моем случае это папка Users/prateekgianchandani/Library/Application Support/iPhone Simulator/6.1/Applications.

Внутри этой папки находится пакет приложений, которые мы запускаем через Xcode для конкретной версии IOS. Мы можем найти наше приложение по дате последнего изменения.

Рисунок 2: Перечень приложений

Заходим внутрь нашего приложения. Все содержимое NSUserDefaults хранится внутри plist-файла с именем Library -> Preferences -> $AppBundleId.plist, как показано на рисунке ниже.

Рисунок 3: Plist-файл, где хранится информация из NSUserDefaults

Открыв вышеуказанный plist-файл, вы легко можете увидеть его содержимое.

Рисунок 4: Содержимое plist-файла

Некоторые plist-файлы могут быть бинарными и их не так просто прочесть. Вы можете либо сконвертировать их в xml при помощи утилиты plutil, либо открыть этот бинарный файл на устройстве при помощи iExplorer.

Plist-файлы

Еще один распространенный способ хранения информации – plist-файлы. К подобному способу следует прибегать лишь тогда, когда хранимая информация не представляет особой ценности, поскольку сами файлы не шифруются, и информацию из них можно легко извлечь даже на неджейлбрейковом устройстве. Были найдены уязвимости, которые свидетельствовали о том, что крупные компании использовали plist-файлы для хранения конфиденциальных данных (токены доступа, имена пользователей и пароли). В тестовом приложении введите информацию в соответствующих текстовых полях и нажмите на кнопку с надписью Save in plist file.

Рисунок 5: Сохранение информации в plist-файле

Ниже показан код, позволяющий сохранять информацию в plist-файле:

[plain]
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingString:@"/userInfo.plist"];
NSMutableDictionary* plist = [[NSMutableDictionary alloc] init];
[plist setValue:self.usernameTextField.text forKey:@"username"];
[plist setValue:self.passwordTextField.text forKey:@"passwprd"];
[plist writeToFile:filePath atomically:YES];
[/plain]

Из кода, показанного выше, видно, что всегда можно задать свой собственный путь для хранения plist-файла. Теперь мы можем поискать все plist-файлы внутри пакета приложения (в нашем случае, будем искать файл с именем userinfo.plist).

Рисунок 6: Местонахождение plist-файла

Заглянув внутрь найденного файла, мы видим введенную ранее комбинацию имени пользователя и пароля.

Рисунок 7: Содержимое plist-файла

Файлы CoreData и Sqlite

Поскольку для хранения информации CoreData использует Sqlite, мы будем рассматривать только CoreData. Ниже приводится выдержка из документации Apple относительно CoreData:

Фреймворк Core Data предоставляет универсальные и автоматические решения для типовых задач, связанных жизненным циклом объекта и управлением графом объектов, включая хранение объектов. Функции фреймворка:

  • Отслеживание изменений и поддержка отмены изменений. У Core Data есть встроенные возможности для отмены и восстановления изменений во время редактирования текста.
  • Поддержка взаимосвязей. Core Data управляет распространением изменений, включая поддержку корректности взаимосвязей среди объектов.
  • Отложенное выполнение. Core Data может уменьшить издержки памяти вашего приложения путем отложенной загрузки объектов. Также присутствует поддержка частично материализованных функций (partially materialized futures) и копирования при записи совместно используемых данных.
  • Автоматическая проверка значений свойств. Управляемые объекты Core Data расширяют стандартные методы валидации данных типа «ключ-значение», которые поддерживают целостность этих данных.
  • Перенос схемы. Изменить схему приложения может быть не так просто, как с точки зрения усилий, затрачиваемых разработчиками, так и использования ресурсов во время выполнения приложения. Утилиты Core Data, предназначенные для переноса схем, упрощают задачу копирования при изменении схемы и в некоторых случаях позволяют выполнять сверхэффективный перенос схемы по месту.
  • Дополнительная интеграция с уровнем контроллера приложения для поддержки синхронизации с пользовательским интерфейсом. При работе с iOS у Core Data есть объект NSFetchedResultsController. При работе в OS X Core Data интегрирован с Cocoa Bindings.
  • Полная и автоматическая поддержка для доступа и отслеживания изменений данных, хранящихся по принципу «ключ-значение». В дополнении к синтезу методов доступа и отслеживания изменений данных, в Core Data присутствует соответствующий набор средств для взаимосвязей типа «один-ко-многим» или «много-ко-многим».
  • Группировка, фильтрация и организация данных в памяти или в пользовательском интерфейсе.
  • Автоматическая поддержка для объектов, хранящихся во внешних репозиториях.
  • Продвинутая компиляция запросов. Вместо написания SQL-кода, вы можете создавать сложные запросы путем связывания объекта NSPredicate с запросом на извлечение данных. В NSPredicate есть поддержка базовых функций, связанных подзапросов и других продвинутых возможностей языка SQL. Также в Core Data есть поддержка корректного Unicode, регионального поиска, сортировки и регулярных выражений.
  • Политики объединения. В Core Data есть встроенные средства отслеживания версий и оптимистичной блокировки для поддержки автоматического разрешения конфликтов при многократной записи.

То есть, по сути, CoreData можно использовать для создания модели, управления взаимосвязью между различными типами объектов, локального сохранения данных и получения данных из локального кэша при помощи запросов в тот момент, когда это необходимо. В этой статье мы будем экспериментировать с тестовым приложением. После запуска на экране появится простейшая RSS-лента.

Рисунок 8: Тестовое приложение с простейшей RSS-лентой

Это приложение использует Core Data для сохранения информации. Важно отметить, что фреймворк Core Data использует Sql-запросы для хранения данных и, следовательно, вся информация хранится в файлах с расширением .db. Теперь посмотрим, где хранится эта информация. Внутри пакета приложения вы можете увидеть файл с именем MyCoreData.sqlite.

Рисунок 9: Содержимое пакета приложения

Сейчас мы будем анализировать файл MyCoreData.sqlite при помощи утилиты sqlite3. В моем случае этот файл находится в папке ~/Library/Application Support/iPhone Simulator/6.1/Applications/51038055-3CEC-4D90-98B8-A70BF12C7E9D/Documents.

Рисунок 10: Начинаем анализировать файл MyCoreData.sqlite

Как видно из рисунка выше, внутри файла присутствует таблица ZSTORIES. В Core Data в начало имени каждой таблицы подставляется Z, то есть на самом деле имя таблицы - STORIES, и мы можем это легко увидеть из исходных файлов проекта.

Рисунок 11: Параметры таблицы STORIES

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

Рисунок 12: Содержимое таблицы STORIES

Как видно из рисунка выше по умолчанию все хранимые данные незашифрованные и могут быть легко получены. Следовательно, использовать CoreData для хранения конфиденциальной информации нецелесообразно. Существуют библиотеки, выступающие в роли обертки для CoreData, которые сохраняют данные в зашифрованном виде. Существуют и другие решения, позволяющие сохранять информацию в зашифрованном виде без использования CoreData. Например, Salesforce Mobile SDK использует функцию SmartStore, которая может хранить зашифрованные данные на устройстве в специальных таблицах (или Soups).

Keychain

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

[plain]
PDKeychainBindings *bindings = [PDKeychainBindings sharedKeychainBindings];
[bindings setObject:@"XYZ" forKey:@"authToken"];
[/plain]

Далее показан небольшой участок кода, про помощи которого можно получать информацию из Keychain.

[plain]
PDKeychainBindings *bindings = [PDKeychainBindings sharedKeychainBindings];
NSLog(@"Auth token is %@",[bindings objectForKey:@"authToken"]]);
[/plain]

Простейшие трюки для повышения уровня безопасности приложения

Как уже говорилось ранее, ничто не защищено на джейлбрековом устройстве. Злоумышленник может доставать информацию из plist-файлов, выгружать все хранилище Keychain, изменять реализацию методов (об этом рассказывалось в восьмой статье этой серии) и вообще делать все, что заблагорассудится. Однако разработчик приложения при помощи некоторых трюков может усложнить жизнь скрипт кидди (или тем, кто только начинает свой путь на этом поприще). Разработчик может шифровать файлы во время их сохранения на устройстве (более подробно об этом читайте в этой статье). Или вы можете затруднить получение злоумышленником корректной информации. Например, рассмотрим пример сохранения в Keychain аутентификационного токена для конкретного пользователя. Неопытный злоумышленник лишь попытается использовать этот токен, выгруженный из Keychain, для подделки пользовательской сессии. Мы же перед сохранением токена можем обработать его каким-нибудь алгоритмом (например, самое простое, что можно сделать, - перемешать символы задом наперед), и злоумышленник не будет знать, что нужно сделать для того, чтобы получить рабочий токен. Само собой, злоумышленник может провести более тщательное исследование приложение и выявить этот трюк, однако в некоторых случаях это может помочь. Еще один простейший трюк: перед сохранением строки добавлять к ней константу.

Устали от того, что Интернет знает о вас все?

Присоединяйтесь к нам и станьте невидимыми!