В этой статье мы рассмотрим различные методы, которые используются в приложениях для хранения локальных данных на устройстве, и проанализируем уровень безопасности этих методов.
Автор: Пратик Джианчандани (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 предоставляет универсальные и автоматические решения для типовых задач, связанных жизненным циклом объекта и управлением графом объектов, включая хранение объектов. Функции фреймворка:
То есть, по сути, 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, для подделки пользовательской сессии. Мы же перед сохранением токена можем обработать его каким-нибудь алгоритмом (например, самое простое, что можно сделать, - перемешать символы задом наперед), и злоумышленник не будет знать, что нужно сделать для того, чтобы получить рабочий токен. Само собой, злоумышленник может провести более тщательное исследование приложение и выявить этот трюк, однако в некоторых случаях это может помочь. Еще один простейший трюк: перед сохранением строки добавлять к ней константу.