1. 从“套娃”到“伪装”:Android 11+的攻防升级
如果你在Android 10或更早的版本里玩过反射调用@hide的API,那你大概率听说过或者用过“套娃反射”这个技巧。简单来说,就是让系统类(比如java.lang.Class)去帮你反射,因为系统类自己调用反射API是不会被检查的。这招在Android 10及之前版本里非常好用,感觉像是找到了一个系统后门。
但好景不长,从Android 11开始,Google把这个“后门”给焊死了。系统在检查调用者时变得更聪明了,它会沿着调用栈一层层往上爬,看看最开始的调用者到底是谁。如果你的调用栈里出现了java.lang.reflect包下的类(也就是“套娃”的痕迹),系统会直接忽略它,继续往上找,直到找到你的应用代码为止,然后无情地拒绝你的访问。这就是为什么你在Android 11/12上再用老方法会直接抛NoSuchMethodException。
那么,路被堵死了,我们是不是就没办法了?当然不是。系统的检查依赖于调用栈这个“证据链”,那我们的新思路就很明确了:伪造证据链。既然系统通过调用栈来判断调用者身份,那我们就想办法在调用栈上动手脚,让系统误以为这次调用来自一个“合法”的地方,比如系统本身。这就是“JNI线程栈伪装技术”的核心思想。我试过不少方案,这个思路在Android 11和12上实测下来非常稳,可以说是目前绕过@hide限制最优雅、最可靠的方法之一。
2. 深入虎穴:Art虚拟机的调用栈检查机制
要成功伪装,我们得先搞清楚“保安”(Art虚拟机)是怎么查岗的。这需要我们深入到Native层的源码去看一看。关键的函数是ShouldDenyAccessToMember,它决定了是否拒绝你对某个类成员的访问。
当你在Java层调用Class.getDeclaredMethod时,最终会走到Native层的Class_getDeclaredMethodInternal函数。在这个函数里,它会调用GetHiddenapiAccessContextFunction(soa.Self())来获取当前的“访问上下文”(AccessContext)。这个上下文里最关键的信息就是“域”(Domain)。系统把代码分成了三个信任等级不同的域:
- kCorePlatform (0): 最核心的平台代码。
- kPlatform (1): 系统框架代码。
- kApplication (2): 普通应用代码。
检查规则很简单:低信任等级的域不能随意访问高信任等级域里的隐藏成员。你的App代码显然属于kApplication域,而@hide的API属于kPlatform域,所以默认情况下访问会被拒绝。
那么,系统是怎么知道当前调用代码属于哪个域的呢?答案就在调用栈里。GetHiddenapiAccessContextFunction函数会遍历当前的调用栈(调用VisitFrame),找出第一个不属于java.lang.reflect包的调用帧,这个调用帧所在的类,其ClassLoader就决定了它的域。系统类由Bootstrap ClassLoader加载(classloader为null),域是kPlatform;你的App类由PathClassLoader加载,域就是kApplication。
所以,Android 11封杀“套娃反射”的代码就藏在VisitFrame的逻辑里:它发现调用栈里有来自java.lang.reflect包的类时,如果系统启用了kPreventMetaReflectionBlacklistAccess这个限制,就会直接跳过这个帧,继续往上找,直到找到你的App代码为止。这就让之前的“套娃”技巧彻底失效了。
3. 金蝉脱壳:JNI线程栈伪装技术原理
理解了检查机制,我们的作战方案就清晰了。目标:让我们执行反射的那段代码,在调用栈上看起来像是来自系统域。
直接在我们的App主线程里搞伪装是不可能的,因为调用栈的起点改不了。这里就要请出JNI(Java Native Interface)了。JNI让我们可以在Native层(C/C++代码)执行操作,这给了我们更大的操作空间。我们的核心策略分为两步:
- 开辟新战场:不在App的主线程里执行反射操作,而是在JNI层创建一个全新的Native线程。这个新线程的调用栈一开始就是干净的,和我们的App主线程没有直接关联。
- 李代桃僵:仅仅创建新线程还不够,因为这个线程默认还是属于App进程的。我们需要使用
AttachCurrentThread这个

218

被折叠的 条评论
为什么被折叠?



