首页
社区
课程
招聘
[原创] 一个通用的纯 Java 安卓隐藏 API 限制绕过方案
发表于: 2021-4-24 20:49 13565

[原创] 一个通用的纯 Java 安卓隐藏 API 限制绕过方案

2021-4-24 20:49
13565

文章同时发表在我个人博客上。转载请务必注明出处。

2018年发布的 Android P 中引入了对隐藏API的限制,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制。比如 Canyie 就曾经提出了两个绕过方法,其中一个便是几乎完美的双重反射(即“元反射”,现在来看叫“套娃反射”比较好);而在即将发布的Android R中把这个方法封杀了。

这是 Canyie 在其博客上^1提到的安卓隐藏 API 的问题。博客上还提出了一个看似可行的解决方案:设置类的 classloadernull 成为 BootClassPath 中的类以解除限制。此法看似可行,然并非万全:

这样看来,纯 Java 绕过隐藏 API 似乎没戏了。只能通过 native 代码,用魔法绕过 dlopen 限制来 dlsymlibart.so 中的 _ZN3artL32VMRuntime_setHiddenApiExemptionsEP7_JNIEnvP7_jclassP13_jobjectArray 来设置允许隐藏 API 了。

等等。

我们好像还有一个非常适合玩魔法的 Java 自带的 API:Unsafe!
这个魔法类顾名思义,是非常不安全的:它可以纯 Java 读写内存!也就是说,有了这个东西,我们就可以读写类中的任意数据成员了!甚至如果拿到 native 指针,还能直接读指针指向的内存内容。那么我们是不是可以藉此来读取类的隐藏函数?答案是肯定的。

但是,ART 中的函数不是存在一个 Java 数组中乖乖等着给你拿的。ART 的模型是 Java 代码中的重要类和 native 代码中的一个类相互 mirror:即共享同一块内存。所以在 Java 中读写这些 Java 类对象的成员相当于同时修改对应的 native 对象成员。一些常见的类就是 ClassMethodField 等。于是为了方便 native 代码访问这些对象,这些类的成员很多都不是以 Java 对象形式存在,而是以 native 指针形式存在。比如一个 Class 对应的 Java 结构如下^3

可以看到有很多 long 成员,这些就是 native 指针了。很可惜,我们想要的 methods 就是以指针形式存在的,而且指针对应的是 ArtMethod 的 native 对象,并非一个对应到 Java 的 mirror 的 Method。感兴趣的小伙伴可以看看 Executable 的实现,其中有一个 long 成员是 artMethod 对应的就是这个东西。

那么我们需要把这个 methods 指针对应的 artMethod 给一个一个枚举出来,并且想办法把他们转换为 Java 可以用的 Executable (用 Executable 是这个包含 ConstructorMethod)。

要理解这个指针到底存了啥,我们就要回到 native 代码^4

这里解释它是一个 length-prefixed array 对象。实际上我们看使用它的地方:

可以看到,它强转成一个模板类 LengthPrefixedArray 的指针,其部分定义如下:

很明显,“类”如其名,就是一个数组,但是前面塞了一个 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)

拿到数组首个元素的地址 base,接下来第 i 个元素就是 base + i * size 啦。但是问题来了,怎么计算 ArtMethod 的大小呢?这个直接参考 SandHook 的实现就可以了:直接在 Java 定义两个连续的方法,然后指针相减就是了^5

这时候就涉及第二个问题:怎么从 Java 的 Method 转换成 ArtMethod 指针?反过来又如何?

我们之前提到过,Executable 里面放了一个 ArtMethod 的指针。既然映射到 Java 就证明它有被使用的地方,我们看看它的源码:

非常明确地说明给 java.lang.invoke.* 接口使用的。这很好,因为这些接口都是 Java 原生地公开接口,谷歌可不能隐藏他们。那么怎么被使用的呢?或者说,怎么被转换的呢?

原来是放在了 MethodHadle 上。而且这个东西也是一个 mirror 类,native 和 Java 一对一的。也就是说,只要把一个 Method 转换成 MethodHandle 然后读出这个 artFieldOrMethod 就可以了。

然后就是转回来。我们把某个 MethodHandle 的这个 artFieldOrMethod 设置成 ArtMethod 指针之后,再把这个 MethodHandle 转成 Executable 就行了。要注意,转换成 Member 时候,这个接口会检查解析出来的 Member 是否能被当前类访问。 不过好在,他得先转换出来才能进行检查。那么,只要忽略这个检查(抛出的异常),然后直接拿出这个成员就行了。

这个方法在 P-S DP2 上均进行了测试,都能完美运行。并且,谷歌方面已经承诺 Unsafe 不会被彻底隐藏,而剩下使用的都是 Java 公开接口,更不可能被隐藏。依赖的 mirror 类的 offset 都被谷歌小心翼翼地维护,改动可能性也不大。因而可以说方法稳定切通用,并且大概率不会被谷歌后续掐掉。

源码已经公开,并且已经上架 Maven,欢迎推广和使用。

 
 
 
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;
    //还有其他成员,这里就写了
}
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;
    //还有其他成员,这里就写了
}
 
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_;
};
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.

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-4-24 20:52 被yujincheng08编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (11)
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不懂
2021-4-25 00:32
0
雪    币: 13
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
没有验证,感觉思路很厉害
2021-4-25 10:15
0
雪    币: 1867
活跃值: (4073)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
4
老板们对新版本的Android研究得挺即时的啊
2021-4-25 14:14
0
雪    币: 477
活跃值: (1412)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
这个么
https://zhuanlan.zhihu.com/p/364884283
2021-4-25 14:31
0
雪    币: 208
活跃值: (387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mb_foyotena 这个么 https://zhuanlan.zhihu.com/p/364884283
很明显是参考了我的方法(发贴日期也在我博客后),但是却写死了 offset。虽然 offset 写死问题不大,但是其搜索 artmethod 大小不稳。实际上随便选两个必然相连的方法就行。
2021-4-25 19:07
0
雪    币: 477
活跃值: (1412)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7

   

最后于 2021-4-26 14:18 被mb_foyotena编辑 ,原因:
2021-4-26 13:55
0
雪    币: 0
活跃值: (21)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
好文要顶
2021-4-26 14:50
0
雪    币: 122
活跃值: (536)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
很牛逼的思路
2021-4-26 18:22
0
雪    币: 2239
活跃值: (1109)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
收藏就是学到 mark
2021-4-27 10:33
0
雪    币: 2089
活跃值: (3933)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
太麻烦,没必要
用DexFile类加载的任何dex文件,只要父ClassLoader为空,则会认为来自BootClassLoader加载
可以绕过任何反射限制,比这个方法简单得多
2021-4-29 15:00
0
雪    币: 208
活跃值: (387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
lhxdiao 太麻烦,没必要 用DexFile类加载的任何dex文件,只要父ClassLoader为空,则会认为来自BootClassLoader加载 可以绕过任何反射限制,比这个方法简单得多

你真的有认真看我的文章吗?你看看我旧方法小节的第三点?谷歌开始不再信任 classloader 为 null 的类了:https://android-review.googlesource.com/c/platform/art/+/1664304。而 dexfile 也要被加入隐藏 api 列表中:https://android-review.googlesource.com/c/platform/libcore/+/1666599。甚至pthread 开新线程也即将被堵。

最后于 2021-4-29 15:22 被yujincheng08编辑 ,原因:
2021-4-29 15:19
0
游客
登录 | 注册 方可回帖
返回
//