Ontwikkelaars: het is super eenvoudig om de verborgen API-beperkingen van Android te omzeilen

Android 9 Pie en Android 10 geven waarschuwingen of blokkeren de toegang tot verborgen API's volledig. Hier leest u hoe ontwikkelaars de verborgen API-beperkingen kunnen omzeilen.

Flashback naar meer dan een jaar geleden, en we zijn allemaal enthousiast over wat er gaat komen in de Android P-bèta's. Gebruikers kijken uit naar nieuwe functies en ontwikkelaars kijken uit naar enkele nieuwe tools om hun apps beter te maken. Helaas voor sommige van deze ontwikkelaars bracht de eerste Android P-bèta een vervelende verrassing met zich mee: verborgen API-beperkingen. Voordat ik inga op wat dat precies betekent, wil ik eerst iets uitleggen over de context ervan.

Waar gaat dit allemaal over?

Ontwikkelaars van Android-apps hoeven niet helemaal opnieuw te beginnen als ze een app maken. Google biedt tools om de ontwikkeling van apps eenvoudiger en minder repetitief te maken. Een van deze tools is de Android SDK. De SDK is in wezen een verwijzing naar alle functies die ontwikkelaars veilig in hun apps kunnen gebruiken. Deze functies zitten standaard op alle varianten van Android die Google goedkeurt. De SDK is echter niet uitputtend. Er zijn nogal wat "verborgen" delen van het Android-framework die geen deel uitmaken van de SDK.

Deze "verborgen" onderdelen kunnen ongelooflijk handig zijn voor meer hacky of low-level dingen. Bijvoorbeeld mijn Widget Drawer-app maakt gebruik van een niet-SDK-functie om te detecteren wanneer een gebruiker een app vanuit een widget start, zodat de lade automatisch kan sluiten. Je denkt misschien: "Waarom maken we deze niet-SDK-functies niet gewoon onderdeel van de SDK?" Het probleem is dat hun werking niet volledig voorspelbaar is. Google kan er niet voor zorgen dat elk afzonderlijk onderdeel van het raamwerk werkt op elk afzonderlijk apparaat dat het goedkeurt, dus worden er belangrijkere methoden geselecteerd om te worden geverifieerd. Google garandeert niet dat de rest van het raamwerk consistent blijft. Fabrikanten kunnen deze verborgen functies wijzigen of volledig verwijderen. Zelfs in verschillende versies van AOSP weet je nooit of een verborgen functie nog steeds zal bestaan ​​of zal werken zoals vroeger.

Als je je afvraagt ​​waarom ik het woord 'verborgen' gebruik, komt dat omdat deze functies niet eens deel uitmaken van de SDK. Normaal gesproken mislukt het compileren als u een verborgen methode of klasse in een app probeert te gebruiken. Het gebruik ervan vereist reflectie of een aangepaste SDK.

Met Android P besloot Google dat alleen het verbergen ervan niet genoeg was. Toen de eerste bèta werd uitgebracht, dat werd aangekondigd de meeste (niet alle) verborgen functies waren niet langer beschikbaar voor app-ontwikkelaars. De eerste bèta waarschuwt u wanneer uw app een functie op de zwarte lijst gebruikt. Bij de volgende bèta's crashte uw app eenvoudigweg. Voor mij was deze zwarte lijst in ieder geval behoorlijk vervelend. Niet alleen is er behoorlijk wat kapot gegaan Navigatiegebaren, maar omdat verborgen functies standaard op de zwarte lijst staan ​​(Google moet sommige per release handmatig op de witte lijst zetten), had ik veel problemen om Widget Drawer werkend te krijgen.

Er waren een paar manieren om de zwarte lijst te omzeilen. De eerste was om uw app eenvoudigweg API 27 (Android 8.1) te laten targeten, omdat de zwarte lijst alleen van toepassing was op apps die de nieuwste API targeten. Helaas, met Google minimale API-vereisten voor publicatie in de Play Store zou het slechts voor een beperkte tijd mogelijk zijn om API 27 te targeten. Vanaf 1 november 2019moeten alle app-updates voor de Play Store zich richten op API 28 of hoger.

De tweede oplossing is eigenlijk iets dat Google in Android heeft ingebouwd. Het is mogelijk om een ​​ADB-opdracht uit te voeren om de zwarte lijst volledig uit te schakelen. Dat is geweldig voor persoonlijk gebruik en testen, maar ik kan je uit de eerste hand vertellen dat het een nachtmerrie is om eindgebruikers ertoe te brengen ADB in te stellen en uit te voeren.

Dus waar blijven we? We kunnen API 27 niet meer targeten als we willen uploaden naar de Play Store, en de ADB-methode is gewoon niet haalbaar. Dat betekent echter niet dat we geen opties meer hebben.

De verborgen API-blacklist is alleen van toepassing op gebruikersapplicaties die niet op de witte lijst staan. Systeemapplicaties, applicaties ondertekend met de platformhandtekening en applicaties gespecificeerd in een configuratiebestand zijn allemaal uitgesloten van de zwarte lijst. Grappig genoeg is de Google Play Services-suite allemaal gespecificeerd in dat configuratiebestand. Ik denk dat Google beter is dan de rest van ons.

Hoe dan ook, laten we het blijven hebben over de zwarte lijst. Het deel waarin we vandaag geïnteresseerd zijn, is dat systeemtoepassingen zijn vrijgesteld. Dat betekent bijvoorbeeld dat de System UI-app alle verborgen functies kan gebruiken die hij wil, aangezien deze zich op de systeempartitie bevindt. Het is duidelijk dat we een app niet zomaar naar de systeempartitie kunnen pushen. Dat heeft root nodig, een goede bestandsbeheerder, kennis van permissies... Het zou gemakkelijker zijn om ADB te gebruiken. Dat is echter niet de enige manier waarop we een systeem-app kunnen zijn, tenminste wat de verborgen API-zwarte lijst betreft.

Denk eens terug aan zeven alinea's geleden, toen ik het had over reflectie. Als je niet weet wat reflectie is, raad ik je aan om te lezen deze pagina, maar hier is een korte samenvatting. In Java is reflectie een manier om toegang te krijgen tot normaal ontoegankelijke klassen, methoden en velden. Het is een ongelooflijk krachtig hulpmiddel. Zoals ik in die paragraaf al zei, was reflectie vroeger een manier om toegang te krijgen tot niet-SDK-functies. De API-blacklist blokkeert het gebruik van reflectie, maar niet het gebruik van dubbele-reflectie.

Nu, hier wordt het een beetje raar. Normaal gesproken zou je, als je een verborgen methode wilt aanroepen, zoiets als dit doen (dit is in Kotlin, maar Java is vergelijkbaar):

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

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

Dankzij de API-blacklist krijg je echter gewoon een ClassNotFoundException. Als je echter twee keer reflecteert, werkt het prima:

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

Raar toch? Nou ja, maar ook nee. De API-blacklist houdt bij wie een functie aanroept. Als de bron niet is vrijgesteld, crasht deze. In het eerste voorbeeld is de bron de app. In het tweede voorbeeld is de bron echter het systeem zelf. In plaats van reflectie te gebruiken om direct te krijgen wat we willen, gebruiken we het om het systeem te vertellen dat we moeten krijgen wat we willen. Omdat de bron van de aanroep naar de verborgen functie het systeem is, heeft de zwarte lijst geen invloed meer op ons.

Dus we zijn klaar. We hebben nu een manier om de API-zwarte lijst te omzeilen. Het is een beetje onhandig, maar we zouden een wrapper-functie kunnen schrijven, zodat we niet elke keer dubbel hoeven te reflecteren. Het is niet geweldig, maar het is beter dan niets. Maar eigenlijk zijn we nog niet klaar. Er is een betere manier om dit te doen, waarbij we normale reflectie of een aangepaste SDK kunnen gebruiken, zoals vroeger.

Omdat de handhaving van de zwarte lijst per proces wordt geëvalueerd (wat in de meeste gevallen hetzelfde is als per app), kan het systeem mogelijk op een of andere manier registreren of de betreffende app is vrijgesteld of niet. Gelukkig is dat zo en is het voor ons toegankelijk. Met behulp van die nieuwe dubbele-reflectie-hack hebben we een codeblok voor eenmalig gebruik:

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

Oké, technisch gezien vertelt dit het systeem niet dat onze app is vrijgesteld van de API-zwarte lijst. Er is eigenlijk nog een ADB-opdracht die u kunt uitvoeren om functies op te geven die niet op de zwarte lijst mogen staan. Daar profiteren we hierboven van. De code overschrijft feitelijk alles wat volgens het systeem vrijgesteld is voor onze app. Als u aan het einde de "L" passeert, betekent dit dat alle methoden zijn vrijgesteld. Als u specifieke methoden wilt uitsluiten, gebruikt u de Smali-syntaxis: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Nu zijn we eigenlijk klaar. Maak een aangepaste Application-klasse, plaats die code in de onCreate() methode, en bam, geen beperkingen meer.


Met dank aan XDA Member weishu, de ontwikkelaar van VirtualXposed en Taichi, voor het oorspronkelijk ontdekken van deze methode. We willen ook XDA Recognized Developer topjohnwu bedanken voor het wijzen op deze oplossing voor mij. Hier is wat meer over hoe het werkt, ook al is het in het Chinees. ik ook schreef hierover op Stack Overflow, met ook een voorbeeld in JNI.