Razvijalci: zelo preprosto je zaobiti skrite omejitve API-ja za Android

Android 9 Pie in Android 10 izdajata opozorila ali popolnoma blokirata dostop do skritih API-jev. Tukaj je opisano, kako lahko razvijalci zaobidejo skrite omejitve API-ja.

Spomin na več kot leto dni nazaj in vsi smo navdušeni nad tem, kaj nas čaka v različicah Android P beta. Uporabniki se veselijo novih funkcij, razvijalci pa se veselijo novih orodij za izboljšanje svojih aplikacij. Na žalost za nekatere od teh razvijalcev je prva različica Android P beta prišla z grdim presenečenjem: skrite omejitve API-ja. Preden se poglobim v to, kaj točno to pomeni, naj pojasnim nekaj o njegovem kontekstu.

Kaj je to?

Razvijalcem aplikacij za Android ni treba začeti iz nič, ko izdelajo aplikacijo. Google ponuja orodja za lažji razvoj aplikacij in manj ponavljanja. Eno od teh orodij je Android SDK. SDK je v bistvu sklicevanje na vse funkcije, ki jih razvijalci lahko varno uporabljajo v svojih aplikacijah. Te funkcije so standardne za vse različice Androida, ki jih Google odobri. SDK pa ni izčrpen. Obstaja kar nekaj "skritih" delov Androidovega ogrodja, ki niso del SDK-ja.

Ti "skriti" deli so lahko neverjetno uporabni za bolj hakerske ali nizkonivojske stvari. Na primer, moj Aplikacija Widget Drawer uporablja funkcijo, ki ni SDK, da zazna, kdaj uporabnik zažene aplikacijo iz gradnika, tako da se predal lahko samodejno zapre. Morda razmišljate: "Zakaj ne bi te funkcije, ki niso SDK, preprosto vključili v SDK?" No, težava je v tem, da njihovo delovanje ni povsem predvidljivo. Google ne more zagotoviti, da vsak posamezen del ogrodja deluje na vsaki napravi, ki jo odobri, zato so za preverjanje izbrane pomembnejše metode. Google ne jamči, da bo preostali del ogrodja ostal dosleden. Proizvajalci lahko spremenijo ali popolnoma odstranijo te skrite funkcije. Tudi v različnih različicah AOSP nikoli ne veste, ali bo skrita funkcija še obstajala ali bo delovala tako, kot je nekoč.

Če se sprašujete, zakaj sem uporabil besedo "skrito", je to zato, ker te funkcije sploh niso del SDK-ja. Če poskusite uporabiti skrito metodo ali razred v aplikaciji, običajno ne bo uspelo prevesti. Njihova uporaba zahteva refleksija ali spremenjen SDK.

Z Androidom P se je Google odločil, da samo skrivanje ni dovolj. Ko je bila izdana prva različica beta, sporočili, da večina (ne vse) skritih funkcij ni bila več na voljo za uporabo razvijalcem aplikacij. Prva različica beta bi vas opozorila, ko bi vaša aplikacija uporabljala funkcijo na črnem seznamu. Naslednje različice beta so preprosto zrušile vašo aplikacijo. Vsaj zame je bil ta črni seznam precej moteč. Ne samo, da se je precej zlomil Navigacijske kretnje, a ker so skrite funkcije privzeto uvrščene na črni seznam (Google mora nekatere izdaje ročno uvrstiti na seznam dovoljenih), sem imel veliko težav s tem, da bi Widget Drawer deloval.

Zdaj je bilo nekaj načinov za obhod črnega seznama. Prvi je bil, da preprosto ohranite svojo aplikacijo, ki cilja na API 27 (Android 8.1), saj se črni seznam uporablja samo za aplikacije, ki ciljajo na najnovejši API. Na žalost z Googlom minimalne zahteve za API za objavo v Trgovini Play bi bilo tako dolgo mogoče ciljati le na API 27. Od 1. novembra 2019, morajo vse posodobitve aplikacij za Trgovino Play ciljati na API 28 ali novejši.

Druga rešitev je pravzaprav nekaj, kar je Google vgradil v Android. Možno je zagnati ukaz ADB, da popolnoma onemogočite črni seznam. To je super za osebno uporabo in testiranje, vendar vam lahko iz prve roke povem, da je poskus, da bi končni uporabniki vzpostavili in zagnali ADB, nočna mora.

Torej, kje nas to pusti? Ne moremo več ciljati na API 27, če želimo nalagati v Trgovino Play, in metoda ADB preprosto ni izvedljiva. To pa ne pomeni, da smo brez možnosti.

Skriti črni seznam API-jev velja samo za uporabniške aplikacije, ki niso na seznamu dovoljenih. Sistemske aplikacije, aplikacije, podpisane s podpisom platforme, in aplikacije, navedene v konfiguracijski datoteki, so vse izvzete iz črnega seznama. Smešno je, da so vsi paketi storitev Google Play navedeni v tej konfiguracijski datoteki. Mislim, da je Google boljši od nas ostalih.

Kakorkoli, nadaljujmo s črnim seznamom. Del, ki nas danes zanima, je, da so sistemske aplikacije izvzete. To na primer pomeni, da lahko aplikacija sistemskega uporabniškega vmesnika uporablja vse skrite funkcije, ki jih želi, saj je na sistemski particiji. Očitno je, da aplikacije ne moremo preprosto potisniti na sistemsko particijo. To potrebuje root, dober upravitelj datotek, poznavanje dovoljenj... Lažje bi bilo uporabiti ADB. Vendar to ni edini način, da smo lahko sistemska aplikacija, vsaj kar zadeva skriti črni seznam API-jev.

Vrnite se nazaj na sedem odstavkov nazaj, ko sem omenil refleksijo. Če ne veste, kaj je refleksija, priporočam branje tej strani, ampak tukaj je kratek povzetek. V Javi je refleksija način za dostop do običajno nedostopnih razredov, metod in polj. To je neverjetno močno orodje. Kot sem rekel v tem odstavku, je bila refleksija včasih način za dostop do funkcij, ki niso SDK. Črni seznam API-ja blokira uporabo refleksije, ne pa tudi uporabe dvojno-odsev.

Tukaj pa postane malce čudno. Običajno bi, če bi želeli poklicati skrito metodo, naredili nekaj takega (to je v Kotlinu, vendar je Java podobna):

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

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

Zahvaljujoč črni listi API-ja pa bi dobili samo ClassNotFoundException. Vendar, če razmišljate dvakrat, deluje dobro:

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

Čudno kajne? No ja, ampak tudi ne. Črni seznam API-ja spremlja, kdo kliče funkcijo. Če vir ni izvzet, se zruši. V prvem primeru je vir aplikacija. Vendar pa je v drugem primeru vir sam sistem. Namesto da bi refleksijo uporabili, da neposredno dobimo, kar želimo, jo uporabljamo, da sistemu povemo, naj dobi, kar želimo. Ker je vir klica skrite funkcije sistem, črni seznam na nas ne vpliva več.

Tako smo končali. Zdaj imamo način, kako zaobiti črni seznam API-jev. Malo je okorno, vendar bi lahko napisali ovojno funkcijo, da nam ni treba vsakič dvojno odražati. Ni super, a je bolje kot nič. A pravzaprav še nismo končali. Obstaja boljši način za to, ki nam bo omogočil uporabo normalne refleksije ali spremenjenega SDK-ja, kot v dobrih starih časih.

Ker se uveljavljanje črnega seznama ocenjuje za vsak proces (kar je v večini primerov enako kot za posamezno aplikacijo), lahko sistem na nek način zabeleži, ali je zadevna aplikacija izvzeta ali ne. Na srečo obstaja in nam je dostopen. Z uporabo tega novoodkritega vdora dvojnega odseva imamo blok kode za enkratno uporabo:

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

V redu, tehnično gledano to sistemu ne pove, da je naša aplikacija izvzeta iz črnega seznama API-ja. Pravzaprav obstaja še en ukaz ADB, ki ga lahko zaženete, da določite funkcije, ki ne bi smele biti na črnem seznamu. To je tisto, kar izkoriščamo zgoraj. Koda v bistvu preglasi vse, kar sistem meni, da je izvzeto za našo aplikacijo. Prenos "L" na koncu pomeni, da so vse metode izvzete. Če želite izvzeti določene metode, uporabite sintakso Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Zdaj smo pravzaprav končali. Ustvarite razred aplikacije po meri, to kodo vstavite v onCreate() metoda in bam, ni več omejitev.


Hvala članu XDA weishu, razvijalcu VirtualXposed in Taichi, za prvotno odkritje te metode. Prav tako bi se radi zahvalili priznanemu razvijalcu XDA topjohnwu, da me je opozoril na to rešitev. Tukaj je nekaj več o tem, kako deluje, čeprav je v kitajščini. tudi jaz o tem pisal na Stack Overflow, s primerom tudi v JNI.