Сегодня в выпуске: история нашумевшей уязвимости в смартфонах на базе процессоров MediaTek, подводные камни портирования Android на iPhone, история о том, почему функция скрытия root с помощью Magisk вскоре станет бесполезной. А также: инлайновые классы и принцип композиции в Kotlin, новый способ запуска активностей и запроса полномочий, несколько новых инструментов пентеста и библиотек для программистов.
 

Почитать

 

История уязвимости устройств на процессорах MediaTek

Critical MediaTek rootkit affecting millions of Android devices has been out in the open for months — история обнаружения и попыток залатать критический баг в устройствах на базе китайских процессоров MediaTek.

Краткая предыстория: в начале марта по всем сколько-нибудь связанным с безопасностью и мобильными устройствами сайтам прокатилась новость о весьма опасной уязвимости, выявленной буквально во всех устройствах, использующих 64-битные процессоры MediaTek. Мало того что уязвимость позволяла получить права root и отключить SELinux (одну из базовых систем безопасности Android), она еще и была очень проста в эксплуатации (один незатейливый эксплоит, не требующий особых условий для успешного взлома).

Широкая общественность узнала об уязвимости благодаря мартовскому патчу безопасности Android, в котором она получила пометку критической. Однако история обнаружения уязвимости начинается вовсе не с патча, а с поста пользователя diplomatic на форуме XDA Developers. Пост был посвящен планшету Amazon Fire.

Еще год назад diplomatic обнаружил, что драйвер CMDQ в ядре Linux для процессоров MediaTek принимает ioctl-команды от кого угодно. С помощью этих команд можно получить доступ к DMA-буферу, модифицировать память ядра и отключить SELinux. Позже diplomatic и другие пользователи форума выяснили, что эксплоит работает почти на всех устройствах с 64-битным процессором MediaTek за исключением устройств Vivо, OPPO, Huawei и Samsung с Android 8 и выше, на которых есть защита от получения прав root с помощью эксплоитов.

Компания MediaTek пропатчила драйвер еще в мае 2019 года, но это не просто не решило проблемы. Дело в том, что MediaTek производит процессоры низкого ценового диапазона, и их устанавливают в бюджетники, поддержка которых нередко заканчивается в момент выпуска смартфона с конвейера. Это те самые Blackview, Elefone и прочий китайский ширпотреб. Теперь все это — переносной бэкдор, беззащитный перед простейшей малварью, установленной из варезников (а такая уже существует — обнаруженный Trend Micro троян использует уязвимость MediaTek наряду с уязвимостью CVE-2019-2215 для получения контроля над устройством).

Именно по этой причине патч для MediaTek в итоге попал в состав официального патча безопасности Android. Это дает шанс, что хотя бы крупные производители обновят свои устройства, чтобы не нарушать договор о двухгодичной поддержке устройств. С другой стороны, доля пропатченных устройств вряд ли поднимется выше 1%.

CVE-2020-0069
CVE-2020-0069
 

История портирования Android на iPhone

An adventure 13 years in the making — история проекта Sandcastle, в рамках которого разработчики из компании Corellium сумели портировать Android на iPhone.

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

Но интересно даже не это, а то, с какими сложностями столкнулись разработчики. В первую очередь это кастомный процессор Apple, который вроде бы совместим со стандартным ARM, но имеет массу мелких отличий: «почти» совместимый с Samsung UART-контроллер, «почти» совместимый с Samsung SPI-контроллер, нестандартный контроллер прерываний, собственный способ включения дополнительных ядер процессора и так далее.

Еще более интересная история произошла с портированием платформы Android поверх уже портированного ядра Linux. Оказалось, что Android в принципе не поддерживает работу со страницами памяти с отличным от 4 Кбайт размером (Apple использует 16 Кбайт). Также Android оказался не совсем 64-разрядной системой: во многих местах ее до сих пор можно найти 32-битный код, который просто не будет работать на полностью 64-битном процессоре Apple.

Парадоксально, но порт Android для iPhone стал первой полностью 64-битной сборкой Android в истории.

 

Факт разлочки загрузчика больше не скрыть

Magisk may no longer be able to hide bootloader unlocking from apps — статья о том, как Google обыграла разработчика Magisk и сделала скрытие факта разлочки загрузчика невозможным.

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

Чтобы скрыть root, Magisk использует несколько трюков, которые обманывают приложение и систему SafetyNet, предназначенную для проверки смартфона на безопасность. Долгое время SafetyNet использовала эвристические методы для определения root и разблокировки загрузчика (которая требуется для установки Magisk).

Однако пользователи начали замечать, что некоторые смартфоны больше не проходят проверку SafetyNet с установленным Magisk. Разработчик Magisk ответил, что иногда SafetyNet больше не полагается на простую проверку состояния загрузчика (которую умеет обманывать Magisk), а вместо этого использует приватный ключ шифрования из защищенного хранилища Keystore, чтобы подтвердить достоверность переданных данных.

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

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

 

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

 

StartActivityForResult в 2020 году

A first look at AndroidX Activity Result APIs — небольшая заметка о решении одной из самых раздражающих задач, возникающих при разработке приложений для Android.

Речь идет о функции startActivityForResult(), которая позволяет запустить активность другого приложения, чтобы переложить на нее решение определенной задачи: получение снимка с помощью камеры, выбор файла и так далее. Данный механизм серьезно облегчает жизнь разработчика, но реализован самым неудобным из возможных способов. Разработчику необходимо запустить активность, передав ей специальный код, а затем ждать результат в колбэке, реализованном с помощью переопределения метода onActivityResult() в активности или фрагменте. И все бы ничего, но точно таким же способом реализован запрос полномочий, так что код приложения в итоге расползается по множеству внешне никак не связанных между собой функций.

Существует масса способов решения этой проблемы с помощью сторонних библиотек, но эта статья рассказывает об официальном решении от Google. В альфа-версии библиотеки AndroidX Activity наконец появился удобный в использовании API, позволяющий работать с активностями других приложений, не размазывая код по активности своего приложения.

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

class MyContract : ActivityResultContract<Int, String>() {
    companion object {
        const val ACTION = "com.myapp.action.MY_ACTION"
        const val INPUT_INT = "input_int"
        const val OUTPUT_STRING = "output_string"
    }

    override fun createIntent(input: Int): Intent {
        return Intent(ACTION)
            .apply { putExtra(INPUT_INT, input) }
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        return when (resultCode) {
            Activity.RESULT_OK -> intent?.getStringExtra(OUTPUT_STRING)
            else -> null
        }
    }
}

Затем мы используем prepareCall(), чтобы создать объект класса ActivityResultsLauncher, с помощью которого запускаем активность и получаем результат в колбэке:

class MyActivity : AppCompatActivity() {
    private val myActionCall = prepareCall(MyContract()) { result ->
        Log.i("MyActivity", "Obtained result: $result")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        button.setOnClickListener {
            myActionCall(500)
        }
    }
}

Выглядит немного сложно. Но такой подход не разрушает связность кода. Кроме того, уже сейчас библиотека содержит несколько предопределенных контрактов, в том числе для получения снимка (TakePicture), выполнения звонка (Dial) и, конечно же, запроса полномочий (RequestPermission).

 

Инлайн-классы в Kotlin 1.3

Zero-cost* abstractions in Kotlin — статья с подробным объяснением новой экспериментальной языковой конструкции Kotlin под названием inline-классы.

Одна из ключевых особенностей языка Kotlin — null safety, которая гарантирует, что программист не сможет по ошибке вызвать методы объекта, имеющего значение null, или передать этот объект в качестве аргумента другим методам. Null safety существенно повышает надежность кода, но не защищает от других ошибок программиста.

Допустим, у тебя есть база котов и собак, которых ты идентифицируешь по ID. Также у тебя есть метод getDogById(dogId: Long), который возвращает информацию о собаке с конкретным ID. Очевидно, что, если в качестве ID собаки ты передашь методу ID кошки, это будет ошибкой, которая приведет к неопределенному результату. Но ни среда разработки, ни компилятор не скажут тебе о ней.

Еще во времена Java программисты придумали метод обойти эту проблему с помощью так называемых классов-оберток. Ты просто создаешь класс DogId с единственным полем (ID собаки) и используешь его везде, где раньше использовал тип Long в качестве ID. Все остальное компилятор и среда разработки сделают за тебя: они просто не позволят передать DogId в качестве аргумента функции, которая ожидает CatId, — это ошибка.

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

И здесь на сцену выходят инлайн-классы. По своей сути инлайн-класс — это класс-враппер с одним параметром, который при компиляции разворачивается в этот параметр, чтобы избежать накладных расходов. Например:

inline class DogId(val id: Long)
val dog = getDogById(DogId(100L))

Данный код написан с использованием враппера, чтобы избежать описанной выше ошибки. Однако при компиляции объект DogId будет заменен Long, так что никаких дополнительных накладных расходов не потребуется.

Компилятор накладывает следующие ограничения на инлайн-классы:

  • не больше одного параметра;
  • никаких теневых полей;
  • никаких блоков инициализации;
  • никакого наследования.

Однако инлайн-классы могут:

  • реализовать интерфейс;
  • иметь свойства и функции.

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

Например, функции для работы с коллекциями (listOf(), setOf() и им подобные) обычно принимают на вход параметр типа Object или Any, так что переданный им объект инлайн-класса развернут не будет. Функция equals() также принимает в качестве аргумента тип Any, поэтому следующие два примера работают одинаково, но второй приведет к дополнительным накладным расходам:

val doggo1 = DogId(1L)
val doggo2 = DogId(2L)

// Оба объекта будут развернуты
doggo1 == doggo2

// doggo1 будет развернут, а doggo2 — нет
doggo1.equals(doggo2)

Объект не будет развернут и если объект инлайн-класса передать функции, аргумент которой имеет nullable-тип:

val doggo = DogId(1L)

fun pet(doggoId: DogId?) {}

// Объект не будет развернут
pet(doggo)

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

fun pet(doggoId: Long) {}
fun pet(doggoId: DogId) {}

Ну и последнее, что стоит иметь в виду: инлайн-классы — это экспериментальная возможность, которая может измениться со временем или будет удалена.

 

Композиция против наследования в Kotlin

Composition over inheritance (and Kotlin) — небольшая заметка, хорошо иллюстрирующая принцип композиции объектов и его преимущества перед наследованием.

Взгляни на следующий код:

open class Parent {
    fun parentFunctionality() {}
}

open class Child(): Parent() {
    fun childFunctionality() {}
}

class Grandchild constructor() : Child() {
    fun grandchildFunctionality() {}
}

Это канонический пример наследования в объектно ориентированном программировании. Объект класса Grandchild сможет вызывать методы parentFunctionality() и childFunctionality(). Код красив и замечателен. Но представь себе, что будет, если сильно усложнить этот пример, добавив в каждый класс множество новых открытых методов и связав их между собой. В какой-то момент может оказаться, что ты переопределяешь метод, который используется другим методом, и таким образом ломаешь функциональность всего объекта.

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

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

Предыдущий код, переписанный с использованием принципа композиции, будет выглядеть так:

class Parent {
    fun parentFunctionality() {}
}

class Child() {
    private val parent = Parent()

    fun parentFunctionality() { parent.parentFunctionality() }
    fun childFunctionality() {}
}

class Grandchild  {
    private val parent = Parent()
    private val child = Child()

    fun parentFunctionality() { parent.parentFunctionality() }
    fun childFunctionality() { child.childFunctionality() }
    fun grandchildFunctionality() {}
}

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

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

Функция-делегат lazy также помогает создавать композиции, а точнее минимизировать возможный оверхед. В следующем коде объект parent будет создан только в момент первого обращения к нему, то есть не будет занимать дополнительную память, если вообще не используется:

open class Parent {
    fun parentFunctionality() {}
}

open class Child() {
    val parent by lazy { Parent() }
    ...
    fun childFunctionality() {}
}

Ну и последнее — функции-расширения, которые позволяют добавить новые методы к существующему классу без необходимости наследоваться от него:

class SystemClass {
    ...
}

fun SystemClass.newFunctionality() {}

SystemClass().newFunctionality()
 

Инструменты

  • Fufluns — скрипт для быстрого анализа APK- и IPA-файлов;
  • Hook_location_frida.js — скрипт Frida для подделки местоположения.
 

Библиотеки

  • AnimatedBottomBar — анимированная панель навигации в нижней части экрана;
  • AndroidColorX — набор функций-расширений Kotlin для работы с цветом;
  • CompoundTextView — TextView, внутри которого можно произвольно расположить изображение;
  • Kotlin-numpy — биндинги Kotlin для библиотеки NumPy;
  • MotionToast — анимированные toast-сообщения;
  • RoomExplorer — инструмент просмотра баз данных прямо через приложение.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии