之前小缘在群里问过这个问题:
先不考虑其实际的使用价值,单纯从技术角度思考,其实也有场景能用上,暂时不表。
我细化一下问题:
前提类:
public class A {
private final AInner inner = new AInner();
private final class AInner {
public void b() {
Log.d("tec-hack", "AInner b invoke");
}
}
}
注意,暂且认为该类是系统类,我们无法在编译期对齐修改。
问题来了:
- 如何构造一个AInner的子类对象?
- 完成对A的inner成员变量替换。
更多问答 >>
-
每日一问 | 可以不借助 bindService,实现跨进程 binder 通信吗?
2022-04-27 23:43 -
2022-05-06 11:37
-
每日一问 | Service onStartCommand 返回STICKY是如何做到被拉活的?
2022-07-24 11:50 -
2022-05-27 20:55
-
每日一问 .class vs Class.forName() vs loadClass() 类加载傻傻分不清楚?
2022-02-11 14:22 -
每日一问 | 脱糖对于Android 打包期间插桩的有什么影响?
2022-03-07 21:26 -
每日一问 | 如何构造一个 hide interface 的实现类?
2022-02-08 23:51 -
每日一问 | android hidden api 不是禁用反射,以及如何突破,「元反射」不行了?
2022-02-08 23:51 -
每日一问 UndeclaredThrowableException 是什么异常?
2021-12-02 00:50
又是我最喜欢的 runtime 题(
系统是怎么知道这个类是 final 的呢:
拿到了这个 mirror::Class 对象的一个叫做
access_flags_
的属性,然后判断它有没有kAccFinal
。可能有同学会问,这个 mirror::Class 和 java 里的 java.lang.Class 对象是个什么关系呀?事实上,java.lang.Class 和 art::mirror::Class 是同一个对象, art::mirror::Class 就是 java.lang.Class 在 c++ 里的反映。如果你有打开过 Class.java 这个文件看一下,就会发现里有很多成员变量,其中就有我们刚刚提到的access_flags_
:假如我们通过反射等手段去修改 java 里面 java.lang.Class 的
因此我们可以写出如下代码:accessFlags
能不能修改到 c++ 里面的access_flags_
呢?答:可以。由于 art::mirror::Class 和 java.lang.Class 是同一个对象,因此它们的对象地址相同,而两者的成员变量布局被特意设计成完全相同的布局,因此它们的偏移量(offset)也相同,最终操作的目标地址=对象地址+成员偏移量,显然相同。经过以上操作之后,现在 art 已经不把该类看作是 final 的了,那么它能继承了吗?可别忘了,这个 class 还是 private 的,外部无法直接访问;不过还好,一个 class 是不是 public 也是存在于
经过以上操作以后,这个 class 现在可以被继承了?其实还有一个最后的问题,非 static 内部类会持有外部类的 this 引用,它其实是偷偷给你加一个带参数的构造函数实现的,我们还要进行处理;而且这个构造方法也有访问权限限制,我们还需要再把这个构造方法也改成 public 的,这涉及到修改 c++ 的accessFlags
里的,也可以照这样反射修改。ArtMethod
了,而且 java 层里这玩意的表示在 6.0 以后就被删掉了,只能通过 Unsafe/JNI 直接修改对应内存了……invoke-direct
指令调用,而虚方法使用invoke-virtual
指令调用,两者的不同就是,后者会去这个 class 的所谓虚方法表(vtable)里查找,我们的子类重写父类方法其实也就是把自己的实现填到这里面去,要调用的时候去这个表里找一下。在第一份代码中,我们要重写的方法是private final
的,显然是一个直接方法,无法重写。而第二份代码中,方法是public
的,是虚方法,可以正常重写。这里有一个有意思的问题,如果我们要重写public final
的方法,它是一个虚方法还是直接方法?我们在运行时用类似的手段把这个 method 的 final flag 去掉之后能否正常重写?答:一、 runtime 其实认为它还是虚方法,虽然文档里这样写:但实际编译成 dex 再反编译成 smali,你会发现它其实还是用的
art 实际也不会把 final 看作 direct:invoke-virtual
指令调用。只认 static/private/constructor,不认 final。至于这样做的原因是什么呢,可能是因为子类可以把父类的虚方法重写成 final 的所以无论如何都必须要在 vtable 里填一份吧。
二、能正常重写吗?答:不一定,虽然 runtime 不认为它是直接方法,但是 runtime 知道 final 的方法不可能被重写(我们特意搞事情除外),它可以做优化啊!既然这个方法只可能有一个实现,那我可以调用的时候直接硬编码它的入口,还可以把它的实现整个内联到调用者,根本不走方法查找~当然这种情况我们也有方法拦下来,下面是广告时间:推销自己的 art method hook 库:https://github.com/canyie/pine ,支持拦截 java 里除去抽象方法之外几乎所有的方法调用,可以在原方法调用前后执行自己的代码,查看或修改参数返回值,甚至可以换成自己的然后阻止原方法执行!至于它的应用场景,可以发挥你的想象力 :)
setAccessible(true)
一下就会直接跳过检查,然后就可以尽情玩耍 :)呜呼,终于打完了,顺便推一首刚刚发现的超好听的歌:https://www.bilibili.com/video/BV12Z4y1Z7na ,睡觉去(
么么哒~感谢回答
补充:
@残页 同学提到,java class的accessFlags
标识可以在运行时通过反射修改。也就是private final的class可以改成public,所以只要能弄到一个编译好的子类class,在加载之前把父类的accessFlags
修改一下就能在题目中第二份代码里正常使用了。具体要怎么做呢? 有两种做法:一种是通过stub的方式(弄一个空实现的类,把private final改成public),让它顺利通过编译;第二种是在运行时通过dexmaker动态生成;有同学可能会说:我不熟dexmaker啊。知道,所以我早就准备好了,这个IDEA插件:DMifier,可以直接由java源码转换成对应的dexmaker代码,非常方便,不用自己动手写。好,现在就来生成一个AInner的子类吧:我们创建的SubAInner并没有显式继承任何一个类,所以等下生成的dexmaker代码默认会是
当然了,如果还需要调用父类的b方法,又不想自己动手写的话,也可以先随便调用一个父类的方法来占坑,在生成dexmaker代码后手动改一下方法名(注意返回类型和参数要一致)即可,像这样:java/lang/Object
,手动把它改成A$AInner
就行。调用
好,看看生成的代码:super.notify
方法来占坑。我们把第五行的
再把"Ljava/lang/Object;"
改成"Ltest/A$AInner;"
(注意前面的test/是我本地这个A类所在包名),让它继承自A$AInner
;"notify"
改成"b"
,和"notify"
前面的"Ljava/lang/Object;"
改成"Ltest/A$AInner;"
,这样调用的就是父类A$AInner
的b
方法了。最后把生成的子类加载出来:还可以看下生成的dex转成class之后的代码:
好了,现在class是有了,可要怎么实例化呢?
肯定不是一个getConstructor().newInstance()
那么简单,别忘了,非静态内部类在编译时自动生成的构造方法,参数是会加上它的外部类的。javap来看一下:其实也只是对
this$0
(外部类的实例)进行赋值罢了。对于这种情况,我们有两种选择:
一种是使用反射创建实例后,再通过反射直接对this$0
进行赋值。但这种方式不适合那些显式声明构造方法并有自己的逻辑的场景;另一种做法,就是模拟常规对象创建流程,即像继承了普通类那样,在构造方法里去调用父类的构造方法,这样创建出来的实例就跟普通类没什么两样了;但结合题目中给出的代码,貌似第二种做法无法实行?因为编译器生成的构造方法修饰符都是跟随类的,刚刚javap打印出来的字节码可以看到,自动生成的A$AInner构造方法,就是private的。这样一来,即使我们在dexmaker代码中加上同样参数的构造方法,并在里面调用super,也调用不了啊!因为父类构造方法的accessFlags
还是private,而你又改不了。有同学可能说:用反射调用父类构造方法可以吗?你怎么调用?你通过getConstructor
获取到的Constructor,只能newInstance
,返回的是这个Constructor所属类的实例。又不能像Method那样,有提供invoke
方法可以传指定的实例进去。咦?等等!或许……还真的可以?Constructor和Method都是Executable的子类,而Executable这个类…… 如果你有看过前段时间的一个 hidden api 的问题,你应该会对 @yujincheng08 同学提出的在java层通过unsafe移花接木来调用hidden api的操作印象深刻:Executable里有个成员变量artMethod ,存的是native层ArtMethod指针(Constructor本质上也是Method)。在Class里面有个methods
,对应native层的methods_ ,存的是LengthPrefixedArray<ArtMethod>,而LengthPrefixedArray中第一个成员size_ 记录了这个数组的长度,即元素ArtMethod的数量(对应java层Constructor+Method的个数)。yujincheng08 同学的做法是:通过unsafe获取目标Class对应的LengthPrefixedArray的size_
,再根据size_
地址 + 偏移量得到各个ArtMethod的指针地址,然后把得到的这些指针地址set进Method(Executable)里面,这样就等于直接拿到了目标类的所有Method(自然也包括hidden)的引用了。我们这里同样也可以参考他的做法,把Constructor转成普通Method来调用!其实字节码里调用构造方法也是这样的逻辑:
看吧,跟调用private方法没区别。
至于子类实例,我们可以通过unsafe.allocateInstance
来创建;然后再调用转换成Method的父类Constructor,invoke
的时候就传这个子类的实例进去!这样就能得到一个完美的子类实例!还有一个,因为我们要转换的方法不是hidden的,能直接通过反射来获取到目标方法的
artMethod
,所以呢,通过unsafe枚举ArtMethod这一步可以免了! 甚至连替换artMethod
这一步也不需要unsafe了,直接通过反射来设置就行!动手试试看吧,先封装一个
newInstance
方法:为了更直观地看到效果,我们在A类中加上一个
innerB
方法,在替换inner
实例前后分别调用:运行看下:
成功替换并运行正常~
这是父类b
方法修饰符为public的情况下。如果把它改成private final呢?当你修改后运行会发现:即使已成功替换实例,但调用的innerB
最终并没有走子类的b
方法,而是跟替换之前一样,走的是内部类AInner的b
方法。至于为什么,@残页 同学已经讲的很详细了:私有方法在编译成dex时用的指令是invoke-direct
,方法入口是固定的(之前一个关于unsafe的问题 也有相关解析,只不过是在Java环境上)。就算使用反射,在查找方法时也有对应的处理:emmmm……现在看来,貌似除了用hook,也没有其他办法了。
等等!最近偶然发现了一个做法,在Android11上,只需要替换几个变量,就能实现hook功能!而且全程在java层操作,非常简单!
先来看一样东西:dex_method_index_
的注释,说它就是对应dex文件method_ids
里面的索引!有了解过dex文件格式的同学会知道,dex文件里面有个method_ids
区,保存的是dex中所有方法的索引(当然都是唯一的,废话)。那如果我们通过某些手段,把这个索引改掉的话…… 哈哈哈哈哈是的,把这个dex_method_index_
改为其他ArtMethod对应的值,在调用时,就变成了那个方法了!至于要怎么改,这里也不卖关子了:
上次hidden api那个问题,@yujincheng08 同学给了我启示,原来在Method里还存有个native层ArtMethod的指针。既然能拿到它的指针,那就等于能对上面这些变量随意访问和修改了,只需知道offset就行。可以看到dex_method_index_
前面还有三个变量,后两个好算,uint32_t占4字节,至于第一个declaring_class_
的大小,就需要sizeof
才敢确定了。不过,YAHFA这个库早已帮我们算好,也是4字节!declaring_class_
=artMethod
+ 0access_flags_
=artMethod
+ 4dex_code_item_offset_
=artMethod
+ 8dex_method_index_
=artMethod
+ 12我们可以通过unsafe的
动手试一下:getLong
方法把这些值读出来,通过putLong
set回去。好,运行试试:
b
方法,是因为原来的b
方法已经被替换了。所以能看到这个错说明已经替换成功了。我们去修改一下子类的b
方法逻辑,把invokeSuper
那一行注释掉,重新运行:睡觉睡觉,好冷啊!
牛批
> 因为父类构造方法的accessFlags还是private,而你又改不了。 这里可以修改 ArtMethod 里的 access_flags_,把 kAccPrivate 去掉然后加上 kA ...查看更多
> 因为父类构造方法的accessFlags还是private,而你又改不了。 这里可以修改 ArtMethod 里的 access_flags_,把 kAccPrivate 去掉然后加上 kAccPublic 或者 kAccProtected > 是的,把这个dex_method_index_改为其他ArtMethod对应的值,在调用时,就变成了那个方法了! 这种方法只对解释执行有用,如果要替换的方法已经被编译过,运行时已经有编译好的代码入口就会直接跳过去而不会再用到 `dex_method_index_` 。
👍🏻学到了,怪不得对系统方法不起作用,原来是这个原因,看来还是不能偷懒啊。。。
那如果在方法被编译之前替换,应该就没问题了吧?对了,我记得YAHFA里有对accessflag设置不可被编译的标记,那是否能通过这个标记来控制呢?
被编译之前替换应该没问题,但是没法保证足够早替换,而且可能有其他问题;DontCompile 那个 flag 只在运行时对 jit 有效,没法阻止 AOT。
👍🏻学到了!感谢解惑!
大佬为何如此优秀
第二份代码有个对应的场景,即Hook ViewRootImpl.TraversalRunnable的public void run方法,如果只是为了hook run方法的话其实有个更简单的方案,不需要修改Modifier,也不需要dexmaker
这操作真是逆天了 ,666
占位学习
学习新姿势
不仔细看还以为你是吴亦凡。。。
坐等大佬
占位学知识
同等