Android: борьба Google с малварью, платная разлочка Huawei и множество советов по языку Kotlin
Источник: t.me/Bureau121
Содержание статьи
Почитать
Как Google борется с малварью
Huawei и платная разблокировка загрузчика
Как использовать Frida для обхода упаковщиков
Разработчику
Трюки с Kotlin
Профайлинг сетевых функций приложения
Не используй массивы в data-классах
Parallelism vs concurrency
Трюки с короутинами Kotlin
Удобочитаемый logcat
Инструменты
Библиотеки
Почитать
Как Google борется с малварью
Combating Potentially Harmful Applications with Machine Learning at Google: Datasets and Models — рассказ разработчиков команды безопасности Android о том, как работает система Google Play Protect, которая выявляет вредоносные приложения в Google Play и на смартфонах пользователей. Несколько тезисов:
- Google анализирует не только приложения из Google Play, но и любые APK-файлы, найденные в интернете.
- Для каждого приложения запускаются процедуры статического и динамического анализа, которые выявляют определенные шаблоны: запрашиваемые разрешения, поведение приложения в тех или иных обстоятельствах.
- Данные, полученные от статических и динамических анализаторов, передаются ИИ, натренированному на выявление определенных типов зловредных приложений: SMS-фрод, фишинг, повышение привилегий.
- Кроме данных о самих приложениях, Google также собирает и агрегирует данные о приложении из Google Play: средняя оценка приложений разработчика, рейтинги, количество установок и удалений; эта информация также передается ИИ.
- На основе всех этих данных ИИ выносит решение о том, может ли приложение быть потенциально зловредным.
- Google постоянно совершенствует ИИ, скармливая ему данные свежевыявленных зловредов.
На фоне всей этой бравады стоит напомнить, что в тестах антивирусов Google Play Protect набирает 0 очков и плетется в конце рейтинга. По информации на январь 2018-го он смог обнаружить лишь 63% вирусов.
Huawei и платная разблокировка загрузчика
Bootloader unlocking is still possible for Huawei and Honor devices, but it’ll cost you — интересная заметка о страданиях владельцев смартфонов Huawei, желающих разблокировать загрузчик своего устройства.
В мае этого года Huawei официально заявила, что больше не будет предоставлять коды для разблокировки загрузчиков своих смартфонов (а также смартфонов своего суббренда Honor). Загрузчик отвечает за проверку целостности и цифровой подписи ядра ОС, а также за возможность прошивки устройства, так что те, кто не успел получить свой код разблокировки, теперь не могут установить на нее кастомную прошивку или даже получить права root с помощью Magisk.
Выяснилось, однако, что совсем недавно сервис FunkyHuawei, занимающийся выпуском утилит для восстановления окирпиченных смартфонов и смены их региона, начал предоставлять возможность разблокировки загрузчика всех моделей Huawei, включая самые свежие. Но есть один нюанс: стоимость кода разблокировки составляет 55 долларов, и он привязан к IMEI устройства, а значит, не может быть использован повторно.
В комментариях к статье уже появились шутки о том, что FunkyHuawei принадлежит самой Huawei и таким образом она пытается дополнительно заработать на пользователях. Шутки шутками, но вопрос о том, где сервис берет коды разблокировки, весьма серьезный. Либо это действительно Huawei, либо коды у нее просто крадут, либо алгоритм их генерации настолько слабый, что его удалось подобрать. Все три варианта не в пользу компании.
Как использовать Frida для обхода упаковщиков
How-to Guide: Defeating an Android Packer with FRIDA — вводная статья об использовании Frida для помощи в анализе вируса.
Дано: вредоносное приложение с подозрительным файлом внутри пакета и небольшим сильно обфусцированным исполняемым dex-файлом. Анализ логов запуска logcat показывает, что приложение при работе создает и загружает еще один dex-файл (запакованный в JAR), а это значит, что, скорее всего, первый исполняемый файл — это всего лишь загрузчик (а точнее, упаковщик), а найденный ранее подозрительный файл — зашифрованный код приложения. При загрузке приложения упаковщик дешифрует файл и загружает его. Но есть одна проблема — сразу после загрузки дешифрованный файл удаляется и его невозможно проанализировать.
К сожалению, декомпиляция и статический анализ упаковщика ничего не дают — он слишком сильно обфусцирован и почти непригоден для чтения. Однако запуск приложения под управлением трассировочной утилиты strace показывает, что удаление происходит с помощью системного вызова unlink
.
Конечная идея — переопределить с помощью Frida код функции unlink
так, чтобы она ничего не удаляла. В этом случае исследователь сможет просто достать расшифрованный dex-файл из устройства и проанализировать его. Код функции перехвата для Frida:
console.log("[*] FRIDA started"); console.log("[*] skip native unlink function"); var unlinkPtr = Module.findExportByName(null, 'unlink'); Interceptor.replace(unlinkPtr, new NativeCallback(function (){ console.log("[*] unlink() encountered, skipping it."); }, 'int', []));
Трассировочный листинг, показывающий создание и удаление файла с расшифрованным кодом приложения
Разработчику
Трюки с Kotlin
Kotlin fun and education on Twitter — создатель Kotlin Academy и автор книги «Android Development with Kotlin» Марсин Москала (Marcin Moskala) рассказывает об интересных трюках, которые можно провернуть в Kotlin.
В Kotlin fun
— это зарезервированное ключевое слово, но его можно использовать, если заключить в обратные кавычки или написать с большой буквы. Те же правила действуют в отношении любых других ключевых слов.
Используя обратные кавычки, можно пойти еще дальше и включить в имена не только ключевые слова, но и пробелы, и даже смайлы.
Как и многие другие языки, Kotlin поддерживает перегрузку операторов.
fold
— одна из самых мощных операций для работы с коллекциями. Она объединяет все элементы коллекции с помощью указанной функции. Например, с помощью fold
очень легко сложить или перемножить все элементы. Но можно сделать и более интересные вещи.
Еще несколько полезных функций для выполнения операций над несколькими последовательными элементами.
Профайлинг сетевых функций приложения
Various methods to debug HTTP traffic in Android applications — статья о том, как выполнять профайлинг сетевых функций приложения. Автор предлагает использовать пять различных методов.
- Android Profiler — включен в состав Android Studio. Он показывает объем входящих и исходящих данных, задержки и даже позволяет взглянуть на сами данные (если приложение использует HttpURLConnection или OkHttp). По умолчанию сетевой профайлер отключен. Чтобы включить его, необходимо зайти в меню Run → Edit Configurations, открыть таб Profiling и поставить галочку напротив Enable advanced profiling. После этого он появится в окне стандартного профайлера (запускается через нижнюю панель Android Studio).
- OkHttp Profiler plugin — плагин Android Studio для отладки реквестов OkHttp. Умеет показывать JSON в виде дерева и генерировать модели для парсера GSON. Недостаток: требуется установка плагина и модификация приложения.
- Stetho — инструмент отладки приложений с помощью Chrome DevTools. Кроме инструментов для анализа лайотов и баз данных, включает в себя мощный сетевой профайлер. Требует установки библиотеки и модификации приложения.
- Charles Proxy — десктопный прокси со встроенным сниффером и множеством дополнительных функций: это мониторинг сокетов, модификация сетевых пакетов, генератор реквестов и многое другое. Требует настройки на эмуляторе/телефоне прокси и установки SSL-сертификата (в случае если требуется отладка HTTPS-трафика). К тому же стоит 50 долларов (есть триальная версия).
- AppSpector — инструменты профайлинга для Android и iOS. Позволяют просматривать логи, изучать базы данных и сетевые запросы. Необходима модификация приложения и регистрация на сайте. Управление только через веб-сайт разработчиков, так что возникает вопрос о конфиденциальности данных.
Network Profiler в Android Studio
Не используй массивы в data-классах
What you didn’t know about arrays in Kotlin — познавательная и полезная заметка о том, почему не стоит использовать массивы в data-классах в Kotlin.
Data-классы в Kotlin — очень полезный элемент языка, позволяющий быстро создавать классы, не обременяя себя написанием однотипного кода. Возьмем, к примеру, следующий код:
data class NumArray(val name: String, val values: IntArray)
Он объявляет data-класс NumArray с двумя полями. Это всего одна строка кода, но в результате ты получишь класс с уже реализованными геттерами, сеттерами и функциями equals(), hash(), toString(). Тебе не придется писать их самому, и это сильно облегчает жизнь.
Но есть одна проблема: если ты создашь два одинаковых объекта этого класса и попробуешь их сравнить, то получишь отрицательный ответ:
val n1 = NumArray("1", intArrayOf(1,2,3,4)) val n2 = NumArray("1", intArrayOf(1,2,3,4)) val result = n1==n2 println("result=$result")
Получается, что автоматически генерируемый метод equals() не работает? На самом деле это не так. Дело в том, что в JVM есть баг, который приводит к тому, что сравнение массивов и коллекций происходит по-разному. Коллекции сравниваются структурно, то есть поэлементно, а при сравнении массивов верный ответ будет только в том случае, если это действительно один и тот же массив, а не два с одинаковым набором элементов.
Поэтому вместо массивов лучше использовать списки:
data class NumList(val name: String, val values: List<Int>))
Parallelism vs concurrency
Concurrent Coroutines — Concurrency is not Parallelism — одна из лучших статей для тех, кто хочет разобраться, что такое короутины Kotlin. Вместо того чтобы рассказывать о стейт-машинах и математических алгоритмах, автор говорит о том, что надо просто осознать разницу между понятиями concurrency и parallelism.
Роб Пайк, один из разработчиков Unix, Plan 9 и языка Go, говорит об этих понятиях так: concurrency — это когда ты имеешь дело со множеством вещей одновременно, а parallelism — это когда ты делаешь множество вещей одновременно. Продемонстрируем это утверждение кодом:
fun main() = runBlocking<Unit> { val time = measureTimeMillis { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms") } suspend fun doSomethingUsefulOne(): Int { delay(1000L) return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) return 29 }
Здесь происходит запуск двух короутин, каждая из которых засыпает на одну секунду, а затем возвращает определенное число. Основная короутина при этом дожидается завершения обеих дочерних короутин и печатает на экран сумму двух возвращенных чисел.
Приложение заканчивает свою работу ровно за одну секунду, что абсолютно логично. Но есть один важный нюанс: оно работает в одном потоке. Когда одна из дочерних короутин блокируется (в данном случае с помощью delay, а в реальном приложении из-за ожидания данных из сети или с диска), основная короутина продолжает свою работу. Это и есть тот случай, когда приложение «имеет дело» с множеством вещей, а не делает их одновременно.
Но мы можем исправить код так, чтобы он действительно делал несколько вещей одновременно:
val time = measureTimeMillis { val one = async(Dispatchers.Default) { doSomethingUsefulOne() } val two = async(Dispatchers.Default) { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") }
Такой код запускает каждую короутину в отдельном потоке (Dispatchers.Main — основной поток приложения, Dispatchers.Default — один из фоновых), и да, теперь приложение делает несколько вещей одновременно.
В этом примере от такой замены нет никакой пользы, но в реальном приложении в фоновые потоки можно отправить тяжелые вычисления, а запускать код, ожидающий данные из сети или с диска (или просто часто спящий), эффективнее в основном потоке. В конце концов, nginx, один из самых производительных веб-серверов, однопоточный (точнее, он использует по одному потоку на ядро, но сути это не меняет).
Трюки с короутинами Kotlin
Advanced Kotlin Coroutines tips and tricks — еще одна статья о короутинах, в этот раз с советами по использованию.
1. Проблемы с Java API. Возьмем следующий пример:
runBlocking(Dispatchers.IO) { withTimeout(1000) { val socket = ServerSocket(42) socket.accept() } }
Подразумевается, что этот код запустит короутину, которая будет ждать подключения ровно одну секунду и затем будет прервана.
Но этого не произойдет, потому что socket.accept()
заблокирует поток короутины, до тех пор пока кто-нибудь действительно не подключится.
Обойти эту проблему можно с помощью suspendCancellableCoroutine
. Создадим небольшую функцию-помощник:
public suspend inline fun <T : Closeable?, R> T.useCancellably( crossinline block: (T) -> R ): R = suspendCancellableCoroutine { cont -> cont.invokeOnCancellation { this?.close() } cont.resume(use(block)) }
И слегка изменим наш пример:
runBlocking(Dispatchers.IO) { withTimeout(1000) { val socket = ServerSocket(42) socket.useCancellably { it.accept() } } }
Теперь все работает как надо.
2. launch vs. async. Это два самых используемых короутин-билдера в Kotlin. И тот и другой порождают новую короутину, но работают немного по-разному:
- любое необработанное исключение в блоке launch будет воспринято как необработанное вообще и уронит приложение; исключение в блоке async можно обработать за пределами этого блока;
- launch не позволит короутинам-родителям завершиться, пока короутины-потомки, запущенные с помощью launch, не завершатся;
- async позволяет вернуть значение, launch — нет.
Удобочитаемый logcat
How To Customize Logcat Appearance in Android Studio — короткая заметка о том, как раскрасить вывод logcat в Android Studio для более удобного чтения. Для этого достаточно перейти в настройки, набрать в поиске logcat, и ты увидишь список уровней логирования от Debug до Assert. Выбирай один из них, отключай галочку Inherit values from и в опциях Foregraund, Background и других выбирай нужные цвета.
Автор статьи предлагает следующую схему цветов:
- ASSERT:
#bb2b2f
; - DEBUG:
#1194d6
; - ERROR:
#db332f
; - INFO:
#0c890d
; - VERBOSE:
#a8a8a8
; - WARNING:
#bb7000
.
Также он создал цветовую схему на базе тем Default и Dracula.
Инструменты
- frida-snippets — набор скриптов Frida для Android, iOS и Windows;
- MagiskFrida — скрипт для запуска сервера Frida при загрузке с помощью Magisk;
- ADBHoney — ханипот, эмулирующий доступный на 5555-м порте демон ADB;
- ish — приложение для запуска Linux-окружения в iOS (использует эмуляцию x86 и трансляцию системных вызовов Linux → XNU);
- androix-migration — скрипт для автоматической миграции с support-библиотек на AndroidX, по словам автора, работает лучше, чем аналогичный конвертер в Android Studio;
- autoplay — плагин Gradle для автоматической публикации приложений в Google Play.