Android: прайваси в Android 12 и анализ трояна FluBot
Сегодня в выпуске: изменения в защите приватных данных пользователей в Android 12, исследование проблем неправильной настройки облачных сервисов в приложениях, статья о правилах разбора и анализа кода малвари на примере FluBot, а также смешная уязвимость в приложении Medium. Как бонус: оптимизация запуска приложения и очередные советы разработчикам.
ПОЧИТАТЬ
Android 12 и прайваси
What’s new in Android Privacy — официальный анонс изменений в защите пользовательских данных в Android 12.
- Privacy-экран. В Android 12 будет специальный экран настроек, показывающий, как часто установленные приложения получали доступ к микрофону, камере, местоположению и другим сенсорам и данным.
- Индикаторы камеры и микрофона. Вслед за Apple инженеры Google добавили в Android 12 индикаторы доступа к камере и микрофону. Если какое‑то приложение в данный момент использует то или другое, в верхней части экрана появится соответствующая иконка. Более того, после открытия шторки можно будет увидеть, какие конкретно приложения имеют доступ к камере и микрофону, и немедленно запретить доступ.
- Приблизительное местоположение. Диалог запроса местоположения будет давать возможность выбрать, каким типом информации о местоположении делиться с приложением: точным местоположением либо приблизительным. Второе хорошо подходит, например, для погодных сервисов.
- Уведомление о чтении буфера обмена. Android 12 будет показывать сообщение каждый раз, когда какое‑либо приложение прочитает содержимое буфера обмена. Единственное исключение, когда этого не произойдет, — если буфер обмена читает приложение, находящееся в текущий момент на экране.
- Полномочие на обнаружение устройств. Обнаружение Bluetooth-устройств в текущей Android 11 и ниже требует разрешение на доступ к местоположению (да, именно так). В Android 12 у этой операции появится собственное специальное разрешение.
- Усыпление приложений. Android 11 умеет отзывать разрешения у давно не используемых приложений. Android 12 идет еще дальше и полностью «усыпляет» такие приложения, забирая у них занимаемое дисковое пространство. Для вывода приложения из сна достаточно запустить его.
Проблемы неправильной настройки облачных сервисов
Mobile app developers’ misconfiguration of third party services leave personal data of over 100 million exposed — статья о таких уязвимостях приложений, которые вызваны неправильной настройкой облачных сервисов. Никаких практических примеров взлома в статье нет, но общая статистика весьма интересна.
- Неправильное конфигурирование облачной БД. Здесь все просто, открытые на всеобщее обозрение базы данных до сих пор часто встречаются в том числе в приложениях. Как пример: приложение‑гороскоп Astro Guru сохраняет в базу данных email пользователя, его имя, пол, местоположение и дату рождения. А приложение для заказа такси T’Leva хранит в открытой БД полную переписку между пассажирами и водителями.
- Пуш‑уведомления. Большинство сервисов push-уведомлений для отправки уведомления от имени конкретного приложения требуют использовать привязанный к нему криптографический ключ. Но что, если этот ключ будет вшит в код самого приложения в открытом виде? В этом случае ключ можно будет извлечь и использовать для отправки уведомлений от имени этого приложения.
- Сетевые хранилища. С сетевыми хранилищами проблема обычно та же, что и с облачными БД. Если не настроить аутентификацию или вшить ключи доступа прямо в код приложения — данные будут в опасности. От этой проблемы, например, страдает приложение Screen Recorder с 10 миллионами установок.
Стоит сказать, что многие разработчики все‑таки пытаются скрыть ключи доступа к облачным сервисам, но обычно делают это столь неумело, что ключи извлекаются тривиально. Например, приложение iFax пытается скрыть ключи, используя Base64, поэтому все, что нужно сделать, — это просто скопировать закодированную в Base64 строку и раскодировать ее с помощью любой утилиты, умеющей работать с этим форматом. Разбивка ключей на части и использование XOR также популярные решения. Забавно, что именно так делает малварь CopyCat.
Еще одна уязвимость в приложении Medium
Exploiting Activity in medium android app — короткая заметка об очередной дурацкой уязвимости в мобильном приложении блог‑платформы Medium.
Суть статьи сводится к следующему. В приложении есть активность SaveToMediumActivity
. Все, что она делает, — это сохраняет статью в список избранного. Оказалось, что активность не только доступна извне любому другому приложению (экспортирована), но и позволяет сохранять в список какой угодно URL, а не только статьи, опубликованные на самом Medium.
Как итог: в список избранного можно легко добавить что угодно с помощью простейшего приложения или такой команды:
$ adb shell am start -n com.medium.reader/com.medium.android.donkey.save.SaveToMediumActivity -e android.intent.extra.TEXT "https://attacker.com"
Ах да. За все это Medium заплатил баг‑баунти.
Разборка и анализ малвари
How to analyze mobile malware: a Cabassous/FluBot Case study — статья об этапах анализа зловредных приложений на примере трояна Cabassous/FluBot.
- Для начала распаковываем APK с помощью apktool или любого другого аналогичного инструмента.
- Открываем файл
AndroidManifest.xml
и находим основную активность приложения. Она имеет следующий интент‑фильтр: <intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
- В случае с FluBot активность имеет имя
com.tencent.mobileqq.MainActivity
. Но в пакете такой активности нет. - Смотрим, какие еще интересные файлы и классы есть в пакете. В случае с FluBot пакет также содержал следующие файлы:
classes-v1.bin
в каталоге dex (возможно, зашифрованный код малвари), пакетcom.whatsapp
в декомпилированном коде приложения, пакетыn
,np
иobfuse
(явно обфусцированные) и библиотекуlibreactnativeblob.so
(ее также можно найти в приложении WhatsApp). - Очевидно, что авторы малвари просто перепаковали WhatsApp, включив в него зловредную функциональность. Поэтому следующее, что нужно сделать, — узнать, чем отличаются оригинальный пакет WhatsApp и оказавшийся у нас образец. Сделать это можно с помощью утилиты apkdiff:
$ python3 apkdiff.py ../com.whatsapp_2.21.3.19-210319006_minAPI16\(x86\)\(nodpi\)_apkmirror.com.apk ../Cabassous.apk
- В данном случае автор сравнивает малварь с WhatsApp версии 2.21.3.19 потому, что в дизассемблированных листингах малвари (в файле
AbstractAppShell
) удалось найти имя версии: 2.21.3.19-play-release. - Оказалось, что авторы малвари модифицировали всего четыре файла оригинального WhatsApp и в общем в них нет ничего интересного. Поэтому следующий шаг — попытка выяснить, где же находится основная активность приложения. Очевидно, что она в файле
classes-v1.bin
, но чем он запакован? Для начала попробуем использовать утилиту APKiD.
- Она не дает результата. В таком случае стоит попробовать разобраться в коде классов тех самых пакетов
n
,np
иobfuse
. Однако они сильно обфусцированы с использованием имен классов в юникоде и рефлексии. В этом можно разобраться, но проще использовать динамический анализ. - Чтобы выполнить динамический анализ, мы должны понять, как малварь вообще запускает свою зловредную функциональность. Как мы уже видели, активности, указанной в манифесте, вообще не существует в коде. Это значит, что она находится где‑то в запакованном (и зашифрованном) коде и поэтому где‑то должен существовать код распаковки. Часто авторы малвари засовывают такой код в блоки статической инициализации Java, которые получают управление сразу после загрузки класса и до запуска основной активности приложения.
- Dexcalibur — один из самых удобных инструментов динамического анализа. По сути, это графическая обертка для Frida с преднастроенными хуками и возможностью быстрой установки своих хуков. В случае с FluBot Dexcalibur показывает все использования рефлексии, однако функции распаковки среди них нет. Это происходит потому, что блоки статической инициализации выполняются еще до начала работы Frida.
- Теперь у нас есть три пути: попробовать все‑таки разобраться в обфусцированном коде загрузчика и удалить из него код удаления распакованного DEX-файла (обычно малварь удаляет его сразу после загрузки в память); найти все статические инициализаторы, превратить их в обычные статические функции и написать код их вызова; скопировать дизассемблированный код приложения в Android Studio и запустить его под управлением отладчика.
- Оказалось, однако, что все это не нужно, так как малварь по какой‑то причине забывает удалить распакованный код после его распаковки. Поэтому достаточно вытащить уже распакованный DEX-файл из приватного каталога приложения:
$ adb shell
hammerhead:/ $ su
hammerhead:/ # cp /data/data/com.tencent.mobileqq/app_apkprotector_dex /data/local/tmp/classes-v1.bin
hammerhead:/ # chmod 666 /data/local/tmp/classes-v1.bin
hammerhead:/ # exit
hammerhead:/ $ exit
$ adb pull /data/local/tmp/classes-v1.bin payload.dex
/data/local/tmp/classes-v1.bin: 1 file pulled. 18.0 MB/s (3229988 bytes in 0.171s)
- Теперь можно спокойно заняться анализом кода малвари. Здесь все достаточно стандартно, и об этом можно прочитать в оригинале статьи.
РАЗРАБОТЧИКУ
Оптимизация скорости запуска приложения
From zero to hero: Optimizing Android app startup time — очередная статья об оптимизации скорости запуска приложений для Android. Ничего нового она не рассказывает, но автор приводит несколько весьма интересных наблюдений.
- Koin не медленнее Dagger. Принято считать, что так называемый service locator, которым является Koin, медленнее «настоящего» DI-фреймворка, такого как Dagger. Замеры автора показывают, что после перевода приложения с Koin на Dagger скорость запуска приложения совсем не меняется. Koin быстрый.
- Создание объектов — дешевая операция. В старые времена считалось хорошей практикой использовать пулы объектов вместо создания объектов с нуля. Современные версии Android позволяют создавать объекты настолько быстро, что пулы объектов перестали вносить какую‑то роль в производительность приложения.
- Очистка класса Application не помогает. Часто в голову разработчиков приходит идея убрать из класса Application все тяжелые операции или вынести их в фоновые потоки. Как ни странно, это ничего не дает.
- Отложенная инициализация Firebase помогает. Около 60 миллисекунд можно выиграть на отложенной инициализации Firebase с помощью androidx.startup.
- Опция android:useEmbeddedDex=true может помочь. По умолчанию Android запускает код приложения не из самого пакета приложения, а из специально подготовленного odex-файла, содержащего оптимизированный и частично скомпилированный в машинные инструкции код. Такой код быстрее работает, но, как оказалось, дольше запускается. Если использовать опцию android:useEmbeddedDex=true, можно добиться ускорения запуска приложения, но проиграть в скорости работы. Кому что важнее.
- onCreate — то место, где обычно кроется проблема. Логично, что большую часть времени запуска сжирает метод
onCreate
основной активности приложения. Это именно то место, куда стоит приложить усилия. - Обфускация с помощью ProGuard серьезно повышает производительность. ProGuard сокращает размер кода приложения, так что его влияние достаточно ожидаемо. С другой стороны, автор не уточняет, просто ли он включил ProGuard или протестировал релизную сборку приложения. Все‑таки дебаг‑сборки всегда работают медленнее из‑за логирования и отключенных оптимизаций в библиотеках (в одном из предыдущих выпусков дайджеста было показано, что отладочная сборка замедляет инициализацию библиотеки корутин примерно в десять раз — до 100 миллисекунд).
Очередная подборка советов
Top Put_your_number Kotlin utils we use all over our project — пять приемов разработки для Android, которые могут помочь писать более понятный и тестопригодный код. Наиболее интересные приемы:
Прокси для доступа к ресурсам. В Android есть класс Resources
, который предназначен для доступа к ресурсам приложения. Стандартный путь получения объекта этого класса — через Context. Однако это не всегда удобно. Например, при биндинге ViewHolder’а получать доступ к ресурсам придется примерно так:
viewHolder.itemView.context.resources.getDimensionPixelSize(R.dimen.my_dimen)
Очень длинная строка. К тому же мы не сможем внятно протестировать этот код (без использования грязных хаков вроде Robolectric).
Решение обеих проблем состоит в том, чтобы создать обертку для класса Resources:
interface ResourcesProvider {
val isRtl: Boolean
@ColorInt
fun getColor(@ColorRes resId: Int): Int
fun getString(@StringRes resId: Int): String
fun getString(@StringRes resId: Int, vararg args: Any): String
fun getDimen(@DimenRes resId: Int): Float
fun getDimenInt(@DimenRes resId: Int): Int
// Другие методы
}
@Suppress("TooManyFunctions")
inline class AppResourcesProvider(
private val context: Context
) : ResourcesProvider {
override val isRtl: Boolean get() = context.isRtl
@ColorInt
override fun getColor(resId: Int) = context.getColorCompat(resId)
override fun getColorStateList(resId: Int): ColorStateList? = context.getColorStateListCompat(resId)
override fun getString(resId: Int) = context.getString(resId)
override fun getString(resId: Int, vararg args: Any) = context.getString(resId, *args)
override fun getDimen(resId: Int): Float = context.resources.getDimension(resId)
override fun getDimenInt(resId: Int) = context.resources.getDimensionPixelSize(resId)
// Другие методы
}
Благодаря интерфейсу класс легко подделать (mocking) для тестирования. К тому же ему можно делегировать все вызовы типа getColor
в том же ViewHolder:
abstract class ResViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView), ResourcesProvider by AppResourcesProvider(itemView.context)
Просто и удобно, а благодаря использованию слова inline — в большинстве случаев класс даже не будет создан — код методов будет встроен в вызывающую сторону.
Функции‑расширения для изменения отступов. Во время разработки нередко возникает необходимость изменить отступ того или иного элемента интерфейса. Популярная библиотека расширений Android KTX предлагает делать это так:
view.updateLayoutParams {
updateMarginsRelative(start = newMargin)
}
И это довольно странный и совсем не описательный способ. Вместо этого мы бы хотели делать это как‑то так:
view.margin.start = newMargin
Код, позволяющий сделать это:
val View.margin get() = ViewMargin(this)
inline class ViewMargin(private val view: View) {
var start: Int
get() = view.marginStart
set(value) = applyMargin { marginStart = value }
var top: Int
get() = view.marginTop
set(value) = applyMargin { topMargin = value }
var end: Int
get() = view.marginEnd
set(value) = applyMargin { marginEnd = value }
var bottom: Int
get() = view.marginBottom
set(value) = applyMargin { bottomMargin = value }
var horizontal: Int
get() = start + end
set(value) {
start = value
end = value
}
var vertical: Int
get() = top + bottom
set(value) {
top = value
bottom = value
}
var total: Int
@Deprecated("No getter for that field", level = DeprecationLevel.HIDDEN)
get() = throw UnsupportedOperationException("No getter for such field")
set(value) {
horizontal = value
vertical = value
}
private inline fun applyMargin(body: ViewGroup.MarginLayoutParams.() -> Unit) {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams(body)
} else {
throw IllegalStateException("Parent layout doesn't support margins")
}
}
}