LSPass 学习
LSPass 学习
前置知识
ART 对象表示
在 ART 中,一个 Java 对象与一个 art::mirror::Object 一一对应,此外一些系统的特殊类会和 mirror Object 的派生类对应,如 java/lang/Class 对应 art::mirror::Class 。
mirror::Object 对象所包含的内容就是 instance field 的内容,因此它的大小和类的 instance field 有关。之所以叫 mirror ,是因为这些 mirror 类中定义的成员和 Java 类的字段(field)是一致的。
一般的类的对象对应的都是 art::mirror::Object ,访问 field 是通过对象地址 + field offset 实现的。如果一个类继承另一个类,则内存布局上子类的 field 排在父类的后面,这和结构体继承的内存布局是一致的。
例如 art::mirror::Object 只有两个成员:
1 | // art/runtime/mirror/object.h |
java.lang.Object 也只有两个字段:
1 | // libcore/ojluni/src/main/java/java/lang/Object.java |
在 C++ 中的两个成员和 Java 中这两个字段都是一一对应的。
又如 art::mirror::Class 和 java.lang.Class ,它的字段更多:
1 | // art/runtime/mirror/class.h |
1 | // libcore/ojluni/src/main/java/java/lang/Class.java |
由于 art::mirror::Class 继承 art::mirror::Object ,因此它也有 shadow$_klass_ 和 shadow$_monitor_ 两个字段,而在 Java 中 java.lang.Class 也是继承 java.lang.Object 的,由此可见二者在 java 和 native 中对象关系的一致性。
从注释中可以看到,Object 的内存布局,也就是各个 instance fields 的布局由 art::ClassLinker::LinkFieldsHelper::LinkFields 决定。
LinkFields
ClassLinker::LinkFieldsHelper::LinkFields -> .. -> ArtField::SetOffset
从注释可以看出,field 的布局与我们在 java 中定义的顺序或者 dex 的 index 都无关,而是与所有 field 的名字和类型相关。首先按照类型 object, long, double, int, float, char, short, boolean, byte 的顺序,然后按照 field 名的字典序分布。因此,art::mirror 上手动定义的成员也要遵守这个顺序。
1 | // we want a relatively stable order so that adding new fields |
隐藏 API
隐藏 API 限制是 Android P 引入的。它可以限制通过反射、JNI 和链接方式获取被禁止的 method 或 field 的访问。实际上被限制的就是这几个地方:
Class.get[Declared]Field|Method[s]JNIEnv::Get{Method|Field}ID- dalvik 指令直接访问 method 或 field ,在 link 阶段,阻止隐藏的 method 或 field 被解析到 dex cache 中。这限制了通过 api stub 的链接方式访问。
值得注意的是,如果得到了 Method 或 Field 对象,然后直接 invoke 或 get/set 是不会有检查的。此外,这个限制也不会阻止访问 Class ,Class.forName 或者在 dalvik 指令访问有隐藏 API 的类都是可以的。可以说这个限制仅阻拦了应用代码以某种形式获取 ArtMethod 或 ArtField 的访问,但并不会在调用时检查。
实现:art/runtime/hidden_api.cc ShouldDenyAccessToMemberImpl
在 Java 层有 API dalvik.system.VMRuntime.setHiddenApiExemptions 可以设置隐藏 API 的豁免名单,然而这也是个隐藏 API 。对于 DexFile ,调用 setTrusted 方法可以解除限制,不过仅当 Java debuggable 的时候有效。
sun.misc.Unsafe 并不是 SDK API ,不过也没有隐藏 API 限制。
getUnsafe
这里用反射调用是因为 getUnsafe 会检查调用者的 ClassLoader ,限制只有 BootClassLoader 才能调用。
1 | static { |
1 | /** |
native pointer fields
接下来通过 unsafe 访问一些 fields ,基本步骤是 objectFieldOffset -> getLong(obj, offset) 。
1 | methodOffset = unsafe.objectFieldOffset(Helper.Executable.class.getDeclaredField("artMethod")); |
Helper 定义了一系列和 android 中 Class, Executable 等类的 field 完全一致的类。因为这几个 fields 都是隐藏 api ,不能直接访问,但可以利用相同布局 offset 一致的特性,通过 Unsafe 访问。
1 | Accessing hidden field Ljava/lang/reflect/Executable;->declaringClass:Ljava/lang/Class; (greylist-max-o, reflection, denied) |
计算 method 和 field 数组的 bias
Class.methods 指向一个长度前缀的数组,由于有内存对齐,我们还是要计算第一个元素的偏移(bias)
1 | Method mA = Helper.NeverCall.class.getDeclaredMethod("a"); |
减去一个 artMethodSize 是因为有一个 constructor
TODO: 方法在 methods 中的排列顺序
在这里没有取 Executable.artMethod 而是用了 MethodHandle.artFieldOrMethod ,理论上两个值是一样的。不过 Field 没有 artField 这样的 field ,只有 getArtField 方法,这个方法也是隐藏的,因此 Field 只能用 MethodHandle 获取 ArtField 的地址了。
1 | Field fI = Helper.NeverCall.class.getDeclaredField("i"); |
getDeclared
利用上面计算的 ArtMethod 、ArtField 的大小和首个元素在带前缀数组的偏移,我们就可以遍历 Class 对象的 methods, iFields, sFields 获取 Method 和 Field 了。
getDeclaredMethods:
1 |
|
首先随便获取一个方法的 MethodHandle ,然后遍历 Class.methods ,获取 ArtMethod ,用 Unsafe 修改 artFieldOrMethod 为获取到的指针,调用 revealDirect ,就会根据 artFieldOrMethod 创建对应的 Member ,保存在 MethodHandleInfo 中,用 Unsafe 获取即可。这里忽略了异常,是因为 revealDirect 会在获取到 Member 之后进行访问检查。
获取 instance fields 和 static fields 也是类似的方法,不过 MethodHandle 是来自 instance field 或者 static field 的。
invoke / newInstance
随便选取一个 Stub 方法,反射获取它的 Method / Constructor 对象,使用 Unsafe 修改 artMethod 的地址为目标方法,再调用 invoke 或者 newInstance 即可。
1 | public static class InvokeStub { |
当然用 getDeclaredMethod 得到的 Method 对象也是可以的。
进程内全局解除 API 限制
有了 invoke 后,调用 dalvik.system.VMRuntime.setHiddenApiExemptions 即可。
应对布局改变
20260302 补充
随着 ART 的更新,LSPass 用到的类的布局也在改变,如 Class 的 iFields 和 sFields 合并 (art),因此 LSPass 直接加载系统的 core-oj.jar ,这个 jar 就是定义了 java.lang.Class 类等的 jar ,这样就能以普通类加载器(PathClassLoader)的身份加载它们,利用 fields 相同布局一致的特性同样可以得到偏移。
为了降低 IO 开销,这里只加载了 BOOTCLASSPATH 的第一个元素,观察 BOOTCLASSPATH 可知 core-oj.jar 是第一位,具体是不是总是第一位有待考证。
我们知道 member 的 AccessContext 取决于其 definingClass 的 DexFile 的文件位置,并且 hiddenapi 检查利用了 caller 和被访问 member 的 AccessContext 进行比较,那么为什么用 PathClassLoader 加载来自 apex art 的类,其 member 不会被 hiddenapi 限制?这是因为 hiddenapi 信息存储在 dex 中,仅当被加载的类是被 BootClassLoader 加载的时候才会读取,否则不会,并用于初始化 member 的 runtimeFlags 。因此用 PathClassLoader 去加载 core-oj.jar 是不会有 hiddenapi 的 flags ,也就能跳过检查。
class_linker.cc LoadField/LoadMethod -> CreateRuntimeFlags -> GetDexFlags ->
ClassAccessor::BaseItem::GetHiddenApiFlagsClassAccessor::BaseItem::hidden_api_flags_默认为 0 ,如果不提供hiddenapi_ptr_pos_(即非 BootClassLoader)则一直为 0 ,这个 flags 值直接作为 ApiList 的值(实际上与 dex format 定义的是一一对应的)
Native caller 限制
20260302 补充
https://cs.android.com/android/_/android/platform/art/+/c372dbb5668ee2347702050964ce042f3dcd175a
这个提交对于 jni 的 GetMethod/FieldID 调用,不再只检查 java caller ,而是同时检查 native caller ,同样根据返回地址获取所属 dso 的路径(利用 dl_iterate_phdr 查询),并根据路径位置决定 AccessContext ,目前存在功能开关 flags ,且只用于检查 application 上下文,其他情况会 fallback 到 java caller 检查。
其他方法
JNI_OnLoad
JNI_OnLoad 的时候,实际上没有任何隐藏 API 限制!
这是因为 JNI 的调用检查的依据是 Java 调用堆栈的 caller class 。当 JNI_OnLoad 被调用的时候,Caller 是 java.lang.Runtime ,这是个系统类,显然可以得到豁免。
一般情况下,调用 JNI 方法的 caller 是自己,因此就无法豁免了。
jni_internal.cc GetJniAccessContext
1 | static hiddenapi::AccessContext GetJniAccessContext(Thread* self) |
其实这和以前的(失效的)元反射类似,都是 visit stack 的时候没有考虑到系统类调用的情形。元反射最终被修复了,因为对于反射,walk stack 会寻找调用者中第一个不是 java.lang.Class, java.lang.reflect, java.lang.invoke 的类。
art/runtime/hidden_api.cc GetReflectionCallerAccessContext
类似地,如果创建一个 native 线程(pthread_create 之类)再 attach ,此时的 caller 根本不存在,因此 GetJniAccessContext 直接把它当作 trusted domain 了,这样也可以调用隐藏 API 。或者也可以通过反射调用自己的 JNI 函数。
系统设置
Google 留给开发者的后门。修改系统设置可以在系统范围内给所有进程解除隐藏 API 限制。修改系统设置是不需要 root 的。
解除:
1 | adb shell settings put global hidden_api_policy 1 |
还原:
1 | adb shell settings delete global hidden_api_policy |
实质上是让系统服务创建进程的时候改变 runtimeFlags ,关闭隐藏 API 限制。
DexFile.setTrusted
LSPlant 的 MakeDexFileTrusted 可以给某个 dex 解除隐藏 api 限制,其实现是调用 art 内部函数 DexFile::setTrusted 。LSPosed 利用这个方法(1, 2)给自己和模块的 dex 解除限制,因此模块可以正常使用隐藏的 api 。
使用 framework 中具有反射功能的方法
20260302 补充
参见 LSPass.java,利用 android.util.Property 创建的 ReflectiveProperty 恰好可以匹配到 getDeclaredMethods/getDeclaredFields ,且这个类属于 Platform context ,如果没有 Platform 对 CorePlatform 的限制还是可以绕过的。
总结
LSPass 应该是目前功能最为完备的隐藏 api 限制绕过工具了,不仅可以在进程内全局解除 api 限制,也可以在不解除 api 限制的情况下使用隐藏 api ,避免对应用产生影响(LSPatch 的例子)。
参考
Android运行时ART加载类和方法的过程分析 · 老罗的Android之旅(总结) · 看云
https://github.com/LSPosed/AndroidHiddenApiBypass
https://lovesykun.cn/archives/android-hidden-api-bypass.html#fn:5