Уязвимости в андроид приложениях. Как их найти и предотвратить эксплуатацию?

Дисклеймер: Не стоит переживать, если вы не знакомы с разработкой под Android и языком программирования Java. В этой статье я постараюсь обрисовать все максимально ясно и для новичков (с минимальными знаниями программирования). Так же все практические примеры утрированы для общего понимания уязвимостей. 

Для кого эта статья?

  1. Для новичков в разработке под Android.
  2. Для новичков в ремесле пентестинга Android приложений.
  3. Для владельцев Android приложений. 

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

Используемый софт:

  1. Java-bytecode-viewer;
  2. Android Studio;
  3. Apktool.

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

Как работают Android приложения?

Код приложения хранится в файле APK, в котором есть файл .dex с двоичным байт-кодом Dalvik. Dalvik - это формат данных, понятный для ОС Android, но нечитаемый для человека. Поэтому для работы с файлами .dex необходимо преобразовать их в читаемый для человека формат, т.е в  smali код. Если вы мало что поняли, то не волнуйтесь, по ходу статьи все встанет на свои места.

Структура приложения

Структура приложения

Больше всего нас интересуют следующие файлы: 

  1. AndroidManifest.xml

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

  1. MainActivity

Код для основного(главного) экрана вашего андроид приложения. Для тех, кто не особо знаком с работой андроид приложений, поясню - любое андроид приложение представляет интерфейс для взаимодействия в виде так называемых activity. Activity - это не что иное, как экран вашего приложения (экран настроек, экран регистрации, экран открытия чата и т.д).

Каждому активити соответствует свой личный Java(Kotlin) класс, который «программирует» поведение экрана. (именно поэтому выше мы можем увидеть activity_main.xml, которому и соответствует наш MainActivity)

  1. strings.xml

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

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

Как выглядит отреверсированный код? 

Напишем самое простое приложение, которое складывает два числа и выводит результат на экран: 

Как выглядит отреверсированный код

А теперь посмотрим на это приложение с точки зрения хакера, используя java-bytecode-viewer: 

java-bytecode-viewer

 

java-bytecode-viewer

 

java-bytecode-viewer 

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

Чувствительные данные, хранящиеся открытым текстом

Давай зададим себе вопрос: за чем в первую очередь охотится хакер? Верно, пароли, API ключи и прочая чувствительная информация.

Зачастую разработчики банально забывают обфусцировать (запутать) чувствительные данные в коде.

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

Мы уже видели, что хакер может получить строки с открытым текстом, при вскрытии приложения.

Рассмотрим пример участка кода приложения «Погода».

пример участка кода приложения

Как вы могли увидеть, в коде открытым текстом хранится апи ключ.

Давайте попробуем его вытащить при помощи bytecode-viewer.

Попробуем вытащить апи ключ

Как вы можете наблюдать, в реверсированном коде мы четко видим API ключ, который хранится открытым текстом, и достать его совершенно не составило никакого труда.

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

Рекомендации по безопасности:

  1. Обфусцируйте (запутывайте) строки.
  2. Не задавайте строки с чувствительными данными явно в коде. 

Уязвимости бекенда

Эта уязвимость вытекает из уязвимости выше.

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

Поэтому, разрабатывая андроид приложения, не забывайте про защиту бэкенда, на который стучит ваше приложение. 

Рекомендации по безопасности:

  1. Регулярно доставляйте обновления ПО на сервера, к которым обращается ваше приложение.
  2. Регулярно проводите аудиты внутренней инфраструктуры, с которой общается приложение. 

Проблемы с криптографией 

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

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

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

Рассмотрим пример с реализацией слабого решения 

  1. Считываем ввод пароля.
  2. Кодируем его в Base64. (который декодируется за секунды)
  3. Записываем в файл на устройстве. 

Код: 

пример с реализацией слабого решения 

Вот так выглядит интерфейс: 

Вот так выглядит интерфейс

Теперь давайте посмотрим в файлы приложения: 

посмотрим в файлы приложения

Если декодировать значение файла, мы получим строку «password», которая и была введена в приложении.

Так что же будет самым хорошим решением данной проблемы?

Банально, использование криптостойких алгоритмов шифрования. Например, тот же RSA. Не стоит использовать: RC4, MD4, MD5, SHA1. Если хэши используются для хранения паролей, следует использовать хэши, устойчивые к брутфорсу, с солью. 

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

Рекомендации по безопасности:

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

Примитивные логические уязвимости

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

Вот как оно выглядит: 

Примитивные логические уязвимости

На самом деле в этом приложении два экрана, тот, который мы видим без купленного премиума, и второй экран, который скрыт от нас: 

второй экран, который скрыт от нас

Теперь давайте взглянем под капот программы, чтобы понять логику получения премиума: 

У нас есть три класса. (MainActivity, PremiumActivity, CheckPremium)

CheckPremium класс реализует следующую логику: 

CheckPremium класс реализует следующую логику

Как вы могли заметить, метод checkPremiumInDatabase всегда возвращает ноль, что в булевой логике соответствует значению «ложь».

Естественно, в реальном приложении в этом методе был бы реализован примерно такой алгоритм:

  1. Найти залогиненного пользователя в базе данных.
  2. Найти столбец в базе данных, отвечающий за наличие премиума, приобретенного, например, через официальный сайт.
  3. Отдать в значение возврата 1 или 0. (Премиум куплен/Не куплен) 

После записи нуля в значение метода идет проверка в MainActivity: 

проверка в MainActivity

В этом участке кода идет проверка значения метода checkPremiumInDatabase, если он равен 1, то вызывается второе активити, если нет, то ничего не происходит. 

А теперь давайте и сделаем то, что описано в этой главе, переделаем приложение так, чтобы оно при запуске всегда открывало нам активити со вторым экраном:

  1. Впишем в консоль команду
java -jar apktool_2.6.0.jar -r d app-debug.apk

Данная команда позволит декомпилировать наше приложение в вид smali кода, который мы сможем поменять через обычный текстовый редактор, чтобы изменить логику приложения (в bytecode-viewer нельзя изменить smali, поэтому будем это делать при помощи apktool). 

  1. Изменим значение возврата функции checkPremiumInDatabase

Сменим с нуля на единицу. (что в булевой алгебре означает «истина», ведь нужно вернуть именно единицу, дабы пройти проверку на премиум) 

Изменим значение возврата функции checkPremiumInDatabase

Но давайте для ясности поймем, что именно и где нужно менять в smali? (ведь он в разы менее читабелен, чем декомпилированный код)

В рамках данного примера мы вновь должны вернуться в декомпилированный код данной функции: 

декомпилированный код данной функции

Мы видим по этому скриншоту, что логика проверки премиума располагается с 4 по 7 строку кода.

А теперь вновь посмотрите на участок кода smali, где мы поменяли значение с нуля на единицу. Вверху мы видим вполне понятную надпись «.line7», которую в более крупном проекте мы бы могли использовать как зацепку для обнаружения слабого места программы. 

Итак, мы поменяли значение возврата с «0» на «1», сохранили. Теперь нужно его пересобрать:

java -jar apktool_2.6.0.jar b app-debug -o modifiedAPK.apk 

Осталось только запустить, но... Приложение не запустится. 

  1. Подпись приложения

Любое приложение перед запуском должно быть подписано, но когда приложение меняется, меняется и подпись, а вот уже на такое наша система Android не готова, приложение должно быть подписано заново. 

Для начала создадим ключ для подписи:

keytool -genkey -keystore keys.keystore -storepass password -alias keycodeby -keypass password -dname "CN=AndroidIB O=Codeby C=RU" -validity 10000 

Теперь подпишем наше приложение этим ключом

jarsigner.exe -keystore keys.keystore -storepass password -keypass password C:\Users\Администратор\Desktop\modifiedAPK.apk keycodeby 

Теперь подпишем наше приложение этим ключом

  1. Запуск измененного приложения 

Запуск измененного приложения

Итак, второй экран запущен и примитивная логика проверки премиума не отработала должным образом.

Рекомендации по безопасности

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

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

Излишние разрешения приложения, описанные в манифесте могут привести к краже и манипуляциям с данными, если на целевое устройство попадет вредоносное приложение. 

Настройка манифеста, где присутствует MODE_WORLD_READABLE и MODE_WORLD_WRITEABLE может привести к краже конфиденциальных данных с устройства. Но почему так?

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

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

Android предоставляет возможность каждому приложению сохранять XML файлы по пути:

/data/data/имя_пакета/shared_prefs/имя_пакета

Иногда в этой папке хранятся конфиденциальные данные открытым текстом. 

Также ОС Android позволяет каждому приложению сохранять базы данных sqlite по пути:

/data/data/имя_пакета/databases/имя_пакета

Эту папку необходимо проверить на наличие файлов, хранящихся открытым текстом.

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

/data/data/the.package.name/databases

Даже если БД зашифрована, утечка пароля через код также является уязвимостью.

Конечно, стоит проверить файл strings.xml, где разработчики случайно могут оставить чувствительные данные в виде паролей, ключей и т.д. 

чувствительные данные в виде паролей

Рекомендации по безопасности:

  1. Избегайте лишних разрешений в файле манифеста.
  2. Никогда не храните чувствительные данные в файле strings.xml.
  3. Не храните пароль от БД в виде открытого текста в коде приложения.
  4. Шифруйте файлы баз данных. 

Использование Журналов сбоев для поиска утечек

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

Рекомендации по безопасности:

  1. Обязательно используйте SSL для передачи любых данных.
  2. Следите за тем, что записывается в журналы сбоев. 

Общие рекомендации, не вошедшие в пункты статьи:

  1. Реализуйте проверку на эмулятор, в котором, скорее всего, и будет запущено ваше приложение. (особенно касается банковских приложений)
  2. Если в приложении есть ввод чувствительных данных (снова возьмем в пример банковские приложения), то создайте собственную клавиатуру для ввода данных. Это поможет защититься от атак с подменой клавиатуры.
  3. В файле манифеста запретите создание резервной копии данных при подключении мобильного устройства к компьютеру, прописав android:allowBackup в значение false.
  4. Используйте черное фоновое изображение, которое будет перекрывать экран приложения во время навигации по недавно запущенным приложениям, если в вашем приложении есть чувствительная информация, т.к вредонос может незаметно сделать скриншот.
  5. Реализуйте ограничения на посылку запросов для входа в аккаунт через приложение.
  6. Обфусцируйте названия классов и методов приложения после сборки приложения дабы запутать хакера при проведении реверс инжиниринга. (для этого можно использовать программу ProGuard)