2018年发布的 Android P 中引入了对隐藏API的限制,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制。比如 Canyie 就曾经提出了两个绕过方法,其中一个便是几乎完美的双重反射(即“元反射”,现在来看叫“套娃反射”比较好);而在即将发布的Android R中把这个方法封杀了。
这是 Canyie 在其博客上1提到的安卓隐藏 API 的问题。博客上还提出了一个看似可行的解决方案:设置类的 classloader
为 null
成为 BootClassPath
中的类以解除限制。此法看似可行,然并非万全:
classloader
非常困难(但是仍可行)classloader
就没有用了。DexFile
加载一个没有 classloader
的类可以(甚至可以通过 base64 加载一个预制在 java 字符串中的 dex 文件),但是 DexFile
已经 deprecated 掉,并且在不日加入豪华隐藏 API 列表2。classloader
为 null
的类了34。这样看来,纯 Java 绕过隐藏 API 似乎没戏了。只能通过 native 代码,用魔法绕过 dlopen
限制来 dlsym
在 libart.so
中的 _ZN3artL32VMRuntime_setHiddenApiExemptionsEP7_JNIEnvP7_jclassP13_jobjectArray
来设置允许隐藏 API 了。
等等。
我们好像还有一个非常适合玩魔法的 Java 自带的 API:Unsafe
!
这个魔法类顾名思义,是非常不安全的:它可以纯 Java 读写内存!也就是说,有了这个东西,我们就可以读写类中的任意数据成员了!甚至如果拿到 native 指针,还能直接读指针指向的内存内容。那么我们是不是可以藉此来读取类的隐藏函数?答案是肯定的。
但是,ART 中的函数不是存在一个 Java 数组中乖乖等着给你拿的。ART 的模型是 Java 代码中的重要类和 native 代码中的一个类相互 mirror:即共享同一块内存。所以在 Java 中读写这些 Java 类对象的成员相当于同时修改对应的 native 对象成员。一些常见的类就是 Class
、Method
、Field
等。于是为了方便 native 代码访问这些对象,这些类的成员很多都不是以 Java 对象形式存在,而是以 native 指针形式存在。比如一个 Class
对应的 Java 结构如下5:
public final class Class<T> {
private transient ClassLoader classLoader;
private transient Class<?> componentType;
private transient Object dexCache;
private transient ClassExt extData;
private transient Object[] ifTable;
private transient Class<? super T> superClass;
private transient Object vtable;
private transient long iFields;
private transient long methods;
private transient long sFields;
private transient int accessFlags;
private transient int classFlags;
//还有其他成员,这里就写了
}
可以看到有很多 long
成员,这些就是 native 指针了。很可惜,我们想要的 methods
就是以指针形式存在的,而且指针对应的是 ArtMethod
的 native 对象,并非一个对应到 Java 的 mirror 的 Method
。感兴趣的小伙伴可以看看 Executable
的实现,其中有一个 long
成员是 artMethod
对应的就是这个东西。
那么我们需要把这个 methods
指针对应的 artMethod
给一个一个枚举出来,并且想办法把他们转换为 Java 可以用的 Executable
(用 Executable
是这个包含 Constructor
和 Method
)。
ArtMethod
要理解这个指针到底存了啥,我们就要回到 native 代码6。
class Class {
// Pointer to an ArtMethod length-prefixed array. All the methods where this class is the place
// where they are logically defined. This includes all private, static, final and virtual methods
// as well as inherited default methods and miranda methods.
//
// The slice methods_ [0, virtual_methods_offset_) are the direct (static, private, init) methods
// declared by this class.
//
// The slice methods_ [virtual_methods_offset_, copied_methods_offset_) are the virtual methods
// declared by this class.
//
// The slice methods_ [copied_methods_offset_, |methods_|) are the methods that are copied from
// interfaces such as miranda or default methods. These are copied for resolution purposes as this
// class is where they are (logically) declared as far as the virtual dispatch is concerned.
//
// Note that this field is used by the native debugger as the unique identifier for the type.
uint64_t methods_;
};
这里解释它是一个 length-prefixed array
对象。实际上我们看使用它的地方:
inline LengthPrefixedArray<ArtMethod>* Class::GetMethodsPtr() {
return reinterpret_cast<LengthPrefixedArray<ArtMethod>*>(
static_cast<uintptr_t>(GetField64(OFFSET_OF_OBJECT_MEMBER(Class, methods_))));
}
可以看到,它强转成一个模板类 LengthPrefixedArray
的指针,其部分定义如下:
template<typename T>
class LengthPrefixedArray {
static size_t OffsetOfElement(size_t index,
size_t element_size = sizeof(T),
size_t alignment = alignof(T)) {
DCHECK_ALIGNED_PARAM(element_size, alignment);
return RoundUp(offsetof(LengthPrefixedArray<T>, data_), alignment) + index * element_size;
}
T& AtUnchecked(size_t index, size_t element_size, size_t alignment) {
return *reinterpret_cast<T*>(
reinterpret_cast<uintptr_t>(this) + OffsetOfElement(index, element_size, alignment));
}
uint32_t size_;
uint8_t data_[0];
};
很明显,“类”如其名,就是一个数组,但是前面塞了一个 uint32_t
的长度而已。并且看其取成员的函数 AtUnchecked
,可以看到数组首元素就是 this + sizeof(size_)
对齐到 alignof(T)
。sizeof(size_)
就是 4
,而 alignof
的话,看 ArtMethod
定义是没有 alignas
或者 pack
的属性定义,那么在 32 位下就是 4
在 64 位下就是 8
,对应 Unsafe
就是 addressSize()
了。
总结来说,首个元素地址就是 methods + unsafe.addressSize()
。而读取方法数量就是 unsafe.readInt(methods)
。
ArtMethod
大小拿到数组首个元素的地址 base
,接下来第 i
个元素就是 base + i * size
啦。但是问题来了,怎么计算 ArtMethod
的大小呢?这个直接参考 SandHook
的实现就可以了:直接在 Java 定义两个连续的方法,然后指针相减就是了7。
这时候就涉及第二个问题:怎么从 Java 的 Method
转换成 ArtMethod
指针?反过来又如何?
Method
与 ArtMethod
互转我们之前提到过,Executable
里面放了一个 ArtMethod
的指针。既然映射到 Java 就证明它有被使用的地方,我们看看它的源码:
public abstract class Executable {
/**
* @hide - exposed for use by {@code java.lang.invoke.*}.
*/
public final long getArtMethod() {
return artMethod;
}
}
非常明确地说明给 java.lang.invoke.*
接口使用的。这很好,因为这些接口都是 Java 原生地公开接口,谷歌可不能隐藏他们。那么怎么被使用的呢?或者说,怎么被转换的呢?
public abstract class MethodHandle {
//其他成员...
/** @hide */ protected final long artFieldOrMethod;
}
原来是放在了 MethodHadle
上。而且这个东西也是一个 mirror 类,native 和 Java 一对一的。也就是说,只要把一个 Method
转换成 MethodHandle
然后读出这个 artFieldOrMethod
就可以了。
然后就是转回来。我们把某个 MethodHandle
的这个 artFieldOrMethod
设置成 ArtMethod
指针之后,再把这个 MethodHandle
转成 Executable
就行了。要注意,转换成 Member
时候,这个接口会检查解析出来的 Member
是否能被当前类访问。 不过好在,他得先转换出来才能进行检查。那么,只要忽略这个检查(抛出的异常),然后直接拿出这个成员就行了。
更新:实际上如果只是为了简单调用这个函数,完全可以直接随便找一个 Method
并用 Unsafe
去设置它的 artMethod
,然后直接调用 Method.invoke
即可。因为从 Method.invoke
的 native 代码可以看到,调用过程仅使用 Method
的 artMethod
成员。
ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod();
这个方法在 P-S DP2 上均进行了测试,都能完美运行。并且,谷歌方面已经承诺 Unsafe
不会被彻底隐藏,而剩下使用的都是 Java 公开接口,更不可能被隐藏。依赖的 mirror 类的 offset 都被谷歌小心翼翼地维护,改动可能性也不大。因而可以说方法稳定且通用,并且大概率不会被谷歌后续掐掉。
源码已经公开,并且已经上架 Maven,欢迎推广和使用。
https://blog.canyie.top/2020/06/10/hiddenapi-restriction-policy-on-android-r/ ↩
https://android-review.googlesource.com/c/platform/libcore/+/1666599 ↩
https://android-review.googlesource.com/c/platform/art/+/1668945 ↩
https://android-review.googlesource.com/c/platform/art/+/1664304 ↩
https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/java/lang/Class.java ↩
https://cs.android.com/android/platform/superproject/+/master:art/runtime/mirror/class.h ↩
https://github.com/ganyao114/SandHook/blob/master/doc/doc.md#artmethod-%E7%9A%84%E5%A4%A7%E5%B0%8F ↩