一种绕过Android P对非SDK接口限制的简单方法
眾所周知,Android P 引入了針對非 SDK 接口(俗稱為隱藏API)的使用限制。這是繼 Android N上針對 NDK 中私有庫的鏈接限制之后的又一次重大調整。從今以后,不論是native層的NDK還是 Java層的SDK,我們只能使用Google提供的、公開的標準接口。這對開發者以及用戶乃至整個Android生態,當然是一件好事。但這也同時意味著Android上的各種黑科技有可能會逐漸走向消亡。
作為一個有追求的開發者,我們既要尊重并遵守規則,也要有能力在必要的時候突破規則的束縛,帶著鐐銬跳舞。恰好最近有人反饋 VirtualXposed 在 Android P上無法運行,那么今天就來探討一下,如何突破Android P上針對非SDK接口調用的限制。
系統是如何實現這個限制的?
知己知彼,百戰不殆。既然我們想要突破這個限制,自然先得弄清楚,系統是如何給我們施加這個限制的。
?
文檔 中說,通過反射或者JNI訪問非公開接口時會觸發警告/異常等,那么不妨跟蹤一下反射的流程,看看系統到底在哪一步做的限制(以下的源碼分析大可以走馬觀花的看一下,需要的時候自己再仔細看)。我們從 java.lang.Class.getDeclaredMethod(String) 看起,這個方法在Java層最終調用到了 getDeclaredMethodInternal 這個native方法,看一下這個方法的源碼:
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis,jstring name, jobjectArray args) {ScopedFastNativeObjectAccess soa(env);StackHandleScope<1> hs(soa.Self());DCHECK_EQ(Runtime::Current()->GetClassLinker()->GetImagePointerSize(), kRuntimePointerSize);DCHECK(!Runtime::Current()->IsActiveTransaction());Handle<mirror::Method> result = hs.NewHandle(mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize, false>(soa.Self(),DecodeClass(soa, javaThis),soa.Decode<mirror::String>(name),soa.Decode<mirror::ObjectArray<mirror::Class>>(args)));if (result == nullptr || ShouldBlockAccessToMember(result->GetArtMethod(), soa.Self())) {return nullptr;}return soa.AddLocalReference<jobject>(result.Get()); } |
?
注意那個 ShouldBlockAccessToMember 調用了嗎?如果它返回false,那么直接返回nullptr,上層就會拋 NoSuchMethodXXX 異常;也就觸發系統的限制了。于是我們繼續跟蹤這個方法,這個方法的實現在 java_lang_Class.cc,源碼如下:
?
| 1 2 3 4 5 6 7 8 9 | ALWAYS_INLINE static bool ShouldBlockAccessToMember(T* member, Thread* self)REQUIRES_SHARED(Locks::mutator_lock_) {hiddenapi::Action action = hiddenapi::GetMemberAction(member, self, IsCallerTrusted, hiddenapi::kReflection);if (action != hiddenapi::kAllow) {hiddenapi::NotifyHiddenApiListener(member);}return action == hiddenapi::kDeny; } |
?
毫無疑問,我們應該繼續看 hidden_api.cc 里面的 GetMemberAction方法 :
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | template<typename T> inline Action GetMemberAction(T* member,Thread* self,std::function<bool(Thread*)> fn_caller_is_trusted,AccessMethod access_method)REQUIRES_SHARED(Locks::mutator_lock_) {DCHECK(member != nullptr);// Decode hidden API access flags.// NB Multiple threads might try to access (and overwrite) these simultaneously,// causing a race. We only do that if access has not been denied, so the race// cannot change Java semantics. We should, however, decode the access flags// once and use it throughout this function, otherwise we may get inconsistent// results, e.g. print whitelist warnings (b/78327881).HiddenApiAccessFlags::ApiList api_list = member->GetHiddenApiAccessFlags();Action action = GetActionFromAccessFlags(member->GetHiddenApiAccessFlags());if (action == kAllow) {// Nothing to do.return action;}// Member is hidden. Invoke `fn_caller_in_platform` and find the origin of the access.// This can be *very* expensive. Save it for last.if (fn_caller_is_trusted(self)) {// Caller is trusted. Exit.return kAllow;}// Member is hidden and caller is not in the platform.return detail::GetMemberActionImpl(member, api_list, action, access_method); } |
?
可以看到,關鍵來了。此方法有三個return語句,如果我們能干涉這幾個語句的返回值,那么就能影響到系統對隱藏API的判斷;進而欺騙系統,繞過限制。
?
應對之策
在分析這三個條件之前,我們再思考一下,在調用一個方法/獲取一個成員的時候,除了反射(JNI也算)就沒有別的辦法了嗎?看起來系統只是把反射這條路堵死了,那如果我不走這條路呢?
?
首先,很顯然,除了反射,我們還能直接調用。打個比方,我們要調用 ActivityThread.currentActivityThread()這個方法,除了使用反射;我們還可以把 Android 源碼中的 ActivityThread 這個類copy到我們的項目中,然后使用 provided 依賴,這樣就能像系統一樣直接調用了。至此,我們得到了第一個信息:public類的public方法,可以通過直接調用的方式訪問;當然,private的就都不行了。
?
其次,我們要訪問一個類的成員,除了直接訪問,反射調用/JNI就沒有別的方法了嗎?當然不是。如果你了解ART的實現原理,知道對象布局,那么這個問題就太簡單了。所有的Java對象在內存中其實就是一個結構體,這份內存在 native 層和Java層是對應的,因此如果我們拿到這份內存的頭指針,直接通過偏移量就能訪問成員。你問我方法怎么訪問?ART的對象模型采用的類似Java的 klass-oop方式,方法是存儲在 java.lang.Class對象中的,它們是Class對象的成員,因此訪問方法最終就是訪問成員。(后續我會專門介紹ART的對象模型,解釋 ArtMethod/java.lang.Method/jmethodId之間的關系)。
?
思考完畢,我們會到反射調用的流程;仔細分析一下這三個條件。
?
第一個條件
先看第一個return語句,GetActionFromAccessFlags,看方法名貌似是根據 Method/Field 的 access_flag 來判斷,具體看下代碼:
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | inline Action GetActionFromAccessFlags(HiddenApiAccessFlags::ApiList api_list) {if (api_list == HiddenApiAccessFlags::kWhitelist) {return kAllow;}EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();if (policy == EnforcementPolicy::kNoChecks) {// Exit early. Nothing to enforce.return kAllow;}// if policy is "just warn", always warn. We returned above for whitelist APIs.if (policy == EnforcementPolicy::kJustWarn) {return kAllowButWarn;}// 略。。。 } |
?
首先,如果 Method/Field 是白名單,那么直接允許訪問。我們再往前看,發現這個 api_list 其實是存儲在 Method/Field 的 access_flag中的。
?
也就是說,所有的Method/Field的access_flag 中存儲了hidden_api 的信息,如果有辦法把這個flag直接設置為 kAllow,那么系統就認為它不是隱藏API了。但是,如果要修改 Method/Field 的 access_flag這個成員變量,我們首先得拿到這個 Method/Field 的引用,然而 Android P上就是限制了我們拿這個引用的過程,似乎死循環了;前面我們提到可以通過偏移量的方式修改,但實際上這個場景還有別限制(比如壓根拿不到Class對象);因此這個條件看似可以達到,實際上比較麻煩,于是我們暫且放下。
?
繼續觀察這個方法,接下來 調用了 GetHiddenApiEnforcementPolicy 方法獲取限制策略,如果是 kNoChecks 直接允許;那 GetHiddenApiEnforcementPolicy 這個方法是啥樣呢?在 runtime.h 中,如下:
?
| 1 2 3 | hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const {return hidden_api_policy_; } |
?
也就是說,返回的是 runtime 這個對象的一個成員。如果我們直接修改內存,把這個成員設置為 kNoChecks,那么不就達到目標了嗎?
?
獲取runtime指針
既然需要修改runtime對象的內存,那么首先得拿到runtime對象的指針。本來這個過程需要去分析 ART runtime的啟動過程,但如果完全寫出來那就又是幾篇文章了;這里直接給出結論:
?
在JNI中,我們可以通過 JNIEnv指針拿到 JavaVM指針,這個JavaVM指針實際上是一個 JavaVMExt對象,runtime是 JavaVMExt結構體的成員。說起來比較繞,實際上你看看代碼就明白了:
?
| 1 2 3 4 | JavaVM *javaVM; env->GetJavaVM(&javaVM); JavaVMExt *javaVMExt = (JavaVMExt *) javaVM; void *runtime = javaVMExt->runtime; |
?
感興趣的可以自己去分析為什么可以這么做。
?
搜索內存
我們已經拿到了 runtime指針,也就是這個對象的起始位置;如果要修改對象的成員,必須要知道偏移量。如何知道這個偏移量呢?直接硬編碼寫死也是可行的,但是一旦廠商做一點修改,那就完蛋了;你程序的結果就沒法預期。因此,我們采用一種動態搜索的辦法。
?
runtime是一個很大的結構體,里面的成員不計其數;如果我們要精準定位里面的某一個成員,需要找一些參照物;然后通過這些參照物進一步定位。我們先來觀察一下這個結構體:
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | struct Runtime {// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.uint64_t callee_save_methods_[kCalleeSaveSize];// Pre-allocated exceptions (see Runtime::Init).GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_throwing_exception_;GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_throwing_oome_;GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_handling_stack_overflow_;GcRoot<mirror::Throwable> pre_allocated_NoClassDefFoundError_;// ... (省略大量成員)std::unique_ptr<JavaVMExt> java_vm_;// ... (省略大量成員)// Specifies target SDK version to allow workarounds for certain API levels.int32_t target_sdk_version_;// ... (省略大量成員)bool is_low_memory_mode_;// Whether or not we use MADV_RANDOM on files that are thought to have random access patterns.// This is beneficial for low RAM devices since it reduces page cache thrashing.bool madvise_random_access_;// Whether the application should run in safe mode, that is, interpreter only.bool safe_mode_;// ... (省略大量成員) } |
?
這個結構體非常大,可以直接去看源碼 runtime.h,上面我們挑出了一些我們能夠使用的參照物,輔助進行內存定位:
?
- javavm :我們很熟悉的JavaVM對象,上面我們已經通過 JNIEnv 獲取了,是個已知值。
- target_sdk_version: 這個是我們APP的 targetSdkVersion,我們可以提前知道。
- safe_mode:safe_mode 是 AndroidManifest 中的配置,已知值。
?
因此結合這三個條件,我們對runtime指針執行線性搜索,首先找到 JavaVM指針,然后找到target_sdk_version,最后直達目標;順便用 safe_mode, java_debuggable 等成員驗證正確性。
?
找到目標 hidden_api_policy_之后,直接修改內存,就能達到目的。用偽代碼表示就是:
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | nt unseal(JNIEnv *env, jint targetSdkVersion) {JavaVM *javaVM;env->GetJavaVM(&javaVM);JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;void *runtime = javaVMExt->runtime;const int MAX = 1000;int offsetOfVmExt = findOffset(runtime, 0, MAX, (size_t) javaVMExt);int targetSdkVersionOffset = findOffset(runtime, offsetOfVmExt, MAX, targetSdkVersion);PartialRuntime *partialRuntime = (PartialRuntime *) ((char *) runtime + targetSdkVersionOffset);EnforcementPolicy policy = partialRuntime->hidden_api_policy_;partialRuntime->hidden_api_policy_ = EnforcementPolicy::kNoChecks;return 0; } |
?
代碼我已經放到 github 上了:FreeReflection,使用起來非常簡單,添加依賴;一步調用即可。覺得好用別忘了 star 哦~
?
看起來我們已經達到目標了,但是不要慌;還有2個條件呢,我們繼續,說不定有新發現。
?
第二個條件
然后看第二個return語句,fn_caller_is_trusted,這里面的代碼我就不分析了,直接給結論:這個方法通過回溯調用棧,通過調用者的Class來判斷是否是系統代碼的調用(所有系統的代碼都通過BootClassLoader加載,判斷ClassLoader即可),如果是系統代碼,那么就允許調用(系統自己的API肯定得讓它調)。這里我們又發現一個判斷條件:caller.classloader == BootClassLoader。因此,如果能把這個調用類的ClassLoader修改為 BootClassLoader,那么問題不就解決了嗎?
?
那么問題來了,如何修改Class的classloader?我們看看Class 類的結構:
?
| 1 2 3 4 5 6 7 8 9 10 | public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type,AnnotatedElement {/** defining class loader, or null for the "bootstrap" system loader. */private transient ClassLoader classLoader;// 略 } |
?
classloader實際上是Class類的第一個成員,而這個java.lang.Class我們肯定是能拿到的,因此我們可以通過上面提到的修改偏移的方式直接修改ClassLoader,進而繞過限制。
?
但是需要注意一下這個偏移量。雖然 Class 聲明沒有繼承任何東西,但實際上它繼承自 Object。我們看下 java.lang.Object:
?
| 1 2 3 4 5 6 | public class Object {private transient Class<?> shadow$_klass_;private transient int shadow$_monitor_;} |
?
因此,Class對象在內存中實際上是這樣:
?
| 1 2 3 4 5 | struct Class {Class<?> shadow$_klass_;int shadow$_monitor_;ClassLoader classLoader; } |
?
JVM規范中,一個int占4字節;在ART實現中,一個Java對象的引用占用4字節(不論是32位還是64位),因此 classloader的偏移量為8;我們拿到調用者的Class對象,在JNI層拿到對象的內存表示,直接把偏移量為8處置空(BootClassLoader在為null)即可。當然,如果你不想用JNI,Unsafe也能滿足這個需求。
?
看起來我們已經有好幾種辦法達到目的了,別著急;我們繼續看第三個條件。
?
第三個條件
當代碼流程走到這里,那個action已經不可能是 kAllow了;不要放棄治療,說不定還能復活。觀察代碼:
?
| 1 2 3 4 5 6 7 8 9 10 11 | if (shouldWarn || action == kDeny) {if (member_signature.IsExempted(runtime->GetHiddenApiExemptions())) {action = kAllow;// Avoid re-examining the exemption list next time.// Note this results in no warning for the member, which seems like what one would expect.// Exemptions effectively adds new members to the whitelist.MaybeWhitelistMember(runtime, member);return kAllow;}// 略 } |
?
果然有“豁免”條件:GetHiddenApiExemptions()。跟蹤這個方法之后,你會發現解決辦法跟上面兩種是一樣的。要么去修改 runtime 的內存,要么修改signature;我就不贅述啦。
?
劍走偏鋒
上面我們分析了系統的源代碼,結合各種條件來實現繞過對非SDK API調用的檢測;但實際上所有這些方式我們的目的都是一樣的—— 通過某種方式修改函數的執行流程;而達到這個目標最直接的方法就是 inline hook!!由于inline hook太強大,你只需要找到一個關鍵的執行流程,hook其中的某個函數,修改他的返回值就OK了;這里我也沒啥好分析的,只能給大家推薦一個 inline hook 庫了,名字叫 HookZz,代碼非常優秀,值得一看。
?
后記
本來真的只是打算介紹那個簡單方法的,結果一不小心全寫完啦 :)
?
文章可能有疏漏,也可能有更優秀的辦法;歡迎交流討論~
http://weishu.me/2018/06/07/free-reflection-above-android-p/
總結
以上是生活随笔為你收集整理的一种绕过Android P对非SDK接口限制的简单方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: integer 负数字符串比较_JAVA
- 下一篇: IntelliJ IDEA tomca