Как обойти защиту на доступ к скрытым методам
Developers: It’s super easy to bypass Android’s hidden API restrictions — детальный рассказ, как обойти защиту на доступ к скрытым методам в Android 9 и выше.
Как и любая другая ОС, Android предоставляет разработчикам доступ к обширному API, который позволяет вызывать те или иные функции ОС. Этот API включает в себя ряд скрытых, но иногда очень полезных функций, например возможность развернуть строку состояния. Вызвать эти функции напрямую не получится, так как их просто нет в SDK. Но можно использовать модифицированный SDK (сложно) или рефлексию (очень просто).
Рефлексия позволяет дотянуться до любых методов и полей классов, что, конечно же, можно использовать для не совсем легальной деятельности. Поэтому, начиная с Android 9, Google создала черный список методов и полей, недоступных для вызова с помощью рефлексии. Если приложение попробует их вызвать, то либо будет принудительно остановлено, либо получит предупреждение (в случае с методами из серого списка).
Но есть в этой защите одна проблема — она основана на проверке имени вызывающего процесса. А это значит, что, если мы не будем вызывать метод напрямую, а попросим саму систему сделать это за нас, проверка даст добро (не может же она запрещать саму себя).
Итак, стандартный способ вызова скрытого метода с помощью рефлексии (не работает, приложение завершается):
val someHiddenClass = Class.forName("android.some.hidden.Class") val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java) someHiddenMethod.invoke(null, "some important string")
Новый способ вызова скрытого метода с помощью двойной рефлексии (работает, потому что вызывает метод не наше приложение, а сама система):
val forName = Class::class.java.getMethod("forName", String::class.java) val getMethod = Class::class.java.getMethod("getMethod", String::class.java, arrayOf<Class<*>>()::class.java) val someHiddenClass = forName.invoke(null, "android.some.hidden.Class") as Class<*> val someHiddenMethod = getMethod.invoke(someHiddenClass, "someHiddenMethod", String::class.java) someHiddenMethod.invoke(null, "some important string")
Но и это еще не все: с помощью этого трюка мы можем вызывать очень интересный скрытый метод setHiddenApiExemptions
, который позволяет (бам!) добавить нужные нам методы в исключения и вызывать их с помощью простой рефлексии.
Следующий код прикажет системе добавить в исключения вообще все скрытые методы:
val forName = Class::class.java.getDeclaredMethod("forName", String::class.java) val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String::class.java, arrayOf<Class<*>>()::class.java) val vmRuntimeClass = forName.invoke(null, "dalvik.system.VMRuntime") as Class<*> val getRuntime = getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null) as Method val setHiddenApiExemptions = getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", arrayOf(arrayOf<String>()::class.java)) as Method val vmRuntime = getRuntime.invoke(null) setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))
Стоит отметить, что Google в курсе этой проблемы. Они отклонили баг-репорт о возможности вызова скрытых методов под предлогом, что это защита от дураков, а не security-фича.
Как обнаружить Magisk
Detecting Magisk Hide — статья о том, как обнаружить присутствие Magisk (и, как следствие, прав root) на устройстве.
Magisk — известный, а в последнее время единственный инструмент systemless-рутинга устройств. Он позволяет получить права root без изменения системного раздела, а также применить различные системные твики. Одна из широко используемых возможностей Magisk — функция Magisk Hide, которая позволяет полностью скрыть сам Magisk и наличие прав root на устройстве от выбранных приложений.
Принцип работы Magisk основан на подключении поверх файловой системы системного раздела другой файловой системы (оверлея), содержащей бинарный файл su (необходимый для получения прав root) и нужные для его работы компоненты. Подключение происходит на ранних этапах загрузки, но, если активирован Magisk Hide, он отключает оверлей для выбранных приложений. Другими словами, обычные приложения будут видеть содержимое оверлея, а те, что указаны в настройках Magisk Hide, — нет. С их точки зрения смартфон будет не рутован.
Но есть в Magisk Hide один изъян. Дело в том, что, если приложение, которое находится в списке для скрытия root, запустит сервис в изолированном процессе, Magisk также отключит для него оверлей, но в списке подключенных файловых систем (/proc/<pid>/mounts
) этот оверлей останется. Соответственно, чтобы обнаружить Magisk, необходимо запустить сервис в изолированном процессе и проверить список подключенных файловых систем.
Автор утверждает, что метод работает для последней версии Magisk на Android 8.0–10.0. Proof of concept можно найти на GitHub.
Посмотреть
Как Google вычисляет зловредные приложения
Why Does Google Think My App Is Harmful? — выступление Алека Гертина (Alec Guertin) на Android Dev Summit’19, посвященное облачному антивирусу Google Play Protect.
В своей работе Google Play Protect полагается на свыше чем 30 тысяч серверов, которые проводят статический анализ и постоянно запускают в эмуляторах и на реальных устройствах приложения, опубликованные в Google Play и за его пределами. Система обращает внимание на следующее поведение приложений:
- к каким URL обращается приложение, нет ли их в базе фишинговых URL;
- какие файлы записывает приложение и нет ли среди них файлов, которые не должны перезаписываться;
- не использует ли приложение известные методы получения прав root (имеются в виду эксплоиты, а не приложения для уже рутованных устройств);
- не слишком ли это приложение похоже на известное зловредное приложение (совпадение в 97%).
Отдельно автор доклада остановился на том, как рядовому разработчику не попасть под подозрение. Google Play Protect может посчитать твое приложение не вызывающим доверия в следующих случаях:
- неполное раскрытие информации о поведении приложения, например уведомить о сборе информации уже после ее сбора или не оговаривая, какие конкретно данные будут отправлены на сервер разработчика;
- использование сторонних SDK со зловредной функциональностью;
- альтернативные методы монетизации вроде майнинга крипты, а также неразглашение цены или автоматические платежи без согласия;
- неполное исправление уязвимостей, о которых сообщает консоль разработчика;
- неожиданное поведение приложения, например показ рекламы, которого, судя по коду, быть не должно.
Мифы о производительности
Performance Myth Busters — презентация разработчиков компилятора ART, используемого в Android для JIT/AOT-компиляции приложений, о мифах повышения производительности приложений. Итак, мифы.
- Код на Kotlin медленнее и толще Java. Эксперимент с конвертацией приложения Google Drive на Kotlin показал, что время запуска и размер приложения остались прежними, зато объем кодовой базы сократился на 25%.
- Геттеры замедляют код. Некоторые разработчики отказываются от использования геттеров в пользу публичных полей (
foo.bar
противfoo.getBar()
), надеясь повысить производительность. Однако это ничего не дает, потому что компилятор ART умеет инлайнить геттеры, превращая их в обычные обращения к полям. - Лямбды замедляют код. Лямбды считаются очень медленными, и ты можешь встретить советы заменить их вложенными классами. На самом деле после компиляции лямбды превращаются в те же анонимные вложенные классы.
- Аллокация объектов и сборка мусора в Android — медленные. Когда-то это действительно было правдой, но за последние годы разработчики сделали аллокатор объектов в десятки раз, а сборщик мусора — в несколько раз быстрее. Например, на Android 10 автоматическая аллокация объектов в тестах производительности ничем не отличается от аллокации и освобождения пула объектов вручную (именно такой способ рекомендовали использовать для сохранения производительности ранее).
- Профайлинг дебаг-сборок — это нормально. Обычно при профайлинге приложения разработчики не обращают внимания, что имеют дело с debug-сборкой. В результате они анализируют производительность неоптимизированного кода и получают неверные результаты профайлинга.
- У MultiDex-приложений более медленный холодный старт. При превышении лимита на количество методов компилятор разбивает приложение на несколько исполняемых файлов DEX. Считается, что это замедляет старт приложения. На деле если такой эффект и есть, то он проявляется только при наличии сотен файлов DEX. С другой стороны, MultiDex-приложения занимают на несколько процентов больше оперативной и постоянной памяти.
Разработчику
Безопасность коммуникаций
Securing Network and Inter-App Communications on Android — вводная статья о том, как обезопасить коммуникацию приложения с сетевым сервером и с другими приложениями.
Одна из основных проблем сетевой коммуникации заключается в возможности перехвата трафика, поэтому первое, что необходимо сделать при разработке приложения, — добавить файл сетевой конфигурации, который запретит использовать незашифрованные подключения (начиная с Android 7.0). Сам файл может иметь произвольное имя, но должен располагаться в каталоге res/xml
. Также на него нужно сослаться из манифеста:
<?xml version="1.0" encoding="utf-8"?> <manifest ... > <application android:networkSecurityConfig="@xml/config" ...> ... </application> </manifest>
Далее приведен файл конфигурации, который запрещает незашифрованный трафик для всех доменов за исключением localhost, а также позволяет использовать для debug-сборок собственный центр сертификации. Необязательно включать в конфиг все эти опции.
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <!-- Запретить незашифрованный трафик для всех доменов, кроме перечисленных в исключениях --> <base-config cleartextTrafficPermitted="false" /> <!-- Домены, с которыми разрешен обмен незашифрованным трафиком --> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">localhost</domain> <trust-anchors> <!-- Кроме системных, также доверяем сертификату из файла debug_certificate --> <certificates src="system" /> <certificates src="@raw/debug_certificate" /> </trust-anchors> </domain-config> <!-- Debug-версия приложения будет доверять также нашему собственному CA --> <debug-overrides> <trust-anchors> <certificates src="@raw/my_ca"/> </trust-anchors> </debug-overrides> </network-security-config>
Конфигурационный файл также можно использовать для Certificate Pinning. Это нужно для того, чтобы приложение могло удостовериться, что удаленный сервер действительно использует настоящий сертификат безопасности. Для этого надо получить хеш SHA-256 этого сертификата и прописать его в нужное поле (его можно узнать с помощью браузера):
<domain-config> <domain includeSubdomains="true">website.net</domain> <pin-set expiration="2020-04-16"> <pin digest="SHA256">хеш</pin> <pin digest="SHA-256">хеш</pin> </pin-set> </domain-config>
Обмен данными между приложениями тоже необходимо защищать. Стандартный механизм обмена сообщениями в Android — это интенты (intent). Они позволяют отправить другому приложению (или группе приложений) сообщение или вызвать ту или иную функцию. Интенты могут быть направлены одному приложению или быть широковещательными (я отправляю сообщение системе, и она пусть разбирается, кому сообщение предназначено, — например, вызовет стандартный браузер, чтобы открыть указанную веб-страницу). Проблема с широковещательными интентами в том, что их могут получить приложения, с которыми не стоит обмениваться информацией. Чтобы решить проблему, можно вызвать диалог выбора, и тогда пользователь сможет сам указать приложение, которое должно получить интент:
val intent = Intent(Intent.ACTION_SEND) val activityList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL) when { activityList.size > 1 -> { val chooser = Intent.createChooser(intent, "Choose an App") startActivity(chooser) } intent.resolveActivity(packageManager) != null -> startActivity(intent) else -> Toast.makeText(this, "No App to launch with", Toast.LENGTH_LONG).show() }
Но что, если ты хочешь защитить приложение от получения любых интентов, кроме отправленных другими твоими приложениями (например, ты реализовал систему плагинов)? Для этого можно создать новое полномочие и автоматически предоставить его приложениям, подписанным тем же ключом.
Сначала объявляем разрешение:
<permission android:name="packageName.HelloWorldPermission" android:protectionLevel="signature" />
Затем защищаем с его помощью нужный компонент приложения:
<provider android:name="android.support.v4.content.FileProvider" ... android:permission="packageName.HelloWorldPermission"/>
В данном случае мы защитили ContentProvider, но также его можно сделать совсем невидимым для других приложений с помощью атрибута android:exported="false"
.
Польза функций-расширений
Kotlin extension functions: more than sugar — краткая статья о пользе функций-расширений Kotlin. Тех самых, что позволяют добавить метод к любому классу (своему или чужому) на лету.
- Функции-расширения улучшают читаемость кода. Например, строка
string.emojify()
выглядит явно лучше, чемemojify(string)
, и тем более лучше, чемStringUtils.emojify(string)
. - Функции-расширения позволяют сделать класс легче и удобнее для чтения и понимания. Если, например, какой-то набор приватных методов класса нужен только одному публичному методу, их вместе с публичным методом можно вынести в расширения.
- Функции-расширения облегчают написание кода, так как IDE будет автоматически подсказывать, какие методы и функции-расширения есть у класса.
Классы-делегаты в Kotlin
Kotlin Delegates in Android: Utilizing the power of Delegated Properties in Android development — статья о делегированных свойствах в Kotlin и о том, как их можно применять при разработке для Android.
Делегированные свойства — это поля класса (или глобальные переменные), обращение к которым вызовет код специального класса-делегата. Простейший пример использования делегированных свойств будет выглядеть так:
class TrimDelegate : ReadWriteProperty<Any?, String> { private var trimmedValue: String = "" override fun getValue( thisRef: Any?, property: KProperty<*> ): String { return trimmedValue } override fun setValue( thisRef: Any?, property: KProperty<*>, value: String ) { trimmedValue = value.trim() } }
Все, что делает этот класс-делегат, — триммит строку (отбрасывает начальные и конечные пробелы), записанную в переменную. Далее, если объявить переменную, используя этот класс-делегат, записанные в нее строки будут автоматически триммиться:
var param: String by TrimDelegate() param = " string "
В Android делегированные свойства очень удобно использовать для обращения к опциям с помощью SharedPreferences. Просто создай следующую функцию-расширение для класса SharedPreferences:
fun SharedPreferences.string( defaultValue: String = "", key: (KProperty<*>) -> String = KProperty<*>::name ): ReadWriteProperty<Any, String> = object : ReadWriteProperty<Any, String> { override fun getValue( thisRef: Any, property: KProperty<*> ) = getString(key(property), defaultValue) override fun setValue( thisRef: Any, property: KProperty<*>, value: String ) = edit().putString(key(property), value).apply() }
Объяви переменную, которая будет привязана к нужной опции, и просто записывай/читай значения. Они будут автоматически сохранены в файл настроек:
var option3 by prefs.string( key = { "option3" }, defaultValue = "default" ) option3 = "new_value"
Инструменты
- Can My Phone Run Linux? — сайт, позволяющий узнать, поддерживает ли твое устройство установку стороннего дистрибутива Linux (OpenEmbedded, PostmarketOS, Ubuntu Touch и так далее);
- Frizzer — фаззер общего назначения, основанный на Frida;
- WhatsDump — скрипт для извлечения ключа шифрования WhatsApp;
- iOS-related scripts — набор скриптов для реверса iOS.
Библиотеки
- FreeReflection — библиотека, позволяющая обойти ограничение на доступ к скрытым API с помощью рефлексии (в Android 9 и 10);
- StringPacks — библиотека для более эффективного хранения строк перевода в пакете (разработчик — WhatsApp);
- Croppy — экран обрезки изображений;
- LiquidSwipe — красивая анимация перехода между страницами ViewPager;
- EasyReveal — библиотека для эффектной смены заднего фона приложения;
- Shortcut — библиотека для удобного создания динамических ярлыков приложения (те, что показываются при долгом удержании иконки);
- ChiliPhotoPicker — библиотека для выбора фотографий на карте памяти;
- FlipTabs — простой и эффектный переключатель между двумя табами;
- Recycleradapter-generator — автоматический генератор адаптеров для RecyclerView;
- IndicatorScrollView — ScrollView, визуально реагирующий на промотку экрана;
- StoryView — библиотека, реализующая сториз а-ля Instagram;
- Falcon — библиотека для LRU-кеширования сериализуемых объектов в памяти и на диске;
- Flow-preferences — версия Rx-preferences, переписанная на Kotlin Flow API;
- FlowRiddles — набор заданий для изучения Kotlin Flow API.