Разработчики: обойти скрытые ограничения API Android очень легко

click fraud protection

Android 9 Pie и Android 10 выдают предупреждения или полностью блокируют доступ к скрытым API. Вот как разработчики могут обойти скрытые ограничения API.

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

Что это такое?

Разработчикам приложений Android не нужно начинать с нуля, когда они создают приложение. Google предоставляет инструменты, которые упрощают разработку приложений и делают ее менее повторяющейся. Одним из таких инструментов является Android SDK. SDK, по сути, представляет собой ссылку на все функции, которые разработчики могут безопасно использовать в своих приложениях. Эти функции входят в стандартную комплектацию всех вариантов Android, одобренных Google. Однако SDK не является исчерпывающим. Существует немало «скрытых» частей платформы Android, которые не являются частью SDK.

Эти «скрытые» части могут быть невероятно полезны для более хакерских или низкоуровневых вещей. Например, мой Приложение «Ящик виджетов» использует функцию, не относящуюся к SDK, для определения того, когда пользователь запускает приложение из виджета, чтобы ящик мог автоматически закрыться. Вы можете подумать: «Почему бы просто не сделать эти функции, не относящиеся к SDK, частью SDK?» Проблема в том, что их действие не полностью предсказуемо. Google не может гарантировать, что каждая часть платформы работает на каждом одобренном ею устройстве, поэтому для проверки выбираются более важные методы. Google не гарантирует, что остальная часть структуры останется неизменной. Производители могут изменить или полностью удалить эти скрытые функции. Даже в разных версиях AOSP никогда не знаешь, будет ли скрытая функция существовать и работать как раньше.

Если вам интересно, почему я использую слово «скрытый», то это потому, что эти функции даже не являются частью SDK. Обычно, если вы попытаетесь использовать скрытый метод или класс в приложении, оно не сможет скомпилироваться. Их использование требует отражение или модифицированный SDK.

В Android P Google решила, что просто скрыть их недостаточно. Когда была выпущена первая бета-версия, было объявлено, что большинство (не все) скрытых функций больше не были доступны разработчикам приложений. Первая бета-версия предупреждала вас, когда ваше приложение использовало функцию из черного списка. Следующие бета-версии просто сломали ваше приложение. По крайней мере меня этот черный список очень раздражал. Мало того, что он сломал довольно много Навигационные жесты, но поскольку скрытые функции по умолчанию занесены в черный список (Google приходится вручную вносить некоторые из них в белый список для каждой версии), у меня было много проблем с запуском Widget Drawer.

Теперь было несколько способов обойти черный список. Первый вариант заключался в том, чтобы просто оставить ваше приложение ориентированным на API 27 (Android 8.1), поскольку черный список применялся только к приложениям, ориентированным на новейший API. К сожалению, с Google минимальные требования API для публикации в Play Store можно будет ориентироваться на API 27 только до определенного момента. По состоянию на 1 ноября 2019 г., все обновления приложений в Play Store должны быть ориентированы на API 28 или более поздней версии.

Второй обходной путь — это то, что Google встроен в Android. Можно запустить команду ADB, чтобы полностью отключить черный список. Это отлично подходит для личного использования и тестирования, но я могу сказать вам из первых рук, что попытка заставить конечных пользователей настроить и запустить ADB — это кошмар.

Так, где это оставляет нас? Мы больше не можем ориентироваться на API 27, если хотим загрузить в Play Store, а метод ADB просто нежизнеспособен. Однако это не значит, что у нас нет вариантов.

Скрытый черный список API применяется только к пользовательским приложениям, не внесенным в белый список. Системные приложения, приложения, подписанные подписью платформы, а также приложения, указанные в файле конфигурации, исключаются из черного списка. Как ни странно, все пакеты сервисов Google Play указаны в этом файле конфигурации. Я думаю, Google лучше, чем остальные из нас.

В любом случае, давайте продолжим разговор о черном списке. Сегодня нас интересует то, что системные приложения освобождены от налога. Это означает, например, что приложение системного пользовательского интерфейса может использовать все необходимые ему скрытые функции, поскольку оно находится в системном разделе. Очевидно, что мы не можем просто перенести приложение в системный раздел. Для этого нужен root, хороший файловый менеджер, знание разрешений... Было бы проще использовать ADB. Однако это не единственный способ стать системным приложением, по крайней мере, что касается скрытого черного списка API.

Вернитесь к семи абзацам назад, когда я упомянул размышление. Если вы не знаете, что такое рефлексия, советую прочитать эта страница, но вот краткое резюме. В Java отражение — это способ доступа к обычно недоступным классам, методам и полям. Это невероятно мощный инструмент. Как я уже говорил в этом абзаце, раньше отражение было способом доступа к функциям, не относящимся к SDK. Черный список API блокирует использование отражения, но не блокирует использование двойной-отражение.

А вот здесь это становится немного странным. Обычно, если вы хотите вызвать скрытый метод, вы должны сделать что-то вроде этого (это в Kotlin, но в Java аналогично):

val someHiddenClass = Class.forName("android.some.hidden.Class")
val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)

someHiddenMethod.invoke(null, "some important string")

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

val forName = Class::class.java.getMethod("forName", String:: class.java)
val getMethod = Class::class.java.getMethod("getMethod", String:: class.java, arrayOf>()::class.java)
val someHiddenClass = forName.invoke(null, "android.some.hidden.Class") asClass
val someHiddenMethod = getMethod.invoke(someHiddenClass, "someHiddenMethod", String::class.java)

someHiddenMethod.invoke(null, "some important string")

Странно, правда? Ну да, но и нет. Черный список API отслеживает, кто вызывает функцию. Если источник не освобожден, произойдет сбой. В первом примере источником является приложение. Однако во втором примере источником является сама система. Вместо того, чтобы использовать рефлексию, чтобы напрямую получить то, что мы хотим, мы используем ее, чтобы сообщить системе, что она должна получить то, что мы хотим. Поскольку источником вызова скрытой функции является система, черный список нас больше не касается.

Итак, мы закончили. Теперь у нас есть способ обойти черный список API. Это немного неуклюже, но мы могли бы написать функцию-обертку, чтобы нам не приходилось каждый раз выполнять двойное отражение. Это не здорово, но это лучше, чем ничего. Но на самом деле мы еще не закончили. Есть лучший способ сделать это, который позволит нам использовать обычное отражение или модифицированный SDK, как в старые добрые времена.

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

val forName = Class::class.java.getDeclaredMethod("forName", String:: class.java)
val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String:: class.java, arrayOf>()::class.java)

val vmRuntimeClass = forName.invoke(null, "dalvik.system.VMRuntime") asClass
val getRuntime = getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null) as Method
val setHiddenApiExemptions = getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", arrayOf(arrayOf<String>()::class.java)) asMethod

val vmRuntime = getRuntime.invoke(null)

setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))

Хорошо, технически это не означает, что наше приложение исключено из черного списка API. На самом деле есть еще одна команда ADB, которую вы можете запустить, чтобы указать функции, которые не следует заносить в черный список. Это то, чем мы воспользовались выше. Код по сути переопределяет все, что, по мнению системы, исключено для нашего приложения. Передача «L» в конце означает, что все методы исключены. Если вы хотите исключить определенные методы, используйте синтаксис Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Теперь мы фактически закончили. Создайте собственный класс приложения, поместите этот код в onCreate() метод, и бац, никаких больше ограничений.


Спасибо участнику XDA weishu, разработчику VirtualXpose и Taichi, за первое открытие этого метода. Мы также хотели бы поблагодарить признанного разработчика XDA topjohnwu за то, что он указал мне на этот обходной путь. Вот еще немного о том, как это работает, хотя он на китайском. Я также написал об этом на Stack Overflow, а также пример в JNI.