В этом выпуске: новый метод рутинга 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 находится в зачаточной стадии развития. Доступен только патч, который энтузиасты могут использовать для сборки кастомных ядер.
Security Code Smells in Android ICC — большое исследование безопасности приложений, использующих механизмы межпроцессного взаимодействия Android. Авторы взяли около 700 открытых приложений из репозитория F-Droid и проанализировали, есть ли в их коде проблемы в использовании IPC.
Анализ был произведен с помощью специально созданного инструмента AndroidLintSecurityChecks, который показывает наличие в коде потенциальных брешей. Все проблемы скомпоновали в 12 категорий:
Context.grantUriPermission()
. Если приложение вызывает его, но не вызывает Context.revokeUriPermission()
, чтобы отозвать доступ, — есть проблемы.myapp://
, вне зависимости от того, использует ли такую схему другое приложение. Как следствие, пересылать важные данные, используя кастомные URI-схемы, крайне небезопасно.Context.checkCallingPermission()
./a/b/c
. Программист может открыть доступ к своему ContentProvider’у, но отрезать доступ к некоторым путям (например, к /data/secret
). Но есть проблема: разработчики часто используют класс UriMatcher для сравнения путей, а он, в отличие от Android, сравнивает их без учета двойных слешей. Отсюда могут возникнуть ошибки при разрешении и запрете доступа./data
всем подряд, но использует специальное разрешение для доступа к /data/secret
, то в итоге доступ к /data/secret
смогут получить все.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 Coroutines patterns & anti-patterns — хорошая подборка советов и антисоветов о короутинах Kotlin.
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:
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() }
Код, подобный этому:
launch { val data = async(Dispatchers.Default) { /* code */ }.await() }
лучше заменить на такой:
launch { val data = withContext(Dispatchers.Default) { /* code */ } }
Этот код не порождает новые короутины, более производителен и нагляден.
Представим такой код:
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 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.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. Итак, как сделать приложение меньше?
./gradlew app:dependencies
.build.gradle
, но, если ты выкладываешь приложение исключительно в Google Play, лучше использовать Applicaton Bundle, который магазин приложений потом сам разбивает на отдельные APK.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) } }
Но также ты мог заметить, что библиотека Firbase ничего подобного не требует. Ты можешь сказать, что, возможно, ей вообще не нужен доступ к контексту или ее инициализация происходит позже, перед использованием. Но нет, требует, и она не просит инициализировать себя позже.
На самом деле секрет в том, что в файле 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 происходит на ранней стадии без посторонней помощи.
Читайте также
Последние новости