破解 Android P 對隱藏Api訪問的限制


本文參考資料:
《一種繞過Android P對非SDK接口限制的簡單方法》
《另一種繞過 Android P以上非公開API限制的辦法》

一、Android P 引入了針對隱藏API的使用限制

眾所周知,Android P 引入了針對非 SDK 接口(俗稱為隱藏API)的使用限制。這是繼 Android N上針對 NDK 中私有庫的鏈接限制之后的又一次重大調整。從今以后,不論是native層的NDK還是 Java層的SDK,我們只能使用Google提供的、公開的標准接口。

舉個例子:
Android SDK的 WifiManager方法對很多的Filed設置了隱藏,舉個例子:

  /**
     * Broadcast intent action indicating whether Wi-Fi scanning is allowed currently
     * @hide
     */
    public static final String WIFI_SCAN_AVAILABLE = "wifi_scan_available";

如果直接用反射區訪問:

 public static void getWifiReflection(Context context) {
        WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        try {
            Field field = wifiManager.getClass().getDeclaredField("WIFI_SCAN_AVAILABLE");
            Log.e("PReflectionUtils", (String) field.get(wifiManager));
        } catch (Exception e) {
            e.printStackTrace();
        }
}

在Android P以上的機型上運行會發現訪問失敗:

W/oid.handwritin: Accessing hidden field Landroid/net/wifi/WifiManager;->WIFI_SCAN_AVAILABLE:Ljava/lang/String; (dark greylist, reflection)
W/System.err: java.lang.NoSuchFieldException: No field WIFI_SCAN_AVAILABLE in class Landroid/net/wifi/WifiManager; (declaration of 'android.net.wifi.WifiManager' appears in /system/framework/framework.jar!classes2.dex)
W/System.err:     at java.lang.Class.getDeclaredField(Native Method)
W/System.err:     at com.renhui.android.handwriting.common.PReflectionUtils.getWifiReflection(PReflectionUtils.java:18)
W/System.err:     at com.renhui.android.handwriting.MyApplication.onCreate(MyApplication.java:17)
W/System.err:     at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1162)
W/System.err:     at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6717)
W/System.err:     at android.app.ActivityThread.access$2000(ActivityThread.java:273)
...

但是在低版本上是能訪問的:

E/PReflectionUtils: wifi_scan_available

二、Android系統如何實現對隱藏API的訪問限制

通過反射或者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 調用了嗎?如果它返回false,那么直接返回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的判斷;進而欺騙系統,繞過限制。

三、我們如何實現對隱藏Api的訪問

我們要訪問一個類的成員,除了直接訪問,反射調用/JNI就沒有別的方法了嗎?當然不是。如果你了解ART的實現原理,知道對象布局,那么這個問題就太簡單了。

所有的Java對象在內存中其實就是一個結構體,這份內存在 native 層和Java層是對應的,因此如果我們拿到這份內存的頭指針,直接通過偏移量就能訪問成員。

那么方法如何訪問呢?ART的對象模型采用的類似Java的 klass-oop方式,方法是存儲在 java.lang.Class對象中的,它們是Class對象的成員,因此訪問方法最終就是訪問成員。

下面我們接着上面繼續看,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;
  }
  ...
}

繼續觀察這個方法,接下來 調用了 GetHiddenApiEnforcementPolicy 方法獲取限制策略,如果是 kNoChecks 直接允許;那 GetHiddenApiEnforcementPolicy 這個方法是啥樣呢?在 runtime.h 中,如下:

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

也就是說,返回的是 runtime 這個對象的一個成員。如果我們直接修改內存,把這個成員設置為 kNoChecks,那么不就達到目標了嗎?

下面我們來實踐一下,首先要獲取runtime指針:

既然需要修改runtime對象的內存,那么首先得拿到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,上面我們挑出了一些我們能夠使用的參照物,輔助進行內存定位:

  • 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_之后,直接修改內存,就能達到目的。用偽代碼表示就是:

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;
}

到此為止,基本上實現對隱藏API訪問限制的破解了。

但是,大佬不滿足只到這個程度,他發現系統有一個 fn_caller_is_trusted 條件:如果調用者是系統類,那么就允許被調用。
也就是說,如果我們能以系統類的身份去反射,那么就能暢通無阻。問題是,我們如何以「系統的身份去反射」呢?一種最常見的辦法是,我們自己寫一個類,然后通過某種途徑把這個類的 ClassLoader 設置為系統的 ClassLoader,再借助這個類去反射其他類。但是這里的「通過某種途徑」依然要使用一些黑科技才能實現,與修改 flags / inline hook 無本質區別。

以系統類的身份去反射 有兩個意思,1. 直接把我們自己變成系統類;2. 借助系統類去調用反射。我們一個個分析。

1.直接把我們自己變成系統類

這個方式有童鞋可能覺得天方夜譚,APP 的類怎么可能成為系統類?但是,一定不要被自己的固有思維給局限,一切皆有可能!我們知道,對APP來說,所謂的系統類就是被 BootstrapClassLoader 加載的類,這個 ClassLoader 並非普通的 DexClassLoader,因此我們無法通過插入 dex path的方式注入類。但是,Android 的 ART 在 Android O 上引入了 JVMTI,JVMTI 提供了將某一個類轉換為 BootstrapClassLoader 中的類的方法!具體來說,我們寫一個類暴露反射相關的接口,然后通過 JVMTI 提供的 AddToBootstrapClassLoaderSearch將此類加入 BootstrapClassLoader 就實現目的了。不過,JVMTI 要在 release 版本的 APP 上運行依然需要 Hack,所以這種途徑與其他的黑科技無本質區別。

2.借助系統的類去反射

如果系統有一個方法systemMethod,這個systemMethod 去調用反射相反的方法,那么systemMethod毋庸置疑會反射成功。但是,我們從哪去找到這么一個方法給我們用?

  • 首先,我們通過反射 API 拿到 getDeclaredMethod 方法。getDeclaredMethod 是 public 的,不存在問題;這個通過反射拿到的方法我們稱之為元反射方法。
  • 然后,我們通過剛剛反射拿到元反射方法去反射調用 getDeclardMethod。這里我們就實現了以系統身份去反射的目的——反射相關的 API 都是系統類,因此我們的元反射方法也是被系統類加載的方法;所以我們的元反射方法調用的 getDeclardMethod 會被認為是系統調用的,可以反射任意的方法。
Method metaGetDeclaredMethod =
        Class.class.getDeclaredMethod("getDeclardMethod"); // 公開API,無問題
Method hiddenMethod = metaGetDeclaredMethod.invoke(hiddenClass,
        "hiddenMethod", "hiddenMethod參數列表"); // 系統類通過反射使用隱藏 API,檢查直接通過。
hiddenMethod.invoke // 正確找到 Method 直接反射調用

到這里,我們已經能通過「元反射」的方式去任意獲取隱藏方法或者隱藏 Field 了。但是,如果我們所有使用的隱藏方法都要這么干,那還有點小麻煩。在 上文中,我們后來發現,隱藏 API 調用還有「豁免」條件,具體代碼如下:

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;
    }
    // 略    
}

只要 IsExempted 方法返回 true,就算這個方法在黑名單中,依然會被放行然后允許被調用。我們再觀察一下IsExempted方法:

bool MemberSignature::IsExempted(const std::vector<std::string>& exemptions) {
  for (const std::string& exemption : exemptions) {
    if (DoesPrefixMatch(exemption)) {
      return true;
    }
  }
  return false;
}

繼續跟蹤傳遞進來的參數 runtime->GetHiddenApiExemptions() 發現這玩意兒也是 runtime 里面的一個參數,既然如此,我們可以一不做二不休,仿照修改 runtime flag 的方式直接修改 hidden_api_exemptions_ 也能繞過去。但如果我們繼續跟蹤下去,會有個有趣的發現:這個API 竟然是暴露到 Java 層的,有一個對應的 VMRuntime.setHiddenApiExemptions Java方法;也就是說,只要我們通過 VMRuntime.setHiddenApiExemptions 設置下豁免條件,我們就能愉快滴使用反射了。

再結合上面這個方法,我們只需要通過 「元反射」來反射調用 VMRuntime.setHiddenApiExemptions 就能將我們自己要使用的隱藏 API 全部都豁免掉了。更進一步,如果我們再觀察下上面的 IsExempted 方法里面調用的 DoesPrefixMatch,發現這玩意兒在對方法簽名進行前綴匹配;童鞋們,我們所有Java方法類的簽名都是以 L開頭啊!如果我們把直接傳個 L進去,所有的隱藏API全部被赦免了!

基於上面的內容weishu 大佬開源了:FreeReflection

同時也提出來了基於修改signature的方式來實現破解的思路。

其實可以看出要實現繞過對非SDK API調用的檢測;實現的方式目的都是一樣的:即通過某種方式修改函數的執行流程;而達到這個目標最直接的方法就是 inline hook!!由於inline hook太強大,你只需要找到一個關鍵的執行流程,hook其中的某個函數,修改他的返回值就OK了;這里我也沒啥好分析的,只能給大家推薦一個 inline hook 庫了,名字叫 HookZz,項目地址:https://github.com/jmpews/Dobby。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM