前言
公司内部APM上报了很多关于SP的ANR问题,因此由于迁移SP到DataStore的时间与测试资源有限,只能采用反射的方式将QueuedWork#waitToFinish中的任务队列清空来绕过阻塞等待。但在Android9+的版本,系统限制了系统api反射,由于android9+的设备已经是常规机型了,这样要使用一些非常规的手段去突破这个限制。
系统是怎么限制的?
image.png
当我们使用getDeclaredField()方法的时候,AS会提示我们反射已被禁用。这里有个注意点是如果我们是debug模式的话,系统是允许我们通过反射调试的,但是当我们在release模式的话,就会抛NoMethodExeception。 那么我们通过getDeclaredMethod()方法看一下系统是如何禁用的。
Class.java # getDeclaredMethod()
Method result = recursivePublicMethods ? getPublicMethodRecursive(name, parameterTypes): getDeclaredMethodInternal(name, parameterTypes);
如果recursivePublicMethods=false则调用getDeclaredMethodInternal()
/**
* Returns the method if it is defined by this class; {@code null} otherwise. This may * return a
* non-public member.
*
* @param name the method name
* @param args the method's parameter types
*/
@FastNative
private native Method getDeclaredMethodInternal(String name, Class<?>[] args);
这个方法是一个native方法,那么我们去系统源码全局搜索一下,还是比较好找到位置的,C层的代码是 java_lang_Class.cc.
红框里边ShouldDenyAccessToMember()如果返回了true,那么直接return一个null,这样外部就会抛noSuchMethodException.
hidden_api.cc # ShouldDenyAccessToMember()
在这个方法中,我们看到第一个if 如果判断条件为true,那么直接返回一个false给到调用方,那么反射的方法就直接可用了,那么我们是不是可以直接hook这个判断条件呢?答案是肯定的。 先看下GetHiddenApiExemptions()这个方法,它在runtime.h中。
从方法上来看,首先是set() get(), 然后见名知意是豁免的意思。哈哈由于提供了set get 方法岂不是美滋滋,可以随意设置了,但是set方法中传入string的数组,那么应该传入什么呢? 我们退到调用方看下DoesPrefixMatchAny()这个方法。
这里仅仅将String数组进行了遍历,真正的判断是使用DoesPrefixMatch()方法,我们再看下。
看见这部分的代码逻辑是对Java字节码类型的签名前缀进行判断是否匹配。compare函数当值为0的时候说明两个字符串相同。 那么这个String要设置成什么呢?我们知道类型签名在Java中一直是‘L’开头的,哈哈 我们直接设置一个 前缀为L的字符串不就行了。
如何Hook豁免函数?
inlink hook 方法
文档
https://weishu.me/2018/06/07/free-reflection-above-android-p/
开源项目
https://github.com/tiann/FreeReflection
元反射方法
文档
https://weishu.me/2019/03/16/another-free-reflection-above-android-p/
开源项目
https://github.com/tiann/FreeReflection
Unsafe内存探索法
文档
https://lovesykun.cn/archives/android-hidden-api-bypass.html
开源项目
https://github.com/LSPosed/AndroidHiddenApiBypass
Tip
方案中使用了MethodHandles去获取对象,MethodHandles它比单纯的反射性能高出很多。
https://zhuanlan.zhihu.com/p/524591401?utm_id=0
总结
相对代码量少的方式是采用元反射+使用顶层classloader的方式。
设置类的 classloader 为 null 成为 BootClassPath 中的类以解除限制。此法看似可行,然并非万全:
首先在有隐藏 API 限制的情况下修改自己的 classloader 非常困难(但是仍可行),类是否有隐藏 API 限制是由其在加载时候就设置好的 Domain 决定,所以在加载类之后再修改 classloader 就没有用了。
利用 DexFile 加载一个没有 classloader 的类可以(甚至可以通过 base64 加载一个预制在 java 字符串中的 dex 文件),但是 DexFile 已经 deprecated 掉,并且在不日加入隐藏 API 列表。谷歌方面已经开始着手不信任 classloader 为 null 的类了。
稳定的方案是使用unsafe类去操作内存,对方法句柄进行内存地址的修改,以达成目的。
综合考虑,选择Unsafe方式去Hook豁免函数,来跳过对系统****api****的拦截检测。
问题延展
在Android 11 + ,元反射(套娃反射)是如何被禁掉的?
https://blog.csdn.net/mldxs/article/details/129192644
https://blog.csdn.net/yudan505/article/details/125208466
因为增加了调用者上下文判断机制,机制给调用者跟被调用的方法增加了domian,调用者domain要等于或者小于 被调用的方法domian,才能允许访问。
元反射被禁了,为什么使用DexFile.loadClass()就可以解决?
在 DexFile 的 loadClass 方法中,如果你将 null 传递给 classLoader 参数,会使用默认的系统类加载器(即 ClassLoader.getSystemClassLoader())来加载类。这意味着加载的类将使用默认的类加载器进行加载。
例如,假设你有一个名为 MyClass 的类,你可以这样使用 DexFile 的 loadClass 方法:
private static final String DEX = "dex file by base64";
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();
}
}
}
在这个例子中,null 被传递给了 classLoader 参数,因此会使用默认的系统类加载器加载 MyClass 类。这个行为与在 Java 中直接使用类名加载类时的行为类似。
然而,需要注意的是,使用默认的系统类加载器来加载类可能会导致一些限制和问题,特别是在 Android 的上下文中。在 Android 应用中,通常推荐使用当前应用的类加载器,即通过 getClassLoader() 方法获取的类加载器,来加载类。这样可以确保类的加载在应用的上下文中进行,避免不必要的问题。
这样就满足第一点的<u>调用者domain要等于被调用的方法domian。</u>
单.java or .kt 文件如何直接转.dex?
首先build工程,获取到.class 文件。
dx --dex --output=xxx.dex xxx.class
// 注意!:Class文件的路径要包含包名路径,也就是com.example.viewdemo,不然转换时会报包名不匹配。
dx --dex --output=test.dex /Users/edisonli/Desktop/dapp-android-component/hidden_reflect/build/intermediates/javac/debug/classes/com/deliverysdk/hiddenapibypass/HiddenApiBypass.class
-
Unsafe顾明之意是不安全的类,那么为什么Java很多并发类中使用了Unsafe的CAS等操作?如果说不安全为什么系统底层会去使用呢?
"Unsafe" 是 Java 中一个被标记为不安全的类,但它被用于一些系统底层操作和高性能并发编程。尽管名字含有 "不安全",但这不仅仅是指潜在的安全风险,更多的是指它对于不受限制地访问内存和执行底层操作的能力。
Java 的设计哲学之一是提供一个相对安全的环境,以避免潜在的内存错误、并发问题和安全漏洞。然而,某些情况下,为了实现更高性能、更精确的控制和底层的操作,必须放宽一些限制,这就是 "Unsafe" 类的作用所在。
在并发编程中,
Unsafe类的一些底层操作(如 CAS 操作)可以实现非阻塞的线程安全算法,从而避免传统锁所带来的性能开销。虽然这些操作可以用来构建高性能的并发数据结构,但需要小心使用,因为错误的使用可能导致内存问题和难以调试的 Bug。
为什么系统底层会使用 Unsafe 类呢?
性能优势: Unsafe 允许直接访问内存,执行原子操作(如 CAS)等,这些操作在某些情况下可以比传统的 Java 并发库提供更高的性能。这在一些对性能要求非常高的场景中很有价值。
* **细粒度控制:** `Unsafe` 允许开发者直接操作内存和底层硬件特性,这对于实现一些复杂的算法和数据结构非常有用,但也需要小心处理。
* **底层库支持:** 一些底层库和框架可能需要直接与操作系统或硬件进行交互,使用 `Unsafe` 可以方便地实现这些功能。
需要强调的是,使用 `Unsafe` 需要高度的技术责任感,因为它允许执行不受限制的操作,可能会导致各种问题,包括内存泄漏、线程安全性问题和崩溃。大多数情况下,不需要直接使用 `Unsafe`,而是可以利用 Java 并发库提供的高级抽象来处理并发问题。只有在深刻理解 `Unsafe` 的工作原理和风险的情况下,才应该在特定场景中使用它。
Unsafe类在Android中,应用层无法使用,那么有什么办法可以调用Unsafe中的方法吗?
类加载的双亲委派模型。
Google Play 会扫描安装包来探查是否使用了unsafe?或者说我们规避反射黑名单,会被市场下掉吗?
谷歌方面已经承诺 Unsafe 不会被彻底隐藏,而剩下使用的都是 Java 公开接口,更不可能被隐藏。依赖的 mirror 类的 offset 都被谷歌小心翼翼地维护,改动可能性也不大。因而可以说方法稳定且通用,并且大概率不会被谷歌后续掐掉。
ART 的模型是 Java 代码中的重要类和 native 代码中的一个类相互镜像:即共享同一块内存。这句话怎么理解?
Android Runtime (ART) 的模型允许 Java 代码中的重要类(Java 类)和 native 代码中的一个类(C/C++ 类)在内存中互相镜像,也就是说它们可以在内存中共享同一块数据区域。
具体来说,这个概念是 ART 在运行时的一种优化策略。在传统的 Dalvik 虚拟机中,Java 类和 native 类在内存中是分开存储的,它们各自有自己的数据结构和内存分配。这可能导致一些性能和内存开销,因为 Java 代码和 native 代码可能需要频繁地进行数据转换和拷贝。
而在 ART 中,这种隔离被部分打破,一些 Java 类和对应的 native 类可以在内存中共享同一块数据区域。这样做的好处在于:
1. 性能优化: 通过共享数据区域,可以减少 Java 代码和 native 代码之间的数据转换和拷贝,从而提高执行效率。
2. 内存节省: 由于共享数据区域,一些数据结构不需要在内存中重复存储,从而减少了内存占用。
3. 更紧密的集成: Java 代码和 native 代码之间的数据共享可以使跨语言开发更加紧密和高效。
这种内存共享模型需要一些底层的技术支持,如内存布局的设计和内存访问的处理。它通常适用于某些特定的场景,比如 Android 应用中的某些关键类(如 Android 系统库中的一些类)以及与底层系统交互的 native 类。并不是所有的 Java 和 native 类都会在内存中共享同一块数据区域,这取决于系统的实现和优化策略。











网友评论