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
2
3
4
5
// art/runtime/mirror/object.h
// The Class representing the type of the object.
HeapReference<Class> klass_;
// Monitor and hash code information.
uint32_t monitor_;

java.lang.Object 也只有两个字段:

1
2
3
// libcore/ojluni/src/main/java/java/lang/Object.java
private transient Class<?> shadow$_klass_;
private transient int shadow$_monitor_;

在 C++ 中的两个成员和 Java 中这两个字段都是一一对应的。

又如 art::mirror::Class 和 java.lang.Class ,它的字段更多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// art/runtime/mirror/class.h
// 'Class' Object Fields
// Order governed by java field ordering. See art::ClassLinker::LinkFieldsHelper::LinkFields.

// Defining class loader, or null for the "bootstrap" system loader.
HeapReference<ClassLoader> class_loader_;

// For array classes, the component class object for instanceof/checkcast
// (for String[][][], this will be String[][]). null for non-array classes.
HeapReference<Class> component_type_;

// DexCache of resolved constant pool entries (will be null for classes generated by the
// runtime such as arrays and primitive classes).
HeapReference<DexCache> dex_cache_;

// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// libcore/ojluni/src/main/java/java/lang/Class.java
/** defining class loader, or null for the "bootstrap" system loader. */
private transient ClassLoader classLoader;

/**
* For array classes, the component class object for instanceof/checkcast (for String[][][],
* this will be String[][]). null for non-array classes.
*/
private transient Class<?> componentType;

/**
* DexCache of resolved constant pool entries. Will be null for certain runtime-generated classes
* e.g. arrays and primitive classes.
*/
private transient Object dexCache;

// ...

由于 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

https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/class_linker.cc;l=9099;drc=c0eb27ecf91c807d66efbae6991918557627e120

从注释可以看出,field 的布局与我们在 java 中定义的顺序或者 dex 的 index 都无关,而是与所有 field 的名字和类型相关。首先按照类型 object, long, double, int, float, char, short, boolean, byte 的顺序,然后按照 field 名的字典序分布。因此,art::mirror 上手动定义的成员也要遵守这个顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// we want a relatively stable order so that adding new fields
// minimizes disruption of C++ version such as Class and Method.
//
// The overall sort order order is:
// 1) All object reference fields, sorted alphabetically.
// 2) All java long (64-bit) integer fields, sorted alphabetically.
// 3) All java double (64-bit) floating point fields, sorted alphabetically.
// 4) All java int (32-bit) integer fields, sorted alphabetically.
// 5) All java float (32-bit) floating point fields, sorted alphabetically.
// 6) All java char (16-bit) integer fields, sorted alphabetically.
// 7) All java short (16-bit) integer fields, sorted alphabetically.
// 8) All java boolean (8-bit) integer fields, sorted alphabetically.
// 9) All java byte (8-bit) integer fields, sorted alphabetically.
//
// (References are first to increase the chance of reference visiting
// being able to take a fast path using a bitmap of references at the
// start of the object, see `Class::reference_instance_offsets_`.)
//
// Once the fields are sorted in this order we will attempt to fill any gaps
// that might be present in the memory layout of the structure.
// Note that we shall not fill gaps between the superclass fields.

// Collect fields and their "type order index" (see numbered points above).

隐藏 API

隐藏 API 限制是 Android P 引入的。它可以限制通过反射、JNI 和链接方式获取被禁止的 method 或 field 的访问。实际上被限制的就是这几个地方:

  1. Class.get[Declared]Field|Method[s]
  2. JNIEnv::Get{Method|Field}ID
  3. 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
2
3
4
5
static {
try {
//noinspection JavaReflectionMemberAccess DiscouragedPrivateApi
unsafe = (Unsafe) Unsafe.class.getDeclaredMethod("getUnsafe").invoke(null);
assert unsafe != null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Gets the unique instance of this class. This is only allowed in
* very limited situations.
*/
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
/*
* Only code on the bootclasspath is allowed to get at the
* Unsafe instance.
*/
ClassLoader calling = (caller == null) ? null : caller.getClassLoader();
if ((calling != null) && (calling != Unsafe.class.getClassLoader())) {
throw new SecurityException("Unsafe access denied");
}

return THE_ONE;
}

native pointer fields

接下来通过 unsafe 访问一些 fields ,基本步骤是 objectFieldOffset -> getLong(obj, offset) 。

1
2
3
4
5
6
7
8
methodOffset = unsafe.objectFieldOffset(Helper.Executable.class.getDeclaredField("artMethod"));
classOffset = unsafe.objectFieldOffset(Helper.Executable.class.getDeclaredField("declaringClass"));
artOffset = unsafe.objectFieldOffset(Helper.MethodHandle.class.getDeclaredField("artFieldOrMethod"));
infoOffset = unsafe.objectFieldOffset(Helper.MethodHandleImpl.class.getDeclaredField("info"));
methodsOffset = unsafe.objectFieldOffset(Helper.Class.class.getDeclaredField("methods"));
iFieldOffset = unsafe.objectFieldOffset(Helper.Class.class.getDeclaredField("iFields"));
sFieldOffset = unsafe.objectFieldOffset(Helper.Class.class.getDeclaredField("sFields"));
memberOffset = unsafe.objectFieldOffset(Helper.HandleInfo.class.getDeclaredField("member"));

Helper 定义了一系列和 android 中 Class, Executable 等类的 field 完全一致的类。因为这几个 fields 都是隐藏 api ,不能直接访问,但可以利用相同布局 offset 一致的特性,通过 Unsafe 访问。

1
2
3
4
5
Accessing hidden field Ljava/lang/reflect/Executable;->declaringClass:Ljava/lang/Class; (greylist-max-o, reflection, denied)
Accessing hidden field Ljava/lang/invoke/MethodHandle;->artFieldOrMethod:J (greylist-max-o, reflection, denied)
Accessing hidden field Ljava/lang/Class;->methods:J (greylist-max-o, reflection, denied)
Accessing hidden field Ljava/lang/Class;->iFields:J (greylist-max-o, reflection, denied)
Accessing hidden field Ljava/lang/Class;->sFields:J (greylist-max-o, reflection, denied)

计算 method 和 field 数组的 bias

Class.methods 指向一个长度前缀的数组,由于有内存对齐,我们还是要计算第一个元素的偏移(bias)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Method mA = Helper.NeverCall.class.getDeclaredMethod("a");
Method mB = Helper.NeverCall.class.getDeclaredMethod("b");
mA.setAccessible(true);
mB.setAccessible(true);
MethodHandle mhA = MethodHandles.lookup().unreflect(mA);
MethodHandle mhB = MethodHandles.lookup().unreflect(mB);
long aAddr = unsafe.getLong(mhA, artOffset);
long bAddr = unsafe.getLong(mhB, artOffset);
long aMethods = unsafe.getLong(Helper.NeverCall.class, methodsOffset);
artMethodSize = bAddr - aAddr;
if (BuildConfig.DEBUG) Log.v(TAG, artMethodSize + " " +
Long.toString(aAddr, 16) + ", " +
Long.toString(bAddr, 16) + ", " +
Long.toString(aMethods, 16));
artMethodBias = aAddr - aMethods - artMethodSize;

减去一个 artMethodSize 是因为有一个 constructor

TODO: 方法在 methods 中的排列顺序

在这里没有取 Executable.artMethod 而是用了 MethodHandle.artFieldOrMethod ,理论上两个值是一样的。不过 Field 没有 artField 这样的 field ,只有 getArtField 方法,这个方法也是隐藏的,因此 Field 只能用 MethodHandle 获取 ArtField 的地址了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        Field fI = Helper.NeverCall.class.getDeclaredField("i");
Field fJ = Helper.NeverCall.class.getDeclaredField("j");
fI.setAccessible(true);
fJ.setAccessible(true);
MethodHandle mhI = MethodHandles.lookup().unreflectGetter(fI);
MethodHandle mhJ = MethodHandles.lookup().unreflectGetter(fJ);
long iAddr = unsafe.getLong(mhI, artOffset);
long jAddr = unsafe.getLong(mhJ, artOffset);
long iFields = unsafe.getLong(Helper.NeverCall.class, iFieldOffset);
artFieldSize = jAddr - iAddr;
if (BuildConfig.DEBUG) Log.v(TAG, artFieldSize + " " +
Long.toString(iAddr, 16) + ", " +
Long.toString(jAddr, 16) + ", " +
Long.toString(iFields, 16));
artFieldBias = iAddr - iFields;
} catch (ReflectiveOperationException e) {
Log.e(TAG, "Initialize error", e);
throw new ExceptionInInitializerError(e);
}
}

getDeclared

利用上面计算的 ArtMethod 、ArtField 的大小和首个元素在带前缀数组的偏移,我们就可以遍历 Class 对象的 methods, iFields, sFields 获取 Method 和 Field 了。

getDeclaredMethods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@NonNull
public static List<Executable> getDeclaredMethods(@NonNull Class<?> clazz) {
ArrayList<Executable> list = new ArrayList<>();
if (clazz.isPrimitive() || clazz.isArray()) return list;
MethodHandle mh;
try {
Method mA = Helper.NeverCall.class.getDeclaredMethod("a");
mA.setAccessible(true);
mh = MethodHandles.lookup().unreflect(mA);
} catch (NoSuchMethodException | IllegalAccessException e) {
return list;
}
long methods = unsafe.getLong(clazz, methodsOffset);
if (methods == 0) return list;
int numMethods = unsafe.getInt(methods);
if (BuildConfig.DEBUG) Log.d(TAG, clazz + " has " + numMethods + " methods");
for (int i = 0; i < numMethods; i++) {
long method = methods + i * artMethodSize + artMethodBias;
unsafe.putLong(mh, artOffset, method);
unsafe.putObject(mh, infoOffset, null);
try {
MethodHandles.lookup().revealDirect(mh);
} catch (Throwable ignored) {
}
MethodHandleInfo info = (MethodHandleInfo) unsafe.getObject(mh, infoOffset);
Executable member = (Executable) unsafe.getObject(info, memberOffset);
if (BuildConfig.DEBUG)
Log.v(TAG, "got " + clazz.getTypeName() + "." + member.getName() +
"(" + Arrays.stream(member.getParameterTypes()).map(Type::getTypeName).collect(Collectors.joining()) + ")");
list.add(member);
}
return list;
}

首先随便获取一个方法的 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
2
3
4
5
6
7
8
9
public static class InvokeStub {
private static Object invoke(Object... args) {
throw new IllegalStateException("Failed to invoke the method");
}

private InvokeStub(Object... args) {
throw new IllegalStateException("Failed to new a instance");
}
}

当然用 getDeclaredMethod 得到的 Method 对象也是可以的。

进程内全局解除 API 限制

有了 invoke 后,调用 dalvik.system.VMRuntime.setHiddenApiExemptions 即可。

其他方法

JNI_OnLoad

JNI_OnLoad 的时候,实际上没有任何隐藏 API 限制!

这是因为 JNI 的调用检查的依据是 Java 调用堆栈的 caller class 。当 JNI_OnLoad 被调用的时候,Caller 是 java.lang.Runtime ,这是个系统类,显然可以得到豁免。

一般情况下,调用 JNI 方法的 caller 是自己,因此就无法豁免了。

jni_internal.cc GetJniAccessContext

1
2
3
4
5
6
7
8
9
static hiddenapi::AccessContext GetJniAccessContext(Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
// Construct AccessContext from the first calling class on stack.
// If the calling class cannot be determined, e.g. unattached threads,
// we conservatively assume the caller is trusted.
ObjPtr<mirror::Class> caller = GetCallingClass(self, /* num_frames= */ 1);
return caller.IsNull() ? hiddenapi::AccessContext(/* is_trusted= */ true)
: hiddenapi::AccessContext(caller);
}

其实这和以前的(失效的)元反射类似,都是 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
2
3
adb shell settings put global hidden_api_policy  1
adb shell settings put global hidden_api_policy_pre_p_apps 1
adb shell settings put global hidden_api_policy_p_apps 1

还原:

1
2
3
adb shell settings delete global hidden_api_policy
adb shell settings delete global hidden_api_policy_pre_p_apps
adb shell settings delete global hidden_api_policy_p_apps

实质上是让系统服务创建进程的时候改变 runtimeFlags ,关闭隐藏 API 限制。

DexFile.setTrusted

LSPlant 的 MakeDexFileTrusted 可以给某个 dex 解除隐藏 api 限制,其实现是调用 art 内部函数 DexFile::setTrusted 。LSPosed 利用这个方法(1, 2)给自己和模块的 dex 解除限制,因此模块可以正常使用隐藏的 api 。

总结

LSPass 应该是目前功能最为完备的隐藏 api 限制绕过工具了,不仅可以在进程内全局解除 api 限制,也可以在不解除 api 限制的情况下使用隐藏 api ,避免对应用产生影响(LSPatch 的例子)。

参考

Android运行时ART加载类和方法的过程分析 · 老罗的Android之旅(总结) · 看云

https://github.com/LSPosed/AndroidHiddenApiBypass

https://github.com/LSPosed/AndroidHiddenApiBypass/blob/545cd590af5990e66c5ec1fee27839f4c0391b2d/library/src/main/java/org/lsposed/hiddenapibypass/HiddenApiBypass.java#L69

https://lovesykun.cn/archives/android-hidden-api-bypass.html#fn:5

ART虚拟机 | Java对象和类的内存结构 - 掘金