Android 9 Pie i Android 10 wyświetlają ostrzeżenia lub całkowicie blokują dostęp do ukrytych interfejsów API. Oto, jak programiści mogą obejść ukryte ograniczenia API.
Wracamy myślami do wydarzeń sprzed ponad roku i wszyscy jesteśmy podekscytowani możliwością zobaczenia, co przyniesie wersja beta Androida P. Użytkownicy nie mogą się doczekać nowych funkcji, a programiści nie mogą się doczekać nowych narzędzi, dzięki którym ich aplikacje będą lepsze. Na nieszczęście dla niektórych z tych programistów pierwsza wersja beta Androida P niosła ze sobą małą niespodziankę: ukryte ograniczenia API. Zanim zagłębię się w to, co to dokładnie oznacza, pozwólcie, że wyjaśnię trochę jego kontekst.
O co w tym wszystkim chodzi?
Twórcy aplikacji na Androida nie muszą zaczynać od zera, tworząc aplikację. Google udostępnia narzędzia, dzięki którym tworzenie aplikacji jest łatwiejsze i mniej powtarzalne. Jednym z takich narzędzi jest zestaw SDK systemu Android. SDK jest zasadniczo odniesieniem do wszystkich funkcji, z których programiści mogą bezpiecznie korzystać w swoich aplikacjach. Funkcje te są standardem we wszystkich wersjach Androida zatwierdzonych przez Google. Zestaw SDK nie jest jednak wyczerpujący. Istnieje sporo „ukrytych” części środowiska Androida, które nie są częścią pakietu SDK.
Te „ukryte” części mogą być niezwykle przydatne w przypadku bardziej hackerskich lub niskopoziomowych rzeczy. Na przykład mój Aplikacja szuflady widżetów wykorzystuje funkcję inną niż SDK do wykrywania, kiedy użytkownik uruchamia aplikację z widżetu, dzięki czemu szuflada może zostać automatycznie zamknięta. Możesz pomyśleć: „Dlaczego po prostu nie włączyć funkcji innych niż SDK do zestawu SDK?” Problem w tym, że ich działanie nie jest do końca przewidywalne. Google nie może upewnić się, że każda część platformy działa na każdym zatwierdzonym przez nią urządzeniu, dlatego do weryfikacji wybierane są ważniejsze metody. Google nie gwarantuje, że pozostała część struktury pozostanie spójna. Producenci mogą zmienić lub całkowicie usunąć te ukryte funkcje. Nawet w różnych wersjach AOSP nigdy nie wiadomo, czy ukryta funkcja będzie nadal istnieć i czy będzie działać tak, jak kiedyś.
Jeśli zastanawiasz się, dlaczego użyłem słowa „ukryty”, dzieje się tak dlatego, że te funkcje nie są nawet częścią zestawu SDK. Zwykle, jeśli spróbujesz użyć ukrytej metody lub klasy w aplikacji, nie uda się jej skompilować. Korzystanie z nich wymaga odbicie lub zmodyfikowany SDK.
W przypadku Androida P Google zdecydował, że samo ich ukrycie nie wystarczy. Kiedy wypuszczono pierwszą wersję beta, ogłoszono, że większość (nie wszystkie) ukrytych funkcji nie była już dostępna dla twórców aplikacji. Pierwsza wersja beta ostrzegała Cię, gdy Twoja aplikacja korzystała z funkcji znajdującej się na czarnej liście. Kolejne wersje beta po prostu spowodowały awarię aplikacji. Przynajmniej dla mnie ta czarna lista była dość denerwująca. Nie tylko trochę się popsuło Gesty nawigacji, ale ponieważ ukryte funkcje są domyślnie umieszczone na czarnej liście (Google musi ręcznie dodać do białej listy niektóre wersje dla poszczególnych wersji), miałem wiele problemów z uruchomieniem Widget Drawer.
Było kilka sposobów obejścia czarnej listy. Pierwszym było po prostu pozostawienie aplikacji kierowanej na interfejs API 27 (Android 8.1), ponieważ czarna lista dotyczyła tylko aplikacji obsługujących najnowszy interfejs API. Niestety, z Google minimalne wymagania API w przypadku publikowania w Sklepie Play celowanie w API 27 byłoby możliwe tylko przez tak długi czas. Od 1 listopada 2019 r, wszystkie aktualizacje aplikacji w Sklepie Play muszą być przeznaczone dla interfejsu API 28 lub nowszego.
Drugie obejście to tak naprawdę coś, co Google wbudowało w Androida. Możliwe jest uruchomienie polecenia ADB, aby całkowicie wyłączyć czarną listę. Świetnie nadaje się do użytku osobistego i testowania, ale mogę powiedzieć z pierwszej ręki, że próby nakłonienia użytkowników końcowych do skonfigurowania i uruchomienia ADB to koszmar.
Więc dokąd nas to prowadzi? Nie możemy już celować w API 27, jeśli chcemy przesłać plik do Sklepu Play, a metoda ADB po prostu nie jest wykonalna. Nie oznacza to jednak, że nie mamy już możliwości.
Ukryta czarna lista API dotyczy tylko aplikacji użytkownika, które nie znajdują się na białej liście. Aplikacje systemowe, aplikacje podpisane sygnaturą platformy oraz aplikacje określone w pliku konfiguracyjnym są wyłączone z czarnej listy. Co zabawne, wszystkie usługi Google Play są określone w tym pliku konfiguracyjnym. Myślę, że Google jest lepszy niż reszta z nas.
W każdym razie porozmawiajmy dalej o czarnej liście. Część, która nas dzisiaj interesuje, to to, że aplikacje systemowe są zwolnione. Oznacza to na przykład, że aplikacja System UI może korzystać ze wszystkich potrzebnych ukrytych funkcji, ponieważ znajduje się na partycji systemowej. Oczywiście nie możemy po prostu wypchnąć aplikacji na partycję systemową. To wymaga roota, dobrego menedżera plików, znajomości uprawnień... Łatwiej byłoby użyć ADB. Jednak nie tylko w ten sposób możemy być aplikacją systemową, przynajmniej jeśli chodzi o ukrytą czarną listę API.
Wróć myślami do siedmiu akapitów temu, kiedy wspomniałem o refleksji. Jeśli nie wiesz, czym jest refleksja, polecam przeczytać ta strona, ale oto krótkie podsumowanie. W Javie odbicie to sposób na uzyskanie dostępu do normalnie niedostępnych klas, metod i pól. To niezwykle potężne narzędzie. Jak powiedziałem w tym akapicie, odbicie było sposobem na dostęp do funkcji innych niż SDK. Czarna lista API blokuje użycie odbicia, ale nie blokuje użycia podwójnie-odbicie.
I tutaj robi się trochę dziwnie. Zwykle, jeśli chcesz wywołać ukrytą metodę, zrobiłbyś coś takiego (to jest w Kotlinie, ale Java jest podobna):
val someHiddenClass = Class.forName("android.some.hidden.Class")
val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)
someHiddenMethod.invoke(null, "some important string")
Jednak dzięki czarnej liście API otrzymasz po prostu wyjątek ClassNotFoundException. Jeśli jednak zastanowisz się dwa razy, działa dobrze:
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")
Dziwne, prawda? No tak, ale też nie. Czarna lista API śledzi, kto wywołuje funkcję. Jeśli źródło nie jest zwolnione, następuje awaria. W pierwszym przykładzie źródłem jest aplikacja. Jednak w drugim przykładzie źródłem jest sam system. Zamiast używać refleksji, aby bezpośrednio uzyskać to, czego chcemy, używamy jej, aby poinformować system, aby otrzymał to, czego chcemy. Ponieważ źródłem wywołania ukrytej funkcji jest system, czarna lista już nas nie dotyczy.
Więc skończyliśmy. Mamy teraz sposób na ominięcie czarnej listy API. To trochę niezgrabne, ale moglibyśmy napisać funkcję opakowującą, więc nie musielibyśmy za każdym razem powtarzać. Nie jest to nic wielkiego, ale lepsze to niż nic. Ale właściwie to jeszcze nie koniec. Jest na to lepszy sposób, który pozwoli nam użyć normalnego odbicia lub zmodyfikowanego pakietu SDK, jak za starych dobrych czasów.
Ponieważ egzekwowanie czarnej listy jest oceniane dla każdego procesu (co w większości przypadków jest takie samo jak dla każdej aplikacji), może istnieć jakiś sposób, aby system zarejestrował, czy dana aplikacja jest zwolniona, czy nie. Na szczęście istnieje i jest dla nas dostępny. Korzystając z nowo odkrytego hacka z podwójną refleksją, mamy jednorazowy blok kodu:
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)) asMethodval vmRuntime = getRuntime.invoke(null)
setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))
OK, technicznie rzecz biorąc, nie oznacza to, że nasza aplikacja jest wyłączona z czarnej listy API. W rzeczywistości istnieje inne polecenie ADB, które można uruchomić, aby określić funkcje, które nie powinny być umieszczane na czarnej liście. Właśnie z tego korzystamy powyżej. Kod zasadniczo zastępuje wszystko, co system uzna za wyłączone w przypadku naszej aplikacji. Przekazanie „L” na końcu oznacza, że wszystkie metody są wyłączone. Jeśli chcesz wykluczyć określone metody, użyj składni Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;
.
Teraz właściwie już skończyliśmy. Utwórz niestandardową klasę aplikacji i umieść ten kod w pliku onCreate()
metoda i bam, żadnych więcej ograniczeń.
Dziękujemy członkowi XDA weishu, twórcy VirtualXposed i Taichi, za pierwotne odkrycie tej metody. Chcielibyśmy również podziękować uznanemu programiście XDA topjohnwu za wskazanie mi tego obejścia. Tutaj trochę więcej o tym jak to działa, chociaż jest po chińsku. ja również napisałem o tym na Stack Overflow, z przykładem również w JNI.