天天看點

Android P (4)一種繞過Android P上非SDK接口限制的簡單方法一種繞過Android P上非SDK接口限制的簡單方法

一種繞過Android P上非SDK接口限制的簡單方法

衆所周知,Android P 引入了 針對非 SDK 接口(俗稱為隐藏API)的使用限制。這是繼 Android N上 針對 NDK 中私有庫的連結限制 之後的又一次重大調整。從今以後,不論是native層的NDK還是 Java層的SDK,我們隻能使用Google提供的、公開的标準接口。這對開發者以及使用者乃至整個Android生态,當然是一件好事。但這也同時意味着Android上的各種黑科技有可能會逐漸走向消亡。

作為一個有追求的開發者,我們既要尊重并遵守規則,也要有能力在必要的時候突破規則的束縛,帶着鐐铐跳舞。那麼今天就來探讨一下,如何突破Android P上針對非SDK接口調用的限制。

系統是如何實作這個限制的?

知己知彼,百戰不殆。既然我們想要突破這個限制,自然先得弄清楚,系統是如何給我們施加這個限制的。

文檔 中說,通過反射或者JNI通路非公開接口時會觸發警告/異常等,那麼不妨跟蹤一下反射的流程,看看系統到底在哪一步做的限制(以下的源碼分析大可以走馬觀花的看一下,需要的時候自己再仔細看)。我們從 `java.lang.Class.getDeclaredMethod(String)` 看起,這個方法在Java層 最終調用到了 `getDeclaredMethodInternal` 這個native方法,看一下這個方法的源碼:

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** 調用了嗎?如果它傳回 true,那麼直接傳回`nullptr`,上層就會抛 `NoSuchMethodXXX` 異常;也就觸發系統的限制了。于是我們繼續跟蹤這個方法,這個方法的實作在 java_lang_Class.cc,源碼如下:

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`方法 :

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` 來判斷,具體看下代碼:

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 中,如下:

hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const {
    return hidden_api_policy_;
}
           

也就是說,傳回的是 runtime 這個對象的一個成員。**如果我們直接修改記憶體,把這個成員設定為 kNoChecks**,那麼不就達到目标了嗎?

#### 擷取runtime指針

既然需要修改runtime對象的記憶體,那麼首先得拿到runtime對象的指針。本來這個過程需要去分析 ART runtime的啟動過程,但如果完全寫出來那就又是幾篇文章了;這裡直接給出結論:

在JNI中,我們可以通過 JNIEnv指針拿到 JavaVM指針,這個JavaVM指針實際上是一個 `JavaVMExt`對象,runtime是 JavaVMExt結構體的成員。說起來比較繞,實際上你看看代碼就明白了:

JavaVM *javaVM;
env->GetJavaVM(&javaVM);
JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;
           

感興趣的可以自己去分析為什麼可以這麼做。

#### 搜尋記憶體

我們已經拿到了 runtime指針,也就是這個對象的起始位置;如果要修改對象的成員,必須要知道偏移量。如何知道這個偏移量呢?直接寫死寫死也是可行的,但是一旦廠商做一點修改,那就完蛋了;你程式的結果就沒法預期。是以,我們采用一種**動态搜尋**的辦法。

runtime是一個很大的結構體,裡面的成員不計其數;如果我們要精準定位裡面的某一個成員,需要找一些參照物;然後通過這些參照物進一步定位。我們先來觀察一下這個結構體:

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,上面我們挑出了一些我們能夠使用的參照物,輔助進行記憶體定位:

  • java_vm_ :我們很熟悉的JavaVM對象,上面我們已經通過 JNIEnv 擷取了,是個已知值。
  • target_sdk_version: 這個是我們APP的 targetSdkVersion,我們可以提前知道。
  • safe_mode:safe_mode 是 AndroidManifest 中的配置,已知值。

是以結合這三個條件,我們對runtime指針執行線性搜尋,首先找到 JavaVM指針,然後找到target_sdk_version,最後直達目标;順便用 safe_mode, java_debuggable 等成員驗證正确性。

找到目标 `hidden_api_policy_`之後,直接修改記憶體,就能達到目的。用僞代碼表示就是:

int 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 上了:

tiann/FreeReflection​github.com

Android P (4)一種繞過Android P上非SDK接口限制的簡單方法一種繞過Android P上非SDK接口限制的簡單方法

使用起來非常簡單,添加依賴;一行代碼調用即可。覺得好用别忘了 star 哦~

看起來我們已經達到目标了,但是不要急;還有2個條件呢,我們繼續,說不定有新發現。

### 第二個條件

然後看第二個return語句,`fn_caller_is_trusted`,這裡面的代碼我就不分析了,直接給結論:這個方法通過回溯調用棧,通過調用者的Class來判斷是否是系統代碼的調用(所有系統的代碼都通過BootClassLoader加載,判斷ClassLoader即可),如果是系統代碼,那麼就允許調用(系統自己的API肯定得讓它調)。這裡我們又發現一個判斷條件:`caller.classloader == BootClassLoader`。是以,如果能把這個調用類的ClassLoader修改為 BootClassLoader,那麼問題不就解決了嗎?

那麼問題來了,如何修改Class的classloader?我們看看Class 類的結構:

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`:

public class Object {

    private transient Class<?> shadow$_klass_;
    private transient int shadow$_monitor_;
    
}
           

是以,Class對象在記憶體中實際上是這樣:

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了;不要放棄治療,說不定還有救。觀察代碼:

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 庫了,名字叫 https://github.com/jmpews/HookZz,代碼非常優秀,值得一看。

## 後記

繼續閱讀