Vývojáři: Je velmi snadné obejít skrytá omezení API systému Android

Android 9 Pie a Android 10 generují varování nebo přímo blokují přístup ke skrytým API. Zde je návod, jak mohou vývojáři obejít skrytá omezení API.

Vraťme se zpět do doby před více než rokem a všichni jsme nadšení z toho, co přijde v beta verzích systému Android P. Uživatelé se těší na nové funkce a vývojáři se těší na některé nové nástroje, které vylepší jejich aplikace. Bohužel pro některé z těchto vývojářů přišla první beta verze Androidu P s trochu nepříjemným překvapením: skrytými omezeními API. Než se ponořím do toho, co to přesně znamená, dovolte mi vysvětlit trochu jeho kontext.

O čem to všechno je?

Vývojáři aplikací pro Android nemusí při vytváření aplikace začínat od nuly. Google poskytuje nástroje, které usnadňují vývoj aplikací a méně se opakují. Jedním z těchto nástrojů je Android SDK. SDK je v podstatě odkazem na všechny funkce, které mohou vývojáři ve svých aplikacích bezpečně používat. Tyto funkce jsou standardní součástí všech variant Androidu, které Google schvaluje. Sada SDK však není vyčerpávající. Existuje poměrně málo „skrytých“ částí rámce Androidu, které nejsou součástí SDK.

Tyto "skryté" části mohou být neuvěřitelně užitečné pro složitější věci nebo věci na nízké úrovni. Například můj Aplikace Widget Drawer využívá funkci bez sady SDK ke zjištění, kdy uživatel spustí aplikaci z widgetu, aby se zásuvka mohla automaticky zavřít. Možná si říkáte: "Proč prostě neudělat tyto funkce, které nejsou součástí sady SDK?" No, problém je v tom, že jejich činnost není plně předvídatelná. Google nemůže zajistit, aby každá jednotlivá část rámce fungovala na každém jednotlivém zařízení, které schválí, takže k ověření jsou vybrány důležitější metody. Google nezaručuje, že zbytek rámce zůstane konzistentní. Výrobci mohou tyto skryté funkce změnit nebo úplně odstranit. Ani v různých verzích AOSP nikdy nevíte, zda skrytá funkce bude stále existovat nebo bude fungovat jako dříve.

Pokud vás zajímá, proč používám slovo „skrytý“, je to proto, že tyto funkce nejsou ani součástí SDK. Normálně, pokud se pokusíte použít skrytou metodu nebo třídu v aplikaci, kompilace se nezdaří. Jejich použití vyžaduje odraz nebo upravenou sadu SDK.

S Androidem P se Google rozhodl, že jen jejich skrytí nestačí. Když byla vydána první beta, bylo to oznámeno většina (ne všechny) skryté funkce již nebyla k dispozici pro použití vývojářům aplikací. První beta by vás varovala, když vaše aplikace používala funkci na černé listině. Další beta verze jednoduše srazily vaši aplikaci. Alespoň pro mě byl tento blacklist pěkně otravný. Nejen, že se to docela zlomilo Navigační gesta, ale protože skryté funkce jsou ve výchozím nastavení na černé listině (Google musí ručně přidat některé na seznam povolených pro jednotlivá vydání), měl jsem velké problémy se zprovozněním zásuvek widgetů.

Nyní existovalo několik způsobů, jak obejít černou listinu. Prvním bylo jednoduše zachovat API pro cílení aplikace 27 (Android 8.1), protože seznam zakázaných položek se týkal pouze aplikací, které cílí na nejnovější rozhraní API. Bohužel s Googlem minimální požadavky na API pro publikování v Obchodě Play by tak dlouho bylo možné cílit pouze na API 27. Od 1. listopadu 2019, všechny aktualizace aplikací v Obchodě Play musí cílit na API 28 nebo novější.

Druhým řešením je ve skutečnosti něco, co Google zabudoval do Androidu. Je možné spustit příkaz ADB pro úplné zakázání černé listiny. To je skvělé pro osobní použití a testování, ale z první ruky vám mohu říci, že snaha přimět koncové uživatele k nastavení a spuštění ADB je noční můra.

Tak kde nás to opouští? Pokud chceme nahrávat do Obchodu Play, nemůžeme už cílit na API 27 a metoda ADB prostě není životaschopná. To však neznamená, že jsme mimo možnosti.

Skrytá černá listina rozhraní API se vztahuje pouze na uživatelské aplikace, které nejsou na seznamu povolených. Systémové aplikace, aplikace podepsané podpisem platformy a aplikace uvedené v konfiguračním souboru jsou z černé listiny vyňaty. Je zajímavé, že v tomto konfiguračním souboru jsou uvedeny všechny sady služeb Google Play. Myslím, že Google je lepší než my ostatní.

Každopádně pojďme dál mluvit o černé listině. Část, která nás dnes zajímá, je, že systémové aplikace jsou vyjmuty. To například znamená, že aplikace System UI může používat všechny skryté funkce, které chce, protože je na systémovém oddílu. Je zřejmé, že nemůžeme jen tak natlačit aplikaci do systémového oddílu. To chce roota, dobrého správce souborů, znalost oprávnění... Bylo by jednodušší použít ADB. To však není jediný způsob, jak můžeme být systémovou aplikací, alespoň co se týče skryté černé listiny API.

Vraťte svou mysl zpět do doby před sedmi odstavci, když jsem zmínil reflexi. Pokud nevíte, co je odraz, doporučuji přečíst tato stránka, ale zde je rychlé shrnutí. V Javě je reflexe způsob, jak získat přístup k běžně nepřístupným třídám, metodám a polím. Je to neuvěřitelně mocný nástroj. Jak jsem řekl v tomto odstavci, reflexe bývala způsobem, jak získat přístup k funkcím, které nejsou SDK. Černá listina API blokuje použití reflexe, ale neblokuje použití dvojnásobek-odraz.

Tady to začíná být trochu divné. Normálně, pokud byste chtěli volat skrytou metodu, udělali byste něco takového (toto je v Kotlinu, ale Java je podobná):

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

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

Díky černé listině API byste však dostali pouze výjimku ClassNotFoundException. Pokud se však zamyslíte dvakrát, funguje to dobře:

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")

Divné, že? Tedy ano, ale také ne. Černá listina API sleduje, kdo volá funkci. Pokud zdroj není vyjmut, zhroutí se. V prvním příkladu je zdrojem aplikace. Ve druhém příkladu je však zdrojem samotný systém. Namísto použití reflexe k přímému získání toho, co chceme, ji používáme k tomu, abychom řekli systému, aby dostal to, co chceme. Vzhledem k tomu, že zdrojem volání skryté funkce je systém, blacklist se nás již netýká.

Takže máme hotovo. Nyní máme způsob, jak obejít blacklist API. Je to trochu neohrabané, ale mohli bychom napsat funkci wrapper, abychom nemuseli pokaždé dvakrát odrážet. Není to skvělé, ale je to lepší než nic. Ale ve skutečnosti jsme ještě neskončili. Existuje lepší způsob, jak toho dosáhnout, který nám umožní použít normální reflexi nebo upravenou sadu SDK, jako za starých dobrých časů.

Vzhledem k tomu, že vynucení na černé listině se vyhodnocuje pro každý proces (což je ve většině případů stejné jako pro jednotlivé aplikace), může systém nějakým způsobem zaznamenat, zda je daná aplikace vyjmuta či nikoli. Naštěstí existuje a je nám dostupný. Pomocí tohoto nově nalezeného hacku s dvojitým odrazem máme blok kódu pro jednorázové použití:

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"))

Dobře, technicky to tedy systému neříká, že naše aplikace je vyňata z černé listiny API. Ve skutečnosti existuje další příkaz ADB, který můžete spustit, abyste určili funkce, které by neměly být na černé listině. To je to, co využíváme výše. Kód v podstatě přepíše vše, co si systém myslí, že je pro naši aplikaci osvobozeno. Předání "L" na konci znamená, že všechny metody jsou vyjmuty. Pokud chcete vyjmout konkrétní metody, použijte syntaxi Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Teď jsme vlastně skončili. Vytvořte vlastní třídu aplikace, vložte tento kód do onCreate() metoda, a bam, žádná další omezení.


Děkujeme XDA Member weishu, vývojáři VirtualXposed a Taichi, za původní objev této metody. Také bychom rádi poděkovali XDA Recognized Developer topjohnwu za to, že mě upozornil na toto řešení. Zde je trochu více o tom, jak to funguje, i když je to v čínštině. Já také napsal o tom na Stack Overflow, s příkladem i v JNI.