【Android】针对非SDK接口的限制解决方案

针对非SDK接口的限制

缘由

谷歌官方对于针对非SDK接口的限制说明,其主要目的是提升用户和开发者体验而为。

从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定,请参阅通过减少非 SDK 接口的使用来提高稳定性。

理解Android P内部API的限制调用机制

另一方面我认为也是对开发者滥用反射能力的约束以及多版本系统兼容性处理:系统底层敏感接口或是将来可能会废弃接口的保护措施。可能在将来某一时刻,以前可以反射使用的接口无法被调用从而引发异常崩溃等不可预估的情况(虽然这种可能性极小)。

多种方案

双重反射(元反射)

此方案来自weishu的FreeReflection

双重反射的核心思路是让系统认为反射调用来自于可信的系统代码而非应用自身。

此方案是绕过系统对系统方法调用判断规则,当反射Class方法时系统底层认为其为系统级别的类方法,由于反射API就是系统API,此时该方法为系统方法可反射被限制的API。

但如果每次反射一个方法都通过双重反射显得非常繁琐且效率低。这时候再通过重设黑名单限制去豁免更多API访问。

通过反射方法获取到的提权反射方法去调用VMRuntime去获取豁免api设置接口方法setHiddenApiExemptions将所有Java方法豁免new String[]{"L"}从而绕开API限制。这样只需要一次双重反射,后面的API访问都被开放了。

Java 复制代码
if (SDK_INT >= Build.VERSION_CODES.P) {
    try {
        Method forName = Class.class.getDeclaredMethod("forName", String.class);
        Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
        Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
        Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
        setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
        sVmRuntime = getRuntime.invoke(null);
    } catch (Throwable e) {
        Log.w(TAG, "reflect bootstrap failed:", e);
    }
}

该方案在Android9以及低版本是可行的,在Android10开始就无法使用了。

Android9系统源码中核心的方法豁免代码在/art/runtime/native/java_lang_Class.cc类中的IsCallerTrusted。其主要实现原理阅读注释以及源码分析可知:当方法为来自系统内部类时可信任,从而反射Class类得到的方法可绕过了系统限制。

再看Android10系统源码IsCallerTrusted被删除,新写了hiddenapi::AccessContext GetReflectionCaller方法。

Walk the stack and find the first frame not from java.lang.Class and not from java.lang.invoke. This is very expensive. Save this till the last.

注释说明也非常明显:遍历堆栈时过滤掉来自classinvoke。因此在Android10以及以上系统版本采用双重反射此方案已不可行。

DexLoader

此方案还是来自weishu的FreeReflection中提到的另一种方式。

元反射方法在Android10以及高版本以上失效后,兼容高版本豁免能力可采取使用DexFile加载Class方案再次绕过系统限制。

Java 复制代码
    private static boolean unsealByDexFile(Context context) {
        byte[] bytes = Base64.decode(DEX, Base64.NO_WRAP);
        File codeCacheDir = getCodeCacheDir(context);
        if (codeCacheDir == null) {
            return false;
        }
        File code = new File(codeCacheDir, System.currentTimeMillis() + ".dex");
        try {

            try (FileOutputStream fos = new FileOutputStream(code)) {
                fos.write(bytes);
            }

            // Support target Android U.
            // https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
            try {
                //noinspection ResultOfMethodCallIgnored
                code.setReadOnly();
            } catch (Throwable ignore) {
            }

            @SuppressWarnings("deprecation")
            DexFile dexFile = new DexFile(code);
            // This class is hardcoded in the dex, Don't use BootstrapClass.class to reference it
            // it maybe obfuscated!!
            Class<?> bootstrapClass = dexFile.loadClass("me.weishu.reflection.BootstrapClass", null);
            Method exemptAll = bootstrapClass.getDeclaredMethod("exemptAll");
            return (boolean) exemptAll.invoke(null);
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        } finally {
            if (code.exists()) {
                //noinspection ResultOfMethodCallIgnored
                code.delete();
            }
        }
    }

此方案是通过Dex文件加载自定义类BootstrapClass,因为在DexFile源码中loadClass是调用底层native方法且当Classloadernull时则会被判断为系统BootClassLoader从而被系统豁免。然后再通过BootstrapClass调用内部方法再执行元反射逻辑。因此可认为该方案是元反射的升级版:系统类不通过再尝试提权到加载类的ClassLoader获取更高权限。

Android10源码下可知初始化注册DexFile时若class_loadernull输出为Domain::KPlatform标识。

原先Android9源码中的ShouldBlockAccessToMember在高版本中修改为ShouldDenyAccessToMember并且方法收敛到hidden_api.h中。在此方法中核心判断上下文是否为Domain::kPlatform,若是判定为系统级别则豁免通过。

该方案目前还可行但在API26被标记为废弃。因此该方案是目前的折中方案,或许在未来某一天就无效了。

DexFile-ClassLoader

This method was deprecated in API level 26. Applications should use one of the standard classloaders such as PathClassLoader instead. This API will be removed in a future Android release.

Unsafe

此方案来自LoveSyKun的AndroidHiddenApiBypass

该方案核心运用:

  • 提取ArtMethod
  • 计算ArtMethod大小
  • MethodArtMethod互转

其核心原理需要对ART和系统底层源码具备一定基础储备和深入了解能力。

分析该方案的文章值得反复阅读理解:

大致总结采用纯Java环境下利用Unsafe通过内存操作能力并结合ART虚拟机Java核心类和native类共享内存镜像(mirror)机制:

  1. 通过镜像绕过Java层私有成员对象无法访问的限制,直接访问底层ArtMethod
  2. 利用 ArtMethod 连续存储的特性,通过两个连续方法的指针差,到单个ArtMethod的大小。
  3. 匹配目标隐藏API的ArtMethod指针,找到底层方法元数据。
  4. 利用Unsafe修改普通Method的artMethod字段,伪装成隐藏载体绕过检查。

JNI_Hook

此方案来自bytedance的bhook

其文档中介绍:Android native hook 的实现方式有很多种,其中使用最广泛,并且通用性最强的是inline hookPLT hook。在真实的线上环境中,经常是 PLT hook 和 inline hook 并存的,这样它们可以各自扬长避短,在不同的场景中发挥作用。

此方案的实现原理相比Unsafe更复杂这里不再展开,可参考文档说明 原理解析

同时可参考学习xHook,作为一个开源较早的Android PLT hook 方案,它的参考文档中也介绍说明了 Android PLT hook 概述可作为学习资料深入研究。

参考

相关推荐
猪哥帅过吴彦祖3 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
2501_916008893 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
stevenzqzq4 小时前
Android Hilt教程_构造函数
android
鹏多多4 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios
GISer_Jing5 小时前
Flutter开发全攻略:从入门到精通
android·前端·flutter
东坡肘子5 小时前
Skip Fuse现在对独立开发者免费! -- 肘子的 Swift 周报 #0110
android·swiftui·swift
江上清风山间明月5 小时前
如何让APK获得系统权限
android·apk·root·系统权限
2501_9400940211 小时前
NDS模拟器安卓版 melonDS模拟器 汉化中文版 NDS BIOS和固件+NDS游戏和详细的使用教程
android·游戏