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

Почитать

 

Новый метод рутинга

Kernel Assisted Superuser (KernelSU) — The Final Frontier for SafetyNet and an Essential Developer Tool — небольшая статья о KernelSU, новом способе рутинга Android путем прямого патчинга ядра.

В последнее время одним из основных методов получения прав root на Android стал Magisk. Он использует так называемый systemless-способ рутинга, когда вместо модификации раздела system поверх него подключается виртуальный раздел, содержащий бинарный файл su, необходимый приложениям для получения прав root. Такой метод позволяет избежать проблем с обновлениями, а также эффективно скрывать наличие прав root на устройстве.

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

Метод KernelSU, предложенный разработчиком zx2c4, базируется на совершенно другой идее. Вместо подключения виртуального раздела или физического размещения файла su в разделе system он использует модифицированное ядро, чтобы заставить приложения «думать», что в системе действительно есть файл /system/bin/su. Ядро перехватывает все обращения к этому файлу и, если приложение пытается с его помощью запустить команды, автоматически исполняет их с правами root.

Работая прямо в ядре, KernelSU имеет гораздо больше возможностей для скрытия и обхода различных ограничений Android, в том числе правил SELinux.

В данный момент проект KernelSU находится в зачаточной стадии развития. Доступен только патч, который энтузиасты могут использовать для сборки кастомных ядер.

 

Небезопасный IPC в софте для Android

Security Code Smells in Android ICC — большое исследование безопасности приложений, использующих механизмы межпроцессного взаимодействия Android. Авторы взяли около 700 открытых приложений из репозитория F-Droid и проанализировали, есть ли в их коде проблемы в использовании IPC.

Анализ был произведен с помощью специально созданного инструмента AndroidLintSecurityChecks, который показывает наличие в коде потенциальных брешей. Все проблемы скомпоновали в 12 категорий:

  • SM01: Persisted Dynamic Permission. В Android есть механизм, позволяющий предоставить другому приложению временный доступ к какому-либо URI своего ContentProvider’а. Это делается с помощью метода Context.grantUriPermission(). Если приложение вызывает его, но не вызывает Context.revokeUriPermission(), чтобы отозвать доступ, — есть проблемы.
  • SM02: Custom Scheme Channel. Любое приложение может зарегистрировать собственную URI-схему, такую как myapp://, вне зависимости от того, использует ли такую схему другое приложение. Как следствие, пересылать важные данные, используя кастомные URI-схемы, крайне небезопасно.
  • SM03: Incorrect Protection Level. В Android есть система разрешений и любое приложение может создать свое собственное разрешение для доступа к своим данным. Но есть проблема: если указать неправильный уровень защиты разрешения (protection level), оно может не сработать. Если разработчик хочет, чтобы пользователь видел диалог запроса разрешений, он должен использовать уровень защиты dangerous или signature, если данное разрешение должно получать только приложение с той же цифровой подписью.
  • SM04: Unauthorized Intent. Любое приложение в Android может зарегистрировать себя в качестве обработчика определенных типов интентов (intent). По умолчанию этот обработчик будет открыт всему миру, но его можно защитить с помощью системы разрешений и строгой валидации входных данных.
  • SM05: Sticky Broadcast. Любое приложение может послать другому приложению интент. Более того, оно может послать широковещательный интент сразу всем приложениям, и он будет обработан первым приложением, способным его принять. Но есть также возможность послать широковещательный sticky-intent, который после обработки одним приложением все равно будет доставлен другим приложениям. Чтобы этого не происходило, не стоит использовать такие интенты, а от широковещательных интентов лучше отказаться вообще.
  • SM06: Slack WebViewClient. Компонент WebView позволяет приложениям показывать веб-страницы внутри своего интерфейса. По умолчанию он никак не фильтрует открываемые URL, чем можно воспользоваться, например, для фишинга. Разработчикам стоит либо использовать белый список адресов, либо выполнять проверку с помощью SafetyNet API.
  • SM07: Broken Service Permission. Приложения могут предоставлять доступ к своей функциональности с помощью сервисов. Злоумышленник может использовать эту возможность для запуска кода с повышенными полномочиями (полномочиями сервиса). Чтобы этого избежать, сервис должен проверять полномочия вызывающего приложения с помощью метода Context.checkCallingPermission().
  • SM08: Insecure Path Permission. Некоторые приложения предоставляют доступ к своим данным с помощью ContentProvider’а, который адресует данные, используя UNIX-подобные пути: /a/b/c. Программист может открыть доступ к своему ContentProvider’у, но отрезать доступ к некоторым путям (например, к /data/secret). Но есть проблема: разработчики часто используют класс UriMatcher для сравнения путей, а он, в отличие от Android, сравнивает их без учета двойных слешей. Отсюда могут возникнуть ошибки при разрешении и запрете доступа.
  • SM09: Broken Path Permission Precedence. Сходная с предыдущей проблема. При описании ContentProvider’а в манифесте разработчик может указать, какие разрешения нужны приложению для доступа к определенным путям. Но в Android есть баг, из-за чего он отдает предпочтение более глобальным путям. Например, если приложение дает доступ к /data всем подряд, но использует специальное разрешение для доступа к /data/secret, то в итоге доступ к /data/secret смогут получить все.
  • SM10: Unprotected Broadcast Receiver. Фактически аналог проблемы SM04, но распространяющийся исключительно на BroadcastReceiver’ы (специальные обработчики интентов).
  • SM11: Implicit Pending Intent. Кроме интентов, в Android есть сущность под названием PendingIntent. Это своего рода отложенные интенты, которые могут быть отправлены позже и даже другим приложением от имени создавшего интент приложения. Если PendingIntent широковещательный, то любое приложение сможет перехватить его и послать интент от имени этого приложения.
  • SM12: Common Task Affinity. Об этой проблеме мы уже рассказывали в «Хакере». Суть в следующем: в Android все экраны приложения (activity) объединяются в таск, который представляет собой своего рода стопку экранов. В то же время в Android есть средство, которое позволяет одному приложению всунуть свой экран в стопку другого приложения. Для этого буквально достаточно указать имя этого приложения в атрибуте android:taskAffinity у активности, которую нужно внедрить. А чтобы защититься, разработчик должен указать в этом атрибуте пустую строку.
Общая распространенность ошибок
Общая распространенность ошибок

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

 

Вредоносные библиотеки

A Confusing Dependency — поучительная история о том, как можно добавить в приложение зловредный код, всего лишь подключив популярную библиотеку.

Все началось с того, что автор решил подключить к проекту библиотеку AndroidAudioRecorder и обнаружил, что сразу после старта приложение крашится, выбрасывая исключение java.lang.SecurityException: Permission denied (missing INTERNET permission?). Это означает, что приложение не может получить доступ к интернету, так как отсутствует необходимое для этого разрешение.

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

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

Суть истории. Существует репозиторий Java-пакетов jCenter, привязанный к системе дистрибуции Bintray. Android Studio использует jCenter как дефолтовый репозиторий для новых проектов: он уже включен в список репозиториев в build.gradle наряду с репозиторием Google. Однако многие разработчики предпочитают размещать свои библиотеки в репозитории JitPack, который умеет автоматически генерировать и выкладывать в репозиторий библиотеки из GitHub-репозитория (это удобно и просто).

Библиотека AndroidAudioRecorder также была выложена в JitPack, так что автор статьи перед ее использованием добавил JitPack в build.gradle. Но оказалось, что в jCenter тоже была выложена эта библиотека с внедренным в нее зловредным кодом. А так как jCenter в списке репозиториев идет первым, система сборки взяла библиотеку именно из него, а не из JitPack.

Один из способов решения этой проблемы — разместить jCenter в конце списка репозиториев в build.gradle.

Кусок зловредного кода
Кусок зловредного кода
 

Разработчику

 

Советы по использованию короутин в Kotlin

Kotlin Coroutines patterns & anti-patterns — хорошая подборка советов и антисоветов о короутинах Kotlin.

Заворачивай вызовы async в coroutineScope или используй SupervisorJob для работы с исключениями

val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)

// Может выбросить исключение
fun doWork(): Deferred<String> = scope.async { ... }

fun loadData() = scope.launch {
    )try {
        doWork().await()
    } catch (e: Exception) { ... }
}

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

Чтобы избежать этого, достаточно использовать SupervisorJob:

val job = SupervisorJob() // <--
val scope = CoroutineScope(Dispatchers.Default + job)

// Может выбросить исключение
fun doWork(): Deferred<String> = scope.async { ... }

fun loadData() = scope.launch {
    try {
        doWork().await()
    } catch (e: Exception) { ... }
}

Используй Main Dispatcher в корневой короутине

Если тебе необходимо постоянно вызывать короутины Main Dispatcher (например, для обновления экрана), используй Main Dispatcher как основную короутину.

Большая часть следующего кода выполняется в рамках Main Dispatcher:

val scope = CoroutineScope(Dispatchers.Default)

fun login() = scope.launch {
    withContext(Dispatcher.Main) { view.showLoading() }
    networkClient.login(...)
    withContext(Dispatcher.Main) { view.hideLoading() }
}

Так почему бы не переписать код так, чтобы основная часть была в Main Dispatcher:

val scope = CoroutineScope(Dispatchers.Main)

fun login() = scope.launch {
    view.showLoading()    
    withContext(Dispatcher.IO) { networkClient.login(...) }
    view.hideLoading()
}

Избегай использования async/await там, где это не нужно

Код, подобный этому:

launch {
    val data = async(Dispatchers.Default) { /* code */ }.await()
}

лучше заменить на такой:

launch {
    val data = withContext(Dispatchers.Default) { /* code */ }
}

Этот код не порождает новые короутины, более производителен и нагляден.

Избегай завершения Scope Job

Представим такой код:

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)

    fun doWork1() {
        scope.launch { /* do work */ }
    }

    fun doWork2() {
        scope.launch { /* do work */ }
    }

    fun cancelAllWork() {
        job.cancel()
    }
}

fun main() {
    val workManager = WorkManager()

    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

Его проблема в том, что повторно короутина через метод doWork1 не запустится, потому что корневая для нее задача уже завершена.

Вместо этого следует использовать функцию cancelChildren:

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)

    fun doWork1(): Job = scope.launch { /* do work */ }

    fun doWork2(): Job = scope.launch { /* do work */ }

    fun cancelAllWork() {
        scope.coroutineContext.cancelChildren()
    }
}

fun main() {
    val workManager = WorkManager()

    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

Постарайся не писать suspend-функции с неявным диспетчером

Представь такую функцию:

suspend fun login(): Result {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()

    return result
}

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

launch(Dispatcher.Main) {     // Все нормально
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // Падение
    val loginResult = login()
    ...
}

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Правильный вариант:

suspend fun login(): Result = withContext(Dispatcher.Main) {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()

    return result
}

Избегай использования GlobalScope

Если ты в своем коде постоянно делаешь

GlobalScope.launch {
    // code
}

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

В Android с этим еще проще. Короутины можно ограничивать активностями, фрагментами, View, ViewModel:

class MainActivity : AppCompatActivity(), CoroutineScope {

    private val job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onDestroy() {
        super.onDestroy()
        coroutineContext.cancelChildren()
    }

    fun loadData() = launch {
        // code
    }
}
 

Как максимально сократить размер приложения

Build your Android app Faster and Smaller than ever — статья о том, как сделать приложения компактнее и собирать их быстрее. Вторая часть статьи (про скорость сборки) не особо полезна и интересна, поэтому остановимся только на способах уменьшения размера APK. Итак, как сделать приложение меньше?

  • Удалить неиспользуемые ресурсы: Refactor → Remove unused resources.
  • Удалить лишние зависимости. Многие библиотеки позволяют включать в приложение не всю библиотеку, а только ее части. Чтобы узнать, из каких компонентов состоит библиотека, можно использовать команду ./gradlew app:dependencies.
  • Разбить приложение на несколько APK в зависимости от DPI экрана. Для этого есть директива splits в build.gradle, но, если ты выкладываешь приложение исключительно в Google Play, лучше использовать Applicaton Bundle, который магазин приложений потом сам разбивает на отдельные APK.
  • Разбить приложение на несколько APK в зависимости от архитектуры процессора. Для этого можно использовать все те же splits или Applicaton Bundle.
  • Собрать приложение только с нужными ресурсами. Если нужна версия приложения исключительно для локального рынка, можно использовать директиву resConfigs.
  • Удалить неиспользуемые ресурсы. Директива minifyEnabled сжимает код, но, кроме нее, также существует директива shrinkResources, которая сжимает ресурсы, выбрасывая неиспользуемые.
  • Использовать Shape Drawable. Если есть необходимость залить фон градиентом или отобразить какую-нибудь фигуру, лучше использовать Shape Drawable вместо изображений.
  • Использовать WebP. Формат WebP обеспечивает на 30% лучшее сжатие в сравнении с PNG.
  • Использовать VectorDrawable. Начиная с версии 5 Android поддерживает векторные иконки. Они намного компактнее обычных.
Удаляем ненужные ресурсы
Удаляем ненужные ресурсы
 

Трюк с инициализацией библиотек

Your android libraries should not ask for an application context — короткая заметка о работе системы инициализации Firebase.

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

class MainApplication : Application() {
    override fun onCreate(){
        super.onCreate()

        // Инициализация четырех библиотек
        Fabric.with(this, new Crashlytics())
        Stetho.initializeWithDefaults(this)
        JodaTimeAndroid.init(this)
        Realm.init(this)
    }
}

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

На самом деле секрет в том, что в файле AndroidManifest.xml библиотеки Firebase есть такой кусок:

<provider
    android:name="com.google.firebase.provider.FirebaseInitProvider"
    android:authorities="${applicationId}.firebaseinitprovider"
    android:exported="false"
    android:initOrder="100" />

Это декларация ContentProvider’а. Но это не ContentProvider. Класс FirebaseInitProvider как раз и содержит код инициализации библиотеки.

Во время сборки приложения среда разработки объединяет файлы AndroidManifest.xml твоего приложения и всех подключенных библиотек в единый файл. А во время запуска приложения Android выполняет код инициализации всех провайдеров еще до запуска самого приложения. Так и получается, что инициализация Firebase происходит на ранней стадии без посторонней помощи.

 

Инструменты

 

Библиотеки

  • Cyanea — мощный движок тем, позволяющий на лету применять темы;
  • log4k — библиотека логирования для Kotlin, вдохновленная log4j;
  • Rialto — библиотека для стилизации текста с использованием аннотаций;
  • Barista — библиотека UI-тестирования на основе Espresso;
  • slycalendarview — календарь в стиле Material Design, позволяющий выбирать дни или интервалы;
  • Regret — своего рода стек для объектов, позволяющий выполнять операции Undo/Redo;
  • klaster — библиотека для быстрого создания адаптеров для RecyclerView в функциональном стиле;
  • KM-Quick-Adapter — еще один генератор адаптеров для RecyclerView;
  • Hasher — простая в использовании библиотека хеширования (алгоритмы MD5, SHA-1, SHA-256, SHA-384 и SHA-512);
  • Valigator — библиотека для валидации полей EditText и вывода на экран предупреждений;
  • Android-EditText-Validations — еще один валидатор для TextView;
  • Material Popup Menu — popup-меню в стиле Material Design;
  • Flutter-for-Android-developers — коллекция материалов для изучающих фреймворк Flutter.

1 комментарий

  1. Tuw

    07.01.2019 at 21:41

    Евгений, здравствуйте! Можете пожалуйста сделать свежую/ статью про перехват трафика в приложениях, где есть ссл пиннинг? Старые методы уже не работают увы, даже iptables не пашет. К примеру приложение snapchat, не отснифать никак вообще, зверь такой, что ужас.

Оставить мнение

Check Also

Мифы об аде. Как и зачем программировать на аде в современном мире

Язык, разработанный по заказу Министерства обороны США и названный в честь первой в мире п…