Наиболее распространенные уязвимости в мобильных приложениях

Kate

Administrator
Команда форума

Вступление​

Все, что написано ниже во вступлении, является личным мнением автора и может не совпадать с действительностью!

Мобильные приложения, которые мы знаем и привыкли видеть, лично для меня начинают свою историю примерно с 2008 года, а именно с выходом iPhone 4, а позже и Android 4 (2012 год). Именно тогда появилась операционная система iOS (была переименована из iPhone OS). По сравнению с Web-приложениями (я бы предложил считать началом современного Web 1995 год, момент появления JavaScript), мобильная экосистема - достаточно молодая технология. Но она очень динамичная, развивается сумасшедшими темпами и набирает все большую популярность у пользователей. По сути, мобильный телефон или планшет - это практически полноценная замена ноутбуку или компьютеру (по крайней мере, для повседневных задач). И, если посмотреть на статистику использования мобильных банков по сравнению с их Web-версиями, то можно увидеть, что уже несколько лет назад количество активных пользователей мобильных клиентов превысило этот показатель для Web-версий и разрыв между ними все больше и больше (по данным Tinkoff Strategy Day 2018)

Статистика активных пользователей мобильного и Web банков по данным Tinkoff (2018 год).
Статистика активных пользователей мобильного и Web банков по данным Tinkoff (2018 год).
И, на самом деле, я не помню, когда мне приходилось последний раз заходить в личный кабинет с компьютера. Да и зачем, если телефон всегда под рукой и я могу сделать тоже самое (а иногда и больше), используя удобное мобильное приложение.

Тот факт, что иногда мобильные приложения по функционалу превосходят Web-версии, определяется их более динамичным развитием и особенностями процесса разработки. Достаточно часто мы встречаемся с тем, что мобильные приложения создаются немного в стороне от основной разработки backend и frontend (а иногда даже вне инфраструктуры компании, или вообще на аутсорсе). И это оправданно, ведь классическая цепочка CI/CD для обычного приложения выглядит примерно так:

  1. Разработчик коммитит код
  2. Инструмент CI собирает новую версию дистрибутива и складывает его в репозиторий
  3. Инструмент CD забирает этот дистрибутив и раскатывает его на стенд
  4. На этом, собственно, все - новая версия доступна всем тестировщикам для проведения автоматических и интеграционных тестов
В мобильных приложениях все немного сложнее, поскольку “стенд“, на который нужно раскатать новую версию, - это физическое устройство (конечно, в дополнение к автотестированию специализированными фреймворками на эмуляторах/симуляторах или “фермах” устройств). Как правило, тестировщики руками проверяют приложения на реальных девайсах, а это значит, что до этих телефонов приложение должно быть как-то доставлено. Тут на помощь приходят удобные системы дистрибуции, такие как Firebase, TestFlight, AppCenter и другие (кстати, интеграцию с большинством из этих систем поддерживает наша система). Все эти решения - облачные, и работать с ними намного удобнее из обычного контура, нежели из закрытого (это актуально для компаний, где нет прямого доступа в интернет).

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

И, скорее всего, раз мобильные приложения такие молодёжные и стремительно развивающиеся, то и с безопасностью все должно быть на уровне, и детские болячки уже давно позади? Давайте об этом чуть подробнее и поговорим.

Снова про OWASP Mobile​

Для того, что бы систематизировать и упорядочить информацию об уязвимостях, их типах и классах, мы привыкли использовать различные стандарты и требования. Методология и классификация OWASP (а в нашем случае - OWASP Mobile) - одна из наиболее популярных и общепризнанных практик. Для мобильных приложений в OWASP есть несколько основных документов, которые отвечают за требования, предъявляемые к мобильным приложениям в части безопасности (MASVS - Mobile Application Security Verification Standard). О том, как эти требования проверять, говорится в MSTG - Mobile Security Testing Guide. OWASP Mobile Top 10 - десять наиболее часто встречающихся проблем безопасности в мобильных приложениях.

О самом стандарте и сопутствующих материалах написано немало статей, на их основе сделан не один доклад, на них очень часто ссылаются в различных методологиях или отчетах. Нет смысла рассматривать сам документ детально, но важно обратить внимание на один момент. У OWASP Mobile Top 10 было всего 2 релиза, в 2014 и в 2016 годах. То есть мы все ссылаемся на классификацию, которой на момент написания статьи уже 6 лет! В то время, как OWASP Top 10 - аналогичный список проблем безопасности, но уже для Web-приложений - постоянно пополняется новыми типами атак, регулярно обновляется в части классификации, меняется. Последняя версия этой классификации датируется 2021 годом и, в целом, она пересматривается примерно раз в 2-3 года.

И тут, с моей точки зрения, получается некоторый парадокс: почему для более молодой технологии, для более динамичных и активно развивающихся мобильных приложений, классификация остается нетронутой на протяжении шести лет, в то время как для Web-приложений она меняется каждые несколько лет? Да, Web-приложения тоже развиваются, но, на мой взгляд, мобильная экосистема все-таки немного более активна. Но, даже если предположить, что они меняются одновременно или Web - чуть быстрее, то чем же обусловлена такая разница?

Только проанализировав все свои находки по анализу мобильных приложений, просмотрев десятки отчетов о различных инструментах и, конечно, прогнав сотни приложений через Stingray, я понял, что все уязвимости из перечня 2016-го года актуальны до сих пор. И тут возникает следующий вопрос: составители настолько хорошо попали в точку с перечнем уязвимостей или настолько все плохо с безопасностью мобильных приложений? Однозначного ответа нет, но я думаю, что такое положение дел является следствием нескольких вероятных причин:

  • Во-первых, сегодня у многих магазинов, детских клубов, кафе и так далее есть мобильное приложение. Соответственно, разработка ведется очень активно, и выпустить свое приложение бывает иногда сильно проще и быстрее, чем, например, поднять Web-ресурс.
  • Во-вторых, мобильные приложения появляются чаще, при этом проверять их может быть сложнее. Более того, анализировать каждый релиз вручную без специального инструментария практически невозможно.
  • В-третьих, часто компании не уделяют должного внимания безопасности мобильных приложений, считая, что финансовые потери в случае взлома будут незначительными. Однако, они при этом забывают про репутационные риски, возможные проблемы из-за нарушения законодательных требований, а также не думают о том, что мобильное приложение - это первая линия обороны перед тем, как данные уйдут в сторону backend.
Далее, я хотел бы разобрать наиболее часто встречающиеся проблемы безопасности, которые мы видим в различных мобильных приложениях практически каждый день, и показать, как их можно было бы избежать.

Наиболее частые категории уязвимостей​

Небезопасное хранение данных​

Одна из самых популярных проблем безопасности - это небезопасное хранение чувствительных данных.

Под термином чувствительная информация, или чувствительные данные, я подразумеваю любую информацию, которая может позволить злоумышленнику выстроить вектор атаки на пользователя. Это могут быть аутентификационные данные пользователя (пароли, cookie и т.д.), токены для доступа к сторонним ресурсам, персональная информация (номер телефона, ФИО, паспортные/контактные данные) т.д.
Почему-то считается, что если данные сохранить во внутренней песочнице (в директории, к которой имеет доступ только приложение), то они уже защищены. Но на самом деле это абсолютно не так. Существует большое количество способов, как можно эти данные получить, начиная от банальных локальных или облачных резервных копий приложения, нескольких уязвимостей, позволяющих читать внутренние файлы приложений, и заканчивая попаданием в руки злоумышленников в результате утери или кражи устройства. Для того, чтобы было интереснее читать, я буду рассказывать о реальных находках в приложениях, которые мы проанализировали за последний год (конечно, не обо всех, поскольку их очень уж много).

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

Хранение учетных данных​

Бывает, встречаешь такое, что глаз начинает дергаться. Например, в одном финтех-приложении, которое мы проверяли до его выхода в магазины приложений (и очень хорошо, что именно так), пинкод пользователя, который он задавал для аутентификации, проверялся локально (то есть backend приложения не участвовал в процессе проверки) и хранился в открытом виде в SharedPreferences (речь про приложение для Android).

Механизм хранения данных в SharedPreferences - это удобная обвязка вокруг работы с xml-файлами внутри директории приложения. То есть пинкод фактически лежал в директории приложения в открытом виде в обычном xml-файле.
Хранение pincode в открытом виде
Хранение pincode в открытом виде
Как можно было бы сделать по-другому? Для начала, не стоит проводить проверки локально. Если совсем не избежать, то делать это нужно только с использованием биометрии (причем обязательно не event-bound, а для доступа к ключам в Keystore/Security Enclave) для расшифровки значения в том же Shared Preferences, например вот так:

  1. Генерируем ключ, доступный только по биометрии:
generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);
generator.init(new KeyGenParameterSpec.Builder (KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.build()); // Не устанавливаем setInvalidatedBiometricEnrollment(false)!
generator.generateKey();
  1. Формируем объект CryptoObject, зная алиас ключа:
SecretKey keyspec = (SecretKey)keyStore.getKey(KEY_ALIAS, null);if (mode == Cipher.ENCRYPT_MODE) {
cipher.init(mode, keyspec);

}
cryptoObject = new FingerprintManager.CryptoObject(cipher);

  1. Вызываем аутентификацию по отпечатку с этим CryptoObject:
fingerprintManager.authenticate(cryptoObject, new CancellationSignal(), 0, this, null);

  1. Обрабатываем коллбэк и расшифровываем данные авторизации:
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
cipher = result.getCryptoObject().getCipher();
// Дальше используем ключ из CryptoObject для расшифрования данных авторизации!
}
Как пример, самую простую схему можно представить в виде диаграммы:

Схема аутентификации с использованием биометрии
Схема аутентификации с использованием биометрии
Другой пример подобной уязвимости: в одном популярном мобильном банке до сих пор пинкод пользователя хранится в открытом виде в KeyChain (iOS). Несмотря на то, что KeyChain считается безопасным методом хранения данных, очень не рекомендуется складывать туда такую важную информацию в открытом виде. Ведь ее можно получить из локального бэкапа (если он при его создании был указан пароль для доступа), из облачного хранилища Keychain (в случае, если аккаунт скомпрометирован) или просто при наличии Jailbreak на устройстве. Помимо пинкода, рядом хранились и другие данные, достаточные для того, чтобы получить полный контроль над банковским аккаунтом пользователя.

Как можно было сделать правильно? Практически так же, как и в примере выше, только шифровать значение пинкода с использованием ключа в Security Enclave, либо хотя бы класть непрямое значение в Keychain, а хэш с солью и ограничить его получение по биометрии.

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

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

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

Файл кэшированных сетевых запросов
Файл кэшированных сетевых запросов
В данном случае можно достаточно просто от этого избавиться:

  1. Можно удалить общий кэш в любое время (например, при запуске приложения), вызвав:
URLCache.shared.removeAllCachedResponses()
  1. Для того, чтобы отключить кэш на глобальном уровне:
let theURLCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
URLCache.shared = theURLCache
  1. Если используется объект NSURLConnection с делегатом, можно отключить кеш с помощью метода:
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse?
{
return nil
}
  1. Чтобы создать URL-запрос, который не будет использовать кэш:
var request = NSMutableURLRequest(url: theUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: urlTimeoutTime)
  1. Так же объект NSURLRequest имеет аттрибут cachePolicy, который определяет работу с кэшем:
    • UseProtocolCachePolicy – значение по умолчанию, кэширование зависит от HTTP заголовков.
    • ReloadIgnoringLocalCacheData – кэш не используется.
  2. Ну и один из самых простых вариантов, при открытии/закрытии приложения просто очищать эту базу сетевых запросов или попросту удалять файл.
Еще один момент с сетевым взаимодействием: это WebView и его база, в которой хранятся значения Cookies. Да, при использовании WebView в приложении нужно быть максимально аккуратным, так как этот компонент любит сохранять дополнительную информацию внутри директории вашего приложения. Для iOS-приложений это файлик Cookies.binarycookies, а для Android это Cookies.db.

Хранение данных Cookie в Android приложении
Хранение данных Cookie в Android приложении
Хранение данных Cookie в iOS приложении
Хранение данных Cookie в iOS приложении
Как сделать так, чтобы WebView не кэшировало лишнего? Достаточно просто знать об этом и использовать его правильные методы:

webView.clearCache(true);
webView.clearHistory();
WebSettings webSettings = webView.getSettings();
webSettings.setSaveFormData(false);

// Not needed for API level 18 or greater (deprecated)
webSettings.setSavePassword(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null);
CookieManager.getInstance().flush();
} else {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(this);
cookieSyncMngr.startSync();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
cookieManager.removeSessionCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
}
Вооружившись этими данными, можно много чего попробовать сделать. А если еще и на старой версии Android такая информация не будет должным образом очищена, это может привести к ее удаленному получению. К большому сожалению, компании вынуждены поддерживать неактуальные версии операционных систем Android, так как число их пользователей все еще велико и никто не хочет терять даже несколько процентов своей аудитории.

А ведь все эти проблемы можно решить достаточно просто, так как текущие возможности операционных систем по использованию криптографических средств защиты или, простыми словами, хранению ключей шифрования, очень обширны и просты в применении. Это Security Enclave для iOS и Keystore для Android, и достаточное количество удобных библиотек, которые намного упрощают использование этих механизмов.

Аутентификация в сторонних сервисах​

Практически все мобильные приложения используют сторонние сервисы для своей работы. Механизм push-уведомлений, аналитика, сбор информации о сбоях, функционал оплаты, геолокации, перевода и многое-многое другое. В одном приложении могут быть использованы все перечисленные выше сервисы сразу, и даже больше. Сложность же состоит в том, что все эти интеграции необходимо правильным образом сконфигурировать, настроить и сохранить в приложении токен/ключ/логин-пароль от сервиса в таком формате, чтобы его не смогли получить злоумышленники. И на всех этих этапах есть свои сложности. Чтобы правильно настроить необходимые привилегии, нужно прочитать достаточно большой объем документации, в которой иногда бывает тяжеловато разобраться. В результате, нередко выдаются токены с расширенными правами с формулировкой “так точно заработает“. Про эти вещи редко кто вспоминает (как показывает практика, очень зря), но при этом мало кто задумывается, а что можно сделать, если такой токен для доступа попадет в руки к злоумышленнику. Мы также регулярно встречаем разнообразные токены от сторонних сервисов в своих сканированиях:

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

Некорректное использование криптографии​

Проблемы, описанные выше, могут привести к появлению уязвимостей из другой распространенной категории, которую обозначим как некорректное использование криптографии. Здесь возможны два вида ошибок: отсутствие шифрования чувствительных данных (тогда это становится первым пунктом про некорректное хранение), неправильное использование алгоритмов.

Это одна из моих любимых категорий, потому что я просто фанатею от криптографии, ведь это магия какая-то! Остановимся, однако, на двух самых проблемных категориях - хранение ключей шифрования и использование устаревших алгоритмов.

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

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

Хранение ключей шифрования​

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

Обнаруженный ключ шифрования в ресурсах Android-приложения
Обнаруженный ключ шифрования в ресурсах Android-приложения
Все эти способы не работают, особенно если один ключ используется на всех клиентов. Намного правильнее, безопаснее, а иногда и проще в реализации применять системные аппаратные средства для хранения и создания ключей (Keystore или Security Enclave). Google и Apple приложили много усилий для того, чтобы сделать процедуру шифрования проще и удобнее. Так давайте использовать то, что у нас уже есть! Для разнообразия, рассмотрим пример с созданием и использованием ключа в Security Enclave:

  1. Шаги для создания приватного ключа в Secure Enclave (и соответствующего публичного ключа) практически аналогичны созданию ключа в обычной ситуации:
let access =
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.privateKeyUsage,
nil)! // Ignore error
  1. Используя объект access control, создадим словарь:
let attributes: [String: Any] = [
kSecAttrKeyType as String: type,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: <# a tag #>,
kSecAttrAccessControl as String: access
]
]
  1. Теперь, когда у нас есть словарь, создадим ключевую пару - аналогично тому, как это делается за пределами Security Enclave, вызвав функцию SecKeyCreateRandomKey():
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
  1. Теперь созданные ключи можно использовать для шифрования или подписи данных. Но применять можно только эллиптические алгоритмы, так как Security Enclave поддерживает лишь эллиптические ключи:
var error: Unmanaged<CFError>?
guard let cipherText = SecKeyCreateEncryptedData(publicKey,
algorithm,
plainText as CFData,
&error) as Data? else {
throw error!.takeRetainedValue() as Error
}
Такой порядок создания и использования в официальной документации, ну или можно использовать обвязки для упрощения всех процедур, как вариант - библиотеку EllipticCurveKeyPair.

В дополнение к этому, можно применять алгоритмы, которые вообще не требуют хранения ключа, а создают его “на лету“, из некоторых данных пользователя (например, его пароля или пинкода). Такие алгоритмы называют процедурой расширения ключа. Они позволяют получить из небольшого количества информации длинный и хороший ключ для шифрования. К примеру, этот механизм можно использовать в подходе KEK&DEK (Key Encryption Key & Data Encryption Key). Такой подход проще всего показать на блок-схеме:

Примерная схема с использованием двух ключей
Примерная схема с использованием двух ключей
При этом подходе мы сначала создаем ключ для шифрования данных (Data Encryption Key). Затем на нем зашифровываем данные, и уже этот ключ шифруем при помощи нового ключа (Key Encryption Key). Как раз этот KEK можно либо сохранить в Keystore / Security Enclave, либо генерировать каждый раз (например, на основе пароля пользователя). При таком механизме мы избавляемся от перешифровки данных в случае изменения/компрометации ключа. Нам достаточно перешифровать только DEK и не трогать данные (конечно, это не так, если у нас скомпрометирован DEK, но такой вариант маловероятен). В этом случае, каков бы ни был объем данных, которые нужно сохранить в секрете, время перешифрования всегда будет одинаковым, так как сами зашифрованые данные мы не трогаем. Кстати, такой подход используется в iPhone для шифрования данных в файловой системе (там, конечно все еще сложнее, но принцип именно такой).

Использование устаревших алгоритмов​

Второй очень частой проблемой является использование устаревших алгоритмов. Это могут быть как алгоритмы шифрования, так и хэширования. К примеру, если взять одно из популярных приложений по сокрытию фотографий из галлереи (более 10 миллионов загрузок в Google Play), то внутри обнаружится примерно вот такой код:

public void mo2594f(Priority iVar, DataFetcher.AbstractC0374a<? super InputStream> aVar) {
try {
...
EncryptUtils.m10791a("12345678", fileInputStream, byteArrayOutputStream);
...
} catch (Exception e) {
aVar.mo2600c(e);
}
}
где вызываемая функция - это обвязка над шифрованием:

private static void m10794d(String str, int i, InputStream inputStream, OutputStream outputStream) {
SecretKey generateSecret = SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(str.getBytes()));
....
}
Приложение применяет не только явно прописанный в исходном коде ключ шифрования (так называемый “хардкод”), но еще и давным-давно устаревший алгоритм шифрования DES. Тут целых два вектора атаки - есть, где разгуляться!

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

Основными врагами таких алгоритмов являются огромные базы данных уже посчитанных различных хэшей для очень большого количества разных строк. Есть целые сервисы, которые занимаются тем, что все время считают различные хэши от всех возможных комбинаций символов. Конечно, сначала им скармливают все базы паролей, все базы утечек и они считают их, потом пользовательские запросы. И уже после они занимаются подсчетом оставшихся значений. Принцип работы таких сервисов прост: вбиваешь интересующий тебя хэш, а в ответе видишь, есть ли он в базе, и какое значение ему соответствует. И для большинства паролей, задаваемых пользователями, такие совпадения есть. Как пример - приложение финансовой компании, новая концепция, чат-банк, который так и не увидел свет, хранил пин пользователя, как простой SHA-1(pin). И, конечно, первой же ссылкой в Google, если вбить туда значение этого хэша, был пинкод пользователя.

Ну и md5. Пожалуйста, если вы это читаете, перестаньте использовать его в приложениях, просто забудьте о его существовании!

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

Руками найти такие данные может быть затруднительно, особенно если они приходят без каких-либо дополнительных маркировок. Например, если они лежат не как пара ключ-значение (с говорящим значением ключа, например pin=5F4DCC3B5AA765D61D8327DEB882CF99), а если есть просто значение хэша, где-нибудь в сетевом трафике или в логах приложения. Это достаточно просто пропустить. Специально для такого случая у нас предусмотрен алгоритм поиска повторной информации, который ищет не только встречающуюся ранее чувствительную информацию, но и популярные производные от нее (md5, sha1, sha256, sha512, base64 и др.) по всем данным приложения. Подробнее об этом механизме, который позволяет быстро обнаружить проблемы такого типа и экономит время, также хочется описать и тут я оставлю ссылку, как статья выйдет в свет.

Обнаруженная уязвимость хранения md5 от найденного ранее идентификатора сессии
Обнаруженная уязвимость хранения md5 от найденного ранее идентификатора сессии

Отсутствие защиты канала связи (SSL Pinning)​

Еще одна ситуация, которую мы часто встречаем - это отсутствие или некорректная реализация защиты канала связи, а именно всем известного SSL Pinning. По поводу SSL Pinning всегда много споров: делать его или нет, нужен ли он вообще в приложении или нет, как избежать “протухания“ сертификата в приложении и т.д. В этой статье я хотел бы отметить лишь несколько ключевых вещей.

Во-первых, ответим на вопрос, для чего вашему приложению нужен SSL Pinning. Как правило, есть три типа ответов на этот вопрос:

  1. Для защиты клиента, если он вдруг попал в недоверенную / скомпроментированную сеть, если кто-то пытается прослушать его трафик и т.д.
  2. Для того, чтобы нельзя было проанализировать backend и поискать ошибки, например, в бизнес-логике (классический Security through obscurity).
  3. Для того, чтобы быть уверенными, что запрос приходит от легитимного приложения или, другими словами, для защиты от ботов.
Однако на мой взгляд SSL Pinning призван решить одну единственную задачу - защитить клиента. Если он подключается к корпоративной сети, где стоят сертификаты и проводится попытка MiTM (да, такое бывает), или к публичной сети со скомпрометированным роутером, нужно как минимум предупредить его, а как максимум запретить подключение. Насколько часто эта защита будет необходима и сколько таких скомпрометированных сетей? Вместо ответа приведу простой пример. Недавно обедал в достаточно большом ресторане и подключился к местному Wi-Fi. В ожидании заказа решил проверить, а что у них там по адресу маршрутизатора? И каково было мое удивление, когда перейдя по адресу 192.168.88.1, я попал в открытую админку роутера Mikrotik. Погуляв по настройкам, я выяснил, что на нем не только развернута публичная точка доступа, но и подключено некоторое важное оборудование. А сколько таких вот открытых админок, наверное, только Shodan'у одному известно… Так что это вполне рабочая история, и попасть под сниффинг трафика можно достаточно просто (особенно если вы любите посещать ИБ-конференции).

По поводу двух оставшихся пунктов скажу кратко. Да, SSL Pinning может косвенно повлиять на них. То есть поможет и закрыть ваш API, и в теории защитить от ботов, но это не серебряная пуля, а лишь способ обезопасить ваших клиентов.

В общем, уязвимости, связанные с некорректной реализации пиннинга, достаточно популярны и встречаются повсеместно во многих приложениях:

Выявленная уязвимость с указанием доменов, где был перехвачен трафик
Выявленная уязвимость с указанием доменов, где был перехвачен трафик
Могу сказать только одно: подумайте о своих клиентах и не бойтесь реализовывать пиннинг, в нем нет ничего страшного. Даже ситуацию с “протуханием” сертификата можно попробовать обойти. Как один из вариантов, использовать встроенный механизм реализации Pinning на уровне системы, а именно конфигурацию сетевого взаимодействия (а именно XML-файл, в котором настраиваются параметры сетевой безопасности для приложения Android). Данная настройка задается специальным атрибутом android:networkSecurityConfig в AndroidManifest.xml.

Пример подключения:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
Network Security Config позволяет достаточно просто подключить механизм Certificate Pinning в приложение. Но при этом стоит учитывать определенные нюансы. Рассмотрим конфигурацию, которая с первого взгляда выглядит, как правильно настроенная, и разберем, как ее можно немного улучшить:

<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set>
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
</pin-set>
</domain-config>
</network-security-config>
Этот пример имеет два небольших недостатка:

  1. Для отпечатка сертификата (pin-set) не установлен срок действия;
  2. Нет резервного сертификата.
Если срок действия вашего сертификата подойдет к концу и в настройках не указан срок действия, приложение перестанет подключаться к серверу и будет выдавать ошибку. Если срок действия установлен и он подойдет к концу, приложение перейдет на использование доверенных центров сертификации, используемых в системе. И вместо того, чтобы получить неработоспособное приложение, вы получите отсутствие SSL Pinning в течении некоторого времени, пока не обновите сертификат в приложении. Этот небольшой трюк как раз позволяет избежать ситуации неработоспособности приложения в случае, если не успели выпустить обновление приложения или клиент его еще не установил.

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

Вот пример наиболее корректного использования функционала Certificate Pinning:

<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2021-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
<!-- backup pin -->
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
</domain-config>
</network-security-config>
В iOS существует механизм под названием AppTransport Security в который аналогичным образом настраиваются сертификаты для приложения (ключ NSPinnedDomains). Более подробно про этот механизм я напишу в следующих статьях и в них рассмотрим настройку более детально. Отдельное спасибо за информацию об этом ключе пользователю @y4ppieflu.

Хранение лишних файлов в приложении​

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

Но иногда встречаются вещи и похуже, например, когда забывают файлы сборки внутри приложения. Ведь в сложных проектах используется достаточно много внешних компонентов, включая сложные скрипты сборки и прочие нюансы, знать про которые обычному пользователю совершенно ни к чему. Так, в одном из проанализированных нашим инструментом приложении мы нашли Pod-файл, который описывал все зависимости, их версии и расположение. Часть из них была во внутренних репозиториях, а значит, мы узнали из этого файла немного о внутренней инфраструктуре, адресах репозиториев и хранилища компонент. Используя данную информацию, можно, например, попробовать проэксплуатировать уязвимость Dependency Confusion, со всеми вытекающими последствиями. Вишенкой на торте были несколько телеграмм-аккаунтов разработчиков. И это в приложении, которое уже далеко не первый год существует и далеко не у самой последней компании на рынке:

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

Для своего успокоения, в случае с iOS это можно проверить в настройках Xcode:

  1. Если отсутствует файл пользовательских настроек для сборки, необходимо его создать:
1fabf8ba2bf4c2180f06213345952ade.png

  1. Добавить туда ключ настройки “EXCLUDED_SOURCE_FILE_NAMES“, если он отсутствует:
87a7cb5e77150780db80fa64b6532725.png

  1. Добавить значение настройки, какие файлы и папки необходимо исключить из конечной сборки приложения:
336d69fc7e2807219b57da6742b5960f.png

Заключение​

Статья получилась далеко не новаторской, об этом уже писали много раз, но против статистики не пойдешь, все эти “детские“ болячки и правда до сих пор присутствуют во многих приложениях. Конечно, здесь представлен далеко не полный список проблем, которые мы видим каждый день во многих приложениях. Есть еще целый ряд интересных уязвимостей и подходов к их обнаружению, о которых хочется рассказать, но уже не в рамках данной статьи. Мы продолжаем собирать статистику и в скором времени вернемся с новым материалом.

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

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

 
Сверху