Привет, Хабр!  Многим из вас я уже знаком по предыдущим статьям. Меня зовут Юрий Шабалин. Мы вместе с командой разрабатываем платформу анализа защищенности мобильных приложений. Сегодня я расскажу о том, на что обратить внимание при поиске и анализе чувствительной информации в приложении, как ее искать, и немного о том, как ее можно правильно хранить.  Статья может быть полезна тем, кто занимается анализом защищенности приложений (подскажет, как улучшить качество и полноту проверок, например) а также разработчикам, заинтересованным в правильном хранении данных и в безопасности в целом. Надеюсь, что каждый найдет для себя здесь что-то познавательное и интересное. Введение Поиск чувствительной информации Расширение поиска Как хранить Создание ключа PBKDF2 Режимы и вектор инициализации Дополнение (Padding) Операции шифрования и дешифрования Encrypt Decrypt Заключение Ссылки Изначально, когда мы разрабатывали продукт для своих целей ежедневного анализа различных мобильных приложений, мы в первую очередь автоматизировали то, что занимало наибольшее количество времени. Речь про поиск различной чувствительной информации*, которую хранит или обрабатывает приложение. Это может быть все, что угодно, - аутентификационные данные (пароли, cookie и т.д.), персональные сведения (номер телефона, ФИО, паспортные/контактные данные), сессионный идентификатор, который вернула серверная часть приложения, различные токены от сторонних сервисов и т.д.  * Под термином чувствительная информация (чувствительные данные) подразумевается любая информация, которая может позволить злоумышленнику выстроить вектор атаки на пользователя. И сегодня я бы хотел поделиться некоторыми заметками, примерами из жизни и поговорить про то, как можно улучшить поиск чувствительной информации и сделать его немного более интересным. Поиск различной чувствительной информации необходимо вести в достаточно большом количестве источников и форматов. Вы наверняка столкнетесь с разнообразием при анализе, поэтому обращайте внимание на: Файлы приложения, которых может быть очень много. Современные платформы активно используют мобильное устройство для хранения, чтобы в дальнейшем работать быстрее; Базы данных. Это частный случай файлов приложения, но проводить поиск там нужно по совершенно другому принципу; Системный журнал. Нужно быть уверенными в том, что в него ничего не попадает, даже учитывая, что читать его может лишь ограниченное количество приложений; Сетевая активность. Надо смотреть, какие данные ходят между приложением и серверной частью и выявлять чувствительную информацию, которая потенциально может быть интересна злоумышленникам; Межпроцессное взаимодействие. Необходимо проверять, как приложение общается с внешним миром, какие есть доступные извне компоненты, что туда приходит и что приложение отправляет в другие приложения/сервисы; Deeplink и Applink, как частный случай межпроцессного взаимодействия; Код  и ресурсы приложения - декомпилированный / дизассемблированный код приложения, в нем надо искать различные оставленные секреты. Если суммировать все эти данные, то получится достаточно большой объем. А ведь вся информация представлена в различных форматах, каждый из которых нужно правильно анализировать. Возьмем хотя бы тот же plist формат. Если искать в нем данные классическим способом, просто по ключевым словам (например, по слову key, как это обычно и делается), то можно получить достаточно большое количество срабатываний, не относящихся к чувствительной информации, и утонуть в их разборе. При этом очень легко потерять из вида что-то важное. Но основной интерес представляет собой даже не сам первоначальный поиск данных, который вполне понятен и очевиден, а валидация того, что они больше нигде не хранятся. Например, мы выяснили, что после аутентификации сервер прислал нам значение Cookie, которое используется дальше в приложении. И нам желательно удостовериться, что значение этого идентификатора нигде в данных не встречается в открытом виде. И тут начинается процесс, который далеко не всегда проводят при анализе, а именно нужно повторно искать эту информацию по всем собранным данным.  Очень хороший пример встретился нам относительно недавно, во время анализа очередного приложения. Он наглядно демонстрирует, как такой подход помогает во время анализа и как с его помощью можно установить первопричину ошибки.  В одном из приложений значение pin-кода, который задавал пользователь, хранилось локально внутри песочницы приложения в виде хэша SHA512. Хоть это и не хранение в открытом виде, но алгоритм хэширования применялся к пятизначному пинкоду из цифр без использования соли. Если поискать в Google значение этого хэша, то оно будет в первых результатах. То есть, с одной стороны, это хранение не в открытом виде, но, с другой стороны, оно небезопасно. Формально прикрыли информацию, чтобы не сильно заметно было.  После анализа нескольких приложений, которые подобным образом хранили чувствительную информацию пользователя, не могу не посоветовать искать не только исходную информацию, но и производные от неё. Условно, процесс поиска таких данных можно проводить в несколько этапов: После первичного анализа всех собранных данных определяется чувствительная информация, которая обрабатывается или хранится в приложении; От каждого срабатывания из этого списка чувствительных данных высчитываются производные в виде md5, sha1, sha256, sha512, base64 и других; После этого необходимо снова пройтись по всем собранным данным и повторно поискать, не встречается ли где-то значение чувствительной информации и производные от нее. Таким образом, если где-то засветится чувствительная информация или ее производная, можно будет это отловить и показать, откуда и на основе чего появился этот дефект.  В качестве иллюстрации к примеру с pin-кодом пользователя - найденный pin в сетевом запросе к серверу при первой аутентификации пользователя: И после повторного поиска производной от этого значения, видно, что это оно было сохранено в виде sha512 во внутренней директории приложения: Такой подход позволит обнаруживать более сложные цепочки и закономерности в хранении и использовании данных в приложении и поспособствует выявлению интересных уязвимостей во время анализа. А еще эти данные можно применять и дальше при анализе бизнес-логики и тестировании других уязвимостей. Еще один хороший пример того, как может помочь поиск повторной информации. В анализируемом приложении использовалась зашифрованная при помощи популярной библиотеки SQLChipher база данных. Так как процесс создания или открытия такой базы давно известен, можно перехватить его в Runtime и посмотреть на значение пароля: Что с этой информацией делать дальше? Все очень просто. Если приложение использует пароль для шифрования базы, он должен где-то храниться. Дальше необходимо во всех данных приложения попробовать обнаружить его значение (и его производные): В этом случае нам сильно повезло - можно было обойтись банальным изучением исходников. Но вообще, используемое значение пароля может быть спрятано где-то намного глубже и интереснее. И тогда подход с повторным поиском может очень пригодиться. То есть мы сразу получим целую цепочку, полностью характеризующую уязвимость и вектор атаки: приложение использует зашифрованную базу данных, применяется определенный пароль для шифрования, пароль хранится в исходниках. Такой процесс актуален для абсолютно любой информации, которая была определена, как чувствительная.  Еще один пример связан с поиском значений сессионных идентификаторов. В нескольких приложениях, когда пользователь аутентифицировался, значение сессионных идентификаторов через стороннюю библиотеку попадало в системный журнал (или сохранялось в кэшированных сетевых запросах в файловой системе). Обычным поиском по ключевым словам такого не найти, но благодаря механизму повторного поиска это становится возможным. Так что не забывайте об анализе собранных во время анализа данных, и делайте это в несколько проходов, чтобы ничего не упустить. После поиска чувствительной информации, её дальнейшего повторного анализа вместе с производными, можно копнуть еще немного глубже (или в сторону). А именно понять, как определить огромное количество различных форматов и вариаций сессионных заголовков, различные форматы и типы ключей шифрования, которые могут быть оставлены в коде или ресурсах приложений. Также возможно, что они пришли во время сетевого взаимодействия или из других мест, и всё это наталкивает на мысль об энтропийном анализе информации.  В прошлой статье я уже упоминал про репозиторий, который умеет определять последовательность символов, не похожую на обычные слова - Gibberish Detector. Это простая модель, основанная на цепи Маркова. Суть ее в том, что, обучаясь на произвольном тексте на английском языке, модель понимает, с какой частой буквы обычно следуют друг за другом, и на этом основании анализирует входные данные (то есть то, насколько часто встречается такая же последовательность букв) и определяет, является ли входная строка словом или случайным набором символов. Для удобства ее применения существует репозиторий, реализующий функционал этой модели в виде библиотеки для Python. По факту, все, что нужно сделать - это получить интересующие нас строки (к примеру, можно взять все строки длиннее 16 символов) и отправить на вход обученной модели. На выходе мы получим некоторое значение, которое чем больше, тем более вероятно, что это не слова, а некоторая случайная последовательность символов. И вот на них стоит обратить внимание, поскольку нередко они оказываются хэшированными значениями интересных данных, а то и просто значением токенов или ключей. Второй вариант без использования моделей основан на вычислении энтропии Шэннона (также известной как информационная энтропия) для каждого фрагмента текста более длиной более тех же 16-ти символов (или любого другого количества). Алгоритм, как именно вычислять это значение, можно найти в интересной статье, ну а реализацию - в одном из инструментов под названием TruffleHog. Он позволяет искать секреты в репозиториях с исходным кодом вплоть до конкретного коммита, когда и кем это значение было добавлено. Так что, если вы анализируете приложения с доступным вам исходным кодом, рекомендую задуматься о постоянном использовании данного инструмента. Такие срабатывания достаточно часто оказываются ложными и их приходится дополнительно фильтровать и обрабатывать, но иногда там можно найти действительно интересные вещи, которые можно пропустить во время первоначального анализа. Одна из традиционных рубрик моих статей - “Как все сделать правильно” :) В прошлых материалах мы рассматривали несколько вариантов хранения чувствительной информации с применением шифрования для Android, генерацию и хранение ключей в Security Enclave и Android KeyStore. А сейчас посмотрим, как это можно реализовать с применением алгоритма расширения ключа, то есть с получением надежного ключа шифрования из информации, которую предоставил пользователь (например, пароль). И в этот раз будут примеры для iOS. Параллельно с разбором процесса создания ключа на основе пароля и шифрованием на нем данных пользователя, я постараюсь немного рассказать про термины, используемые в криптографии. Если к этой теме будет интерес, я подготовлю и выпущу статью с описанием основ криптографии простыми словами. О том, что необходимо знать, чтобы не делать простых ошибок и понимать описываемое в том или ином репозитории с примерами или библиотеками по использованию криптографии. Но прежде, чем приступить к коду и шифрованию, еще раз подчеркну, что я не разработчик. Поэтому не надо пробовать слепо копировать то, что здесь написано (и вообще так лучше никогда не делать). Постарайтесь использовать это как материал, позволяющий структурировать информацию и найти отправную точку для собственной безопасной реализации. Очень распространенной ошибкой при использовании любого шифрования является применение пароля в качестве ключа. Что делать, если пользователь выберет очень простой или предсказуемый пароль? Как заставить его применить для шифрования ключ, который был бы случайным и достаточно сильным (имел бы достаточную энтропию)? Что делать, если пользователь затем запомнит его и будет вводить каждый раз для входа в приложение или на устройство? Решением является применение алгоритмов усиления ключа (Key Stretching). Это позволяет получить ключ шифрования из достаточно простого пароля, применяя к нему несколько раз функцию хеширования вместе с солью. Соль - это некая последовательность случайных данных. Распространенной ошибкой является исключение соли из алгоритма. Соль дает ключу намного большую энтропию. Без неё намного проще получить/восстановить/подобрать ключ. Тем более, без использования соли два одинаковых пароля будут иметь одинаковое значение хэша и, соответственно, одинаковое окончательное значение ключа шифрования.  Еще одна ошибка - применять предсказуемый генератор случайных чисел при создании соли. Примером может служить функция rand() в C, к которой можно получить доступ из Swift или Objective-C. Результат данной функции может оказаться очень предсказуемым. Чтобы создать достаточно случайную соль, рекомендуется применять функцию SecRandomCopyBytes для генерации  криптографически безопасной последовательности случайных чисел. Чтобы использовать код из приведенного далее примера, нужно добавить следующую строку в заголовки: Ниже приведен код, создающий соль: Дальше по тексту мы будем понемногу дополнять этот код, приводя его к законченному виду. Теперь приступим к процедуре усиления ключа. Выполнять то, что нам нужно, позволит функция  определения ключа на основе пароля (PBKDF2 - Password-Based Key Derivation Function): PBKDF2 выполняет функцию усиления в несколько итераций для получения ключа. Обычно это около 10 тысяч итераций; Рост количества итераций увеличивает время, необходимое для успешной атаки с использованием полного перебора (brute force). Блочные алгоритмы шифрования работают с текстом определенной длины. Если изначальное сообщение, которое мы хотим зашифровать, длиннее, чем блок, с которым умеет работать алгоритм, оно просто разделяется на части. Из-за этого при неправильной конфигурации алгоритма могут возникнуть некоторые особенности. Например, если при разделении на блоки текст в них совпадет, то и в зашифрованном виде мы получим одинаковый шифротекст. Как раз для того, чтобы избежать таких ситуаций, используют различные варианты связи блоков между собой: Электронная кодовая книга (Electronic Code Book) – ECB Сцепление блоков (Cipher Block Chaining) – CBC Обратная связь по шифротексту (Cipher Feedback) – CFB Обратная связь по выходу (Output Feedback) – OFB Режим счетчика (Counter Mode) – CM, CTR Режим “электронная кодовая книга” (ECB) - самый простой вариант, при котором все блоки шифруются независимо друг от друга. Как раз именно в этом случае блоки друг от друга не зависят и шифруются отдельно. Это применяется по умолчанию. Если мы при настройке шифрования просто укажем AES без каких-либо дополнительных параметров, то именно такой вариант и будет использован. По этой причине настоятельно рекомендуется явно указывать режим связи блоков между собой. Режим сцепления блоков шифротекста — один из режимов шифрования для симметричного  шифра с использованием механизма обратной связи. Это означает, что при шифровании все блоки связываются между собой и зависят друг от друга. Такой подход позволяет избежать повторения информации в одинаковых блоках.  Именно его в основном и рекомендуется использовать. Остальные варианты мы рассматривать не будем, так как они отличаются между собой только способом связывания блоков, а для нас это в целом не имеет значения. Главное - понять отличие ECB от остальных способов. Но существует еще одна проблема - первый блок в любом из режимов остается одинаковым. Если сообщение, которое нужно зашифровать, начинается так же, как и другое зашифрованное сообщение, начальный зашифрованный текст (первый блок) будет одинаковым в обоих случаях. Это даст злоумышленнику понимание того, что текст в этих блоках совпадает.  Для того, чтобы избежать подобных проблем, вводится понятие вектора инициализации (IV).  Initialization vector (IV) — вектор инициализации, представляющий собой произвольное число, которое может применяться вместе с ключом для шифрования данных. Использование IV предотвращает повторение шифрования данных в первом блоке. Для генерации вектора инициализации рекомендуется использовать функцию SecRandomCopyBytes: Блочные алгоритмы шифрования работают с сообщениями открытого текста, длина которых должна быть кратна длине одного блока. Если это свойство не выполняется, то к сообщению необходимо добавить необходимое количество битов, называемых дополнением (Padding). Этот параметр указывает, каким именно способом необходимо провести дополнение блока меньшей длины. Существуют различные варианты, но предпочтительным методом дополнения блоков шифротекста является PKCS7. В нем значение каждого дополняемого байта устанавливается равным количеству дополняемых байтов. Так, если мы имеем блок из 12 символов, он будет дополнен четырьмя байтами  до стандартного размера блока в 16 байт. Если блок имеет размер в 15 байт, он будет дополнен одним байтом . Если блок имеет размер ровно в 16 байт, мы добавляем новый блок состоящий из [16]*16. Я попытался разобраться, почему рекомендуют использовать именно этот метод, а не остальные, но мои поиски не увенчались успехом. Некоторые мои коллеги рекомендуют вообще не применять дополнение из-за атаки Padding Oracle (вот, кстати, отличнейший перевод статьи про эту атаку). Так что вопрос для меня пока еще открытый, использовать дополнение или нет. Наконец-то мы добрались до самого интересного! Пора связать все вместе, провести операции усиления пароля, шифрования и расшифровки. Поскольку используем алгоритм усиления ключа, нам нет необходимости его где-то хранить. Каждый раз, когда в нем возникнет необходимость, мы будем задействовать данные пользователя для его генерации. Один из вариантов пришел мне в голову прямо во время написания этой статьи. Можно сохранить случайно сгенерированное значение в Keychain (обязательно с правильными ключами доступа), защитив его с использованием биометрии. Таким образом, доступ к значению будет осуществлен только после подтверждения пользователем биометрических данных. Полученное значение следует передавать на вход в функцию PBKDF2 для генерации ключа. В результате, пользователю не нужно будет каждый раз вводить пароль/пин. Достаточно будет предоставить отпечаток пальца или лицо. Схема, конечно, со своими недостатками, но вполне неплохая. Хотя, таким же образом можно и просто использовать Security Enclave. Для шифрования и дешифрования используем функцию CCCrypt с помощью kCCEncrypt или kCCDecrypt. Поскольку применяется блочный шифр, необходимо дополнить сообщение, если оно не соответствует кратности размера блока. Используя параметр KCCOptionPKCS7Padding, определяем тип дополнения, как PKCS7: И, соответственно, функция расшифровки: Для проверки того, что эти функции работают и шифрование/расшифровка проходят корректно, можно воспользоваться простым примером: В этом примере мы упаковываем всю необходимую информацию и возвращаем ее в виде словаря, чтобы впоследствии все части могли использоваться для успешного дешифрования данных. Для этого необходимо хранить IV и соль - либо в Keychain, либо на сервере. К данным, которые хранятся и обрабатываются в мобильном приложении, надо относиться с большой осторожностью и вниманием. Во-первых, не стоит забывать, что оно работает на устройстве пользователя, то есть в неблагоприятной для себя среде. Кроме того, мобильное приложение можно рассматривать как еще одну версию Frontend. Мы ведь не храним пароль пользователя в Local Storage браузера (по крайней мере, я очень надеюсь на это). Так почему же мы можем позволить себе делать это в мобильном приложении?  К большому сожалению, как я уже писал в прошлых статьях, проблемы с хранением чувствительной информации пока все еще на первом месте по распространенности. Мы практически каждый день сталкиваемся с новыми и новыми срабатываниями. Своим циклом статей я хотел бы помочь разработчикам и аналитикам безопасности понять, как можно искать такие проблемы, на что стоит обращать внимание, и главное, как это можно  попробовать исправить и сделать правильно.  Если в результате прочтения этих статей хотя бы в одном приложении все станет чуть лучше, значит, моё время было потрачено не зря. SQLCipher  Информационная энтропия  Наиболее распространенные уязвимости в мобильных приложениях  Знай свои секреты. Поиск и автоматическая проверка секретов сторонних сервисов Key stretching PBKDF2 Padding (cryptography)  Gibberish Detector Краткое введение в цепи Маркова Gibberish Detector Python Module Scanning data for entropy anomalies TruffleHog Padding Oracle Attack или почему криптография пугает   Архитектор по информационной безопасности