zoukankan      html  css  js  c++  java
  • 破解 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。

  • 相关阅读:
    Java核心API需要掌握的程度
    JAVA开发者最常去的20个英文网站
    Java .class files decompile tools
    让aptget到网络上查找源,避免经常插入光盘
    Google Job : Research Engineer Beijing
    教你亚健康该如何饮食
    tcpdump w 和 r 的使用
    黑客发布iOS 4.1永久越狱程序 狼人:
    微软将迎来迄今最大补丁日 一次修补49个漏洞 狼人:
    百度瞄准客户端 迅雷、360、腾讯皆是目标 狼人:
  • 原文地址:https://www.cnblogs.com/renhui/p/14214996.html
Copyright © 2011-2022 走看看