Розробники: надзвичайно легко обійти приховані обмеження API Android

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

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

Що це все?

Розробникам програм для Android не потрібно починати з нуля, коли вони створюють програму. Google надає інструменти, щоб зробити розробку додатків легшою та менш повторюваною. Одним із таких інструментів є Android SDK. SDK, по суті, є посиланням на всі функції, які розробники можуть безпечно використовувати у своїх програмах. Ці функції є стандартними для всіх варіантів Android, схвалених Google. Однак SDK не є вичерпним. Існує чимало «прихованих» частин інфраструктури Android, які не є частиною SDK.

Ці «приховані» частини можуть бути неймовірно корисними для більш хакерських або низькорівневих речей. Наприклад, мій Додаток Widget Drawer використовує функцію, не пов’язану з SDK, щоб визначити, коли користувач запускає програму з віджета, щоб ящик міг автоматично закритися. Ви можете подумати: «Чому б просто не зробити ці функції, що не належать до SDK, частиною SDK?» Ну, проблема в тому, що їхня робота не є повністю передбачуваною. Google не може переконатися, що кожна окрема частина фреймворку працює на кожному схваленому пристрої, тому для перевірки вибираються важливіші методи. Google не гарантує, що решта фреймворку залишатиметься узгодженою. Виробники можуть змінити або повністю видалити ці приховані функції. Навіть у різних версіях AOSP ви ніколи не знаєте, чи існуватиме прихована функція чи працюватиме так, як раніше.

Якщо вам цікаво, чому я використовую слово «прихований», це тому, що ці функції навіть не є частиною SDK. Зазвичай, якщо ви спробуєте використати прихований метод або клас у програмі, її не вдасться скомпілювати. Їх використання вимагає рефлексія або модифікований SDK.

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

Тепер було кілька способів обійти чорний список. Перше полягало в тому, щоб ваш додаток був націлений на API 27 (Android 8.1), оскільки чорний список застосовувався лише до програм, націлених на останній API. На жаль, з Google мінімальні вимоги до API для публікації в магазині Play було б можливим націлюватися лише на API 27 так довго. Станом на 01.11.2019р, усі оновлення програми для Play Store мають бути націлені на API 28 або новішої версії.

Другий обхідний шлях — це те, що Google вбудовано в Android. Можна запустити команду ADB, щоб повністю вимкнути чорний список. Це чудово для особистого користування та тестування, але я можу вам сказати з перших вуст, що намагатися змусити кінцевих користувачів налаштувати та запустити ADB — це кошмар.

Отже, що це залишило нас? Ми більше не можемо націлюватися на API 27, якщо хочемо завантажити в Play Store, а метод ADB просто нежиттєздатний. Однак це не означає, що у нас немає варіантів.

Прихований чорний список API стосується лише додатків користувачів, які не внесені до білого списку. Системні додатки, додатки, підписані підписом платформи, і додатки, зазначені у конфігураційному файлі, виключені з чорного списку. Досить дивно, що набір служб Google Play указано в цьому файлі конфігурації. Я вважаю, що Google кращий за всіх нас.

У будь-якому випадку, давайте продовжимо говорити про чорний список. Нас сьогодні цікавить те, що системні програми звільнені. Це означає, наприклад, що програма System UI може використовувати всі приховані функції, оскільки вона знаходиться в системному розділі. Очевидно, ми не можемо просто розмістити програму в системному розділі. Для цього потрібен 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;.

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


Дякуємо учаснику XDA weishu, розробнику VirtualXposed і Taichi, за відкриття цього методу. Ми також хотіли б подякувати визнаному розробнику XDA topjohnwu за те, що він вказав мені на це обхідне рішення. Ось трохи більше про те, як це працює, хоча це китайською мовою. я також написав про це на Stack Overflow, із прикладом також у JNI.