Fejlesztők: Az Android rejtett API-korlátozásait rendkívül egyszerű megkerülni

Az Android 9 Pie és az Android 10 figyelmeztetéseket küld, vagy egyenesen blokkolja a rejtett API-khoz való hozzáférést. Így kerülhetik meg a fejlesztők a rejtett API-korlátozásokat.

Visszatekintés több mint egy évvel ezelőttre, és mindannyian izgatottan várjuk, hogy mi várható az Android P bétaverziójában. A felhasználók már alig várják az új funkciókat, a fejlesztők pedig néhány új eszközt várnak alkalmazásaik jobbá tételére. Sajnos néhány fejlesztő számára az első Android P béta egy csúnya meglepetéssel érkezett: rejtett API-korlátozások. Mielőtt belemerülnék abba, hogy ez pontosan mit is jelent, hadd magyarázzam el egy kicsit a kontextusát.

Miről szól ez az egész?

Az Android-alkalmazások fejlesztőinek nem kell a nulláról kezdeniük, amikor alkalmazásokat készítenek. A Google eszközöket biztosít az alkalmazásfejlesztés egyszerűbbé és kevésbé ismétlődővé tételéhez. Az egyik ilyen eszköz az Android SDK. Az SDK lényegében hivatkozás az összes olyan funkcióra, amelyet a fejlesztők biztonságosan használhatnak alkalmazásaikban. Ezek a funkciók az Android minden Google által jóváhagyott változatán alapfelszereltségnek számítanak. Az SDK azonban nem teljes. Az Android keretrendszerének jó néhány „rejtett” része van, amely nem része az SDK-nak.

Ezek a "rejtett" részek hihetetlenül hasznosak lehetnek a durvább vagy alacsony szintű dolgokhoz. Például az enyém Widget Drawer alkalmazás egy nem SDK funkciót használ annak észlelésére, ha a felhasználó elindít egy alkalmazást egy widgetből, így a fiók automatikusan bezáródhat. Lehet, hogy azt gondolja: "Miért nem teszik ezeket a nem SDK-függvényeket az SDK részévé?" Nos, az a probléma, hogy működésük nem teljesen kiszámítható. A Google nem tudja megbizonyosodni arról, hogy a keretrendszer minden egyes része minden általa jóváhagyott eszközön működik, ezért fontosabb módszereket választanak ki az ellenőrzésre. A Google nem garantálja, hogy a keret többi része egységes marad. A gyártók módosíthatják vagy teljesen eltávolíthatják ezeket a rejtett funkciókat. Még az AOSP különböző verzióiban sem tudhatod, hogy egy rejtett funkció továbbra is létezik-e, vagy a régi módon működik-e.

Ha kíváncsi arra, hogy miért használtam a „rejtett” szót, az azért van, mert ezek a funkciók nem is részei az SDK-nak. Általában, ha egy rejtett metódust vagy osztályt próbál meg használni egy alkalmazásban, akkor az nem sikerül lefordítani. Használatuk megköveteli visszaverődés vagy egy módosított SDK.

Az Android P esetében a Google úgy döntött, hogy nem elég elrejteni őket. Amikor megjelent az első béta, azt jelentették be a legtöbb (nem minden) rejtett funkció már nem volt elérhető az alkalmazásfejlesztők számára. Az első béta figyelmezteti Önt, ha az alkalmazás feketelistán szereplő funkciót használt. A következő béták egyszerűen összeomlották az alkalmazást. Legalábbis számomra elég bosszantó volt ez a feketelista. Nem csak, hogy eltört egy kicsit Navigációs gesztusok, de mivel a rejtett funkciók alapértelmezés szerint feketelistán vannak (a Google-nak manuálisan kell engedélyezőlistára tennie néhányat kiadásonként), sok gondom volt a Widget Drawer működésbe hozásával.

Nos, volt néhány módszer a feketelista megkerülésére. Az első az volt, hogy az alkalmazást egyszerűen meg kell tartani az API 27 (Android 8.1) célzásával, mivel a feketelista csak a legújabb API-t célzó alkalmazásokra vonatkozik. Sajnos a Google-val minimális API követelmények a Play Áruházban való közzétételhez csak az API 27-et lehetne megcélozni ennyi ideig. 2019. november 1-től, a Play Áruház minden alkalmazásfrissítésének az API 28-as vagy újabb verzióját kell céloznia.

A második megoldás valójában valami, amit a Google beépített az Androidba. Lehetőség van egy ADB parancs futtatására a feketelista teljes letiltásához. Ez nagyszerű személyes használatra és tesztelésre, de első kézből elmondhatom, hogy a végfelhasználókat rávenni az ADB beállítására és futtatására egy rémálom.

Akkor ez hol hagy minket? Az API 27-et már nem célozhatjuk meg, ha a Play Áruházba szeretnénk feltölteni, és az ADB módszer egyszerűen nem életképes. Ez azonban nem jelenti azt, hogy kifogytunk a lehetőségekből.

A rejtett API feketelista csak a nem engedélyezett felhasználói alkalmazásokra vonatkozik. A rendszeralkalmazások, a platform aláírásával aláírt alkalmazások és a konfigurációs fájlban megadott alkalmazások mind mentesülnek a feketelistáról. Érdekes módon a Google Play-szolgáltatások mindegyike meg van adva ebben a konfigurációs fájlban. Azt hiszem, a Google jobb, mint mi.

Na mindegy, beszéljünk tovább a feketelistáról. Ma az érdekel minket, hogy a rendszeralkalmazások mentesek. Ez például azt jelenti, hogy a System UI alkalmazás az összes rejtett funkciót használhatja, mivel a rendszerpartíción van. Nyilvánvaló, hogy egy alkalmazást nem csak a rendszerpartícióra tolhatunk. Ehhez root kell, jó fájlkezelő, engedélyek ismerete... Egyszerűbb lenne az ADB használata. Nem csak így lehetünk rendszeralkalmazások, legalábbis ami a rejtett API feketelistát illeti.

Tekintse vissza gondolatait hét bekezdésre, amikor a reflexiót említettem. Ha nem tudod, mi az a tükröződés, ajánlom elolvasásra ez az oldal, de itt egy gyors összefoglaló. A Java-ban a tükrözés az általában nem elérhető osztályok, metódusok és mezők elérésének módja. Ez egy hihetetlenül erős eszköz. Ahogy abban a bekezdésben mondtam, a reflektálás korábban a nem SDK-funkciók elérésének módja volt. Az API feketelista blokkolja a tükrözés használatát, de nem blokkolja a használatát kettős-visszaverődés.

Nos, itt lesz egy kicsit furcsa. Általában, ha rejtett metódust szeretne meghívni, akkor valami ilyesmit kell tennie (ez a Kotlinban van, de a Java hasonló):

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

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

Az API feketelistájának köszönhetően azonban csak egy ClassNotFoundException kivételt kap. Ha azonban kétszer reflektál, akkor jól működik:

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

Furcsa igaz? Hát igen, de nem is. Az API feketelista nyomon követi, hogy ki hív meg egy függvényt. Ha a forrás nem mentesül, akkor összeomlik. Az első példában a forrás az alkalmazás. A második példában azonban a forrás maga a rendszer. Ahelyett, hogy a reflexiót arra használnánk, hogy közvetlenül megkapjuk, amit akarunk, hanem arra használjuk, hogy megmondjuk a rendszernek, hogy szerezze meg, amit akarunk. Mivel a rejtett függvény hívásának forrása a rendszer, a feketelista már nem érint minket.

Szóval végeztünk. Most módunkban áll megkerülni az API feketelistáját. Kicsit macerás, de írhatnánk egy wrapper függvényt, hogy ne kelljen minden alkalommal duplán tükrözni. Nem nagyszerű, de a semminél jobb. De valójában még nem végeztünk. Van erre egy jobb módszer is, amely lehetővé teszi, hogy normál tükrözést vagy módosított SDK-t használjunk, mint a régi szép időkben.

Mivel a feketelista végrehajtását folyamatonként értékelik (ami a legtöbb esetben ugyanaz, mint az alkalmazásonként), előfordulhat, hogy a rendszer rögzíti, hogy a kérdéses alkalmazás mentes-e vagy sem. Szerencsére van, és elérhető számunkra. Az újonnan talált kettős tükröződésű hack segítségével egy egyszer használatos kódblokkot kaptunk:

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

Rendben, technikailag ez nem azt jelenti a rendszernek, hogy alkalmazásunk mentesül az API feketelistájáról. Valójában van egy másik ADB-parancs, amelyet futtathat, hogy meghatározza azokat a függvényeket, amelyeket nem szabad feketelistára tenni. Ezt használjuk ki a fentiekben. A kód alapvetően felülír mindent, amit a rendszer mentesít az alkalmazásunktól. Ha a végén az „L” jelet adja át, akkor az összes módszer mentességet jelent. Ha bizonyos módszereket szeretne mentesíteni, használja a Smali szintaxist: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Most már tényleg végeztünk. Hozzon létre egy egyéni alkalmazásosztályt, és tegye be a kódot a onCreate() módszer, és bam, nincs több korlátozás.


Köszönet az XDA Member weishunak, a VirtualXposed és a Taichi fejlesztőjének, hogy eredetileg felfedezte ezt a módszert. Ezúton is szeretnénk köszönetet mondani topjohnwu XDA Recognized Developernek, hogy rámutatott erre a megoldásra. Itt van egy kicsit bővebben a működéséről, bár kínaiul van. én is írt erről a Stack Overflow-n, példával a JNI-ben is.