针对非SDK接口的限制
缘由
谷歌官方对于针对非SDK接口的限制说明,其主要目的是提升用户和开发者体验而为。
从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定,请参阅通过减少非 SDK 接口的使用来提高稳定性。
另一方面我认为也是对开发者滥用反射能力的约束以及多版本系统兼容性处理:系统底层敏感接口或是将来可能会废弃接口的保护措施。可能在将来某一时刻,以前可以反射使用的接口无法被调用从而引发异常崩溃等不可预估的情况(虽然这种可能性极小)。
多种方案
双重反射(元反射)
此方案来自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.
注释说明也非常明显:遍历堆栈时过滤掉来自class和invoke。因此在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方法且当Classloader是null时则会被判断为系统BootClassLoader从而被系统豁免。然后再通过BootstrapClass调用内部方法再执行元反射逻辑。因此可认为该方案是元反射的升级版:系统类不通过再尝试提权到加载类的ClassLoader获取更高权限。
在Android10源码下可知初始化注册DexFile时若class_loader为null输出为Domain::KPlatform标识。

原先Android9源码中的ShouldBlockAccessToMember在高版本中修改为ShouldDenyAccessToMember并且方法收敛到hidden_api.h中。在此方法中核心判断上下文是否为Domain::kPlatform,若是判定为系统级别则豁免通过。
该方案目前还可行但在API26被标记为废弃。因此该方案是目前的折中方案,或许在未来某一天就无效了。
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大小 Method与ArtMethod互转
其核心原理需要对ART和系统底层源码具备一定基础储备和深入了解能力。
分析该方案的文章值得反复阅读理解:
大致总结采用纯Java环境下利用Unsafe通过内存操作能力并结合ART虚拟机Java核心类和native类共享内存镜像(mirror)机制:
- 通过镜像绕过Java层私有成员对象无法访问的限制,直接访问底层
ArtMethod - 利用 ArtMethod 连续存储的特性,通过两个连续方法的指针差,到单个
ArtMethod的大小。 - 匹配目标隐藏API的
ArtMethod指针,找到底层方法元数据。 - 利用
Unsafe修改普通Method的artMethod字段,伪装成隐藏载体绕过检查。
JNI_Hook
此方案来自bytedance的bhook
其文档中介绍:Android native hook 的实现方式有很多种,其中使用最广泛,并且通用性最强的是inline hook和PLT hook。在真实的线上环境中,经常是 PLT hook 和 inline hook 并存的,这样它们可以各自扬长避短,在不同的场景中发挥作用。
此方案的实现原理相比Unsafe更复杂这里不再展开,可参考文档说明 原理解析。
同时可参考学习xHook,作为一个开源较早的Android PLT hook 方案,它的参考文档中也介绍说明了 Android PLT hook 概述可作为学习资料深入研究。