Android上的热修复框架 AndFix 大家都很熟悉了,它的原理实际上很简单:

  方法替换——Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令;这样就能实现热修复,详细代码见 AndFix。需要了解Android 虚拟机的方法调用过程才能彻底理解。

众所周知,AndFix是一种 native 的hotfix方案,它的替换过程是用 c 在 native层完成的,但其实,我们也可以用纯Java实现它!

方法替换原理

既然我们知道 AndFix 的原理是方法替换,那么为什么直接替换Java里面的 java.lang.reflect.Method 有什么问题吗?直接这样貌似很难下结论,那我们换个思路。我们实现方法替换的结果,就是调用原方法的时候最终是调用被替换的方法。因此,我们可以看看 java.lang.reflect.Method类的 invoke 方法。(Foo.bar()这种直接调用与反射调用Foo.class.getDeclaredMethod(“bar”).invoke(null) 有什么区别吗?)

  1. private native Object invoke(Object receiver, Object[] args, boolean accessible)
  2. throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

  这个invoke是一个native方法,它的native实现在 art/runtime/native/java_lang_reflect_Method.cc 里面,这个jni方法最终调用了 art/runtime/reflection.cc 的 InvokeMethod方法:

  1. object InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
  2. jobject javaReceiver, jobject javaArgs, bool accessible) {
  3. // 略...
  4.  
  5. mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod);
  6.  
  7. mirror::Class* declaring_class = m->GetDeclaringClass();
  8.  
  9. // 按需初始化类,略。。
  10.  
  11. mirror::Object* receiver = nullptr;
  12. if (!m->IsStatic()) {
  13. // Check that the receiver is non-null and an instance of the field's declaring class.
  14. receiver = soa.Decode<mirror::Object*>(javaReceiver);
  15. if (!VerifyObjectIsClass(receiver, declaring_class)) {
  16. return NULL;
  17. }
  18.  
  19. // Find the actual implementation of the virtual method.
  20. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m);
  21. }
  22.  
  23. // 略..
  24. InvokeWithArgArray(soa, m, &arg_array, &result, shorty);
  25. // 略 。。
  26. // Box if necessary and return.
  27. return soa.AddLocalReference<jobject>(BoxPrimitive(mh.GetReturnType()->GetPrimitiveType(),
  28. result));
  29. }

上面函数 InvokeMethod 的第二个参数 javaMethod 就是Java层我们进行反射调用的那个Method对象,在jni层反映为一个jobject;InvokeMethod这个native方法首先通过 mirror::ArtMethod::FromReflectedMethod 获取了Java对象的在native层的 ArtMethod指针,我们跟进去看看是怎么实现的:

  1. 1 ArtMethod* ArtMethod::FromReflectedMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject jlr_method) {
    2
  2. mirror::ArtField* f =
  3. soa.DecodeField(WellKnownClasses::java_lang_reflect_AbstractMethod_artMethod);
  4. mirror::ArtMethod* method = f->GetObject(soa.Decode<mirror::Object*>(jlr_method))->AsArtMethod();
  5. DCHECK(method != nullptr);
  6. return method;
  7. }

AndFix的实现里面,也正是使用这个 FromReflectedMethod 方法拿到Java层Method对应native层的ArtMethod指针,然后执行替换的。我们在这里看到了一点端倪,获取到了Java层那个Method对象的一个叫做 artMethod的字段,然后强转成了ArtMethod指针(这里的说法不是很准确,但是要搞明白这里面的细节一两篇文章讲不清楚 ~_~,我们暂且这么认为吧。)

上面我们也看到了,我们在native层替换的那个 ArtMethod 不是在 Java 层也有对应的东西么?我们直接替换掉 Java 层的这个artMethod 字段不就OK了?但是我们要注意的是,在Java里面除了基本类型,其他东西都是引用。要实现类似C++里面那种替换引用所指向内容的机智,需要一些黑科技。

Unsafe 和 Memory

要在Java层操作内容,也不是没有办法做到;JDK给我们留了一个后门:sun.misc.Unsafe 类;在OpenJDK里面这个类灰常强大,从内存操作到CAS到锁机制,无所不能(可惜的是据说JDK8要去掉?)但是在Android 平台还有一点点不一样,在 Android N之前,Android的JDK实现是 Apache Harmony,这个实现里面的Unsafe就有点鸡肋了,没法写内存;好在Android 又开了一个后门:Memory 类。

有了这两个类,我们就能在Java层进行简单的内存操作了!!由于这两个类是隐藏类,我写了一个wrapper,如下:

  1. private static class Memory {
  2.  
  3. // libcode.io.Memory#peekByte
  4. static byte peekByte(long address) {
  5. return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address});
  6. }
  7.  
  8. static void pokeByte(long address, byte value) {
  9. Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value});
  10. }
  11.  
  12. public static void memcpy(long dst, long src, long length) {
  13. for (long i = 0; i < length; i++) {
  14. pokeByte(dst, peekByte(src));
  15. dst++;
  16. src++;
  17. }
  18. }
  19. }
  20.  
  21. static class Unsafe {
  22.  
  23. static final String UNSAFE_CLASS = "sun.misc.Unsafe";
  24. static Object THE_UNSAFE;
  25.  
  26. private static boolean is64Bit;
  27.  
  28. static {
  29. THE_UNSAFE = Reflection.get(null, UNSAFE_CLASS, "THE_ONE", null);
  30. Object runtime = Reflection.call(null, "dalvik.system.VMRuntime", "getRuntime", null, null, null);
  31. is64Bit = (Boolean) Reflection.call(null, "dalvik.system.VMRuntime", "is64Bit", runtime, null, null);
  32. }
  33.  
  34. public static long getObjectAddress(Object o) {
  35. Object[] objects = {o};
  36. Integer baseOffset = (Integer) Reflection.call(null, UNSAFE_CLASS,
  37. "arrayBaseOffset", THE_UNSAFE, new Class[]{Class.class}, new Object[]{Object[].class});
  38. return ((Number) Reflection.call(null, UNSAFE_CLASS, is64Bit ? "getLong" : "getInt", THE_UNSAFE,
  39. new Class[]{Object.class, long.class}, new Object[]{objects, baseOffset.longValue()})).longValue();
  40. }
  41. }

具体实现

接下来思路就很简单了呀,用伪代码表示就是:  

  1. memcopy(originArtMethod, replaceArtMethod);

但是事情没有一个 sizeof 那么简单。你看AndFix的实现是在每个Android版本把ArtMethod这个结构体复制一份的;要想用sizeof还得把这个类所有的引用复制过来,及其麻烦。更何况在Java里面 sizeof都没有。不过也不是没有办法,既然我们已经能在Java层拿到对象的地址,只需要创建一个数组,丢两个ArtMethod,把两个数组元素的起始地址相减不就得到一个 artMethod的大小了吗?(此方法来自Android热修复升级探索——追寻极致的代码热替换)但是还有一个问题,我们要整个把 originMethod 的 artMethod 所在的内存直接替换为 replaceMethod 的artMethod 所在的内存(上面我们已经知道,Java层Method类的artMethod实际上就是native层的指针表示,在Android N上更明显,这玩意儿直接就是一个long),现在我们已经知道这两个地址是什么,那么我们把 replaceArtMethod 代表的内存复制到 originArtMethod 的区域,应该还需要知道一个 artMethod 有多大。

不过,既然我们实现了方法替换;还有最后一个问题,如果我们需要在替换后的方法里面调用原函数呢?这个也很简单,我们只需要把原函数copy一份保存起来,需要调用原函数的时候调用那个copy的函数不就行了?不过在具体实现的时候,会遇到一个问题,就是 Java的非static 非private的方法默认是虚方法,在调用这个方法的时候会有一个类似查找虚函数表的过程,这个在上面的代码 InvokeMethod 里面可以看到:  

  1. mirror::Object* receiver = nullptr;
  2. if (!m->IsStatic()) {
  3. // Check that the receiver is non-null and an instance of the field's declaring class.
  4. receiver = soa.Decode<mirror::Object*>(javaReceiver);
  5. if (!VerifyObjectIsClass(receiver, declaring_class)) {
  6. return NULL;
  7. }
  8.  
  9. // Find the actual implementation of the virtual method.
  10. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m);
  11. }

详细代码见:github/epic

在调用的时候,如果不是static的方法,会去查找这个方法的真正实现;我们直接把原方法做了备份之后,去调用备份的那个方法,如果此方法是public的,则会查找到原来的那个函数,于是就无限循环了;我们只需要阻止这个过程,查看 FindVirtualMethodForVirtualOrInterface 这个方法的实现就知道,只要方法是 invoke-direct 进行调用的,就会直接返回原方法,这些方法包括:构造函数,private的方法( 见 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此,我们手动把这个备份的方法属性修改为private即可解决这个问题。

android java层实现hook替换method的更多相关文章

  1. Android java层常见加密算法的hook自吐以及栈信息的打印

    杂谈:其实原理并没有很难,本质就是hook Android的框架层中的api将我们想要的key和iv(也可以没有,就打个比方),但是目前的话,很多厂家已经不在直接调用java层的这些加密算法的api了 ...

  2. android java层通过jni加载使用第三方的so库

    1.例如我们自己编译一个so库,我们的其他模块要加载如何操作了 首先在c盘新建立一个文件夹sb,在sb下面新建立一个文件夹jni,如果你要使用ndk编译so库,必须需要有jni目录 2.在jni目录下 ...

  3. Android Java层,Native层,Lib层打印Log简介【转】

    本文转载自:https://blog.csdn.net/AndroidMage/article/details/52225068 说明: 这里我根据个人工作情况说明在各个层打印log.如有问题欢迎拍砖 ...

  4. Android C/C++层hook和java层hook原理以及比较

    作者:Denny Qiao(乔喜铭),云智慧/架构师. 云智慧集团成立于2009年,是全栈智能业务运维解决方案服务商.经过多年自主研发,公司形成了从IT运维.电力运维到IoT运维的产业布局,覆盖ITO ...

  5. cocos2d-x 通过JNI实现c/c++和Android的java层函数互调

    文章摘要: 本文主要实现两个功能: (1)通过Android sdk的API得到应用程序的包名(PackageName),然后传递给c++层函数. (2)通过c++函数调用Android的java层函 ...

  6. Android进程so注入Hook java方法

    本文博客链接:http://blog.csdn.net/qq1084283172/article/details/53769331 Andorid的Hook方式比较多,现在来学习下,基于Android ...

  7. Android逆向之旅---Native层的Hook神器Cydia Substrate使用详解

    一.前言 在之前已经介绍过了Android中一款hook神器Xposed,那个框架使用非常简单,方法也就那几个,其实最主要的是我们如何找到一个想要hook的应用的那个突破点.需要逆向分析app即可.不 ...

  8. Cocos2d-x3.3RC0通过JNI调用Android的Java层URI代码发送短信

    1.Jni不在赘述.翻看前面博客 2.直接上代码 1)Java层,直接加在AppActivity.java中 public class AppActivity extends Cocos2dxActi ...

  9. Android JNI 由C/C++本地代码向Java层传递数据

    最近做的Android项目需要调用C代码,进行串口通信及与硬件设备通信,因此要用到JNI,其中本地代码需要向Java层返回三个参数,分别为 参数一:int型: 参数二: 通信指令,本地代码中为unsi ...

随机推荐

  1. Gson转换时,Double转式化

    package com.mall.core; import java.lang.reflect.Type; import java.text.DecimalFormat; import com.goo ...

  2. bootstrap中给表格设置display之后表格宽度变小问题解决

    问题描述:bootstrap中给表格设置display之后表格宽度变小了 解决方案:给表格加上 display:table样式就可以了.

  3. JAVA HttpClient进行POST请求(HTTPS)

    目前,要为另一个项目提供接口,接口是用HTTP URL实现的,最初的想法是另一个项目用jQuery post进行请求. 但是,很可能另一个项目是部署在别的机器上,那么就存在跨域问题,而jquery的p ...

  4. ContextLoaderListener和Spring MVC中的DispatcherServlet学习

    DispatcherServlet介绍 DispatcherServlet是Spring前端控制器的实现,提供Spring Web MVC的集中访问点,并且负责职责的分派,与Spring IoC容器无 ...

  5. a标签伪类的LOVE HATE原则

    a标签伪类的LOVE HATE原则 a标签有四个伪类,分别是: a:link 未访问的链接 a:visited 已访问的链接 a:hover 鼠标移动到链接上 a:active 选定的链接 遇到的问题 ...

  6. ajax返回填充的数据不显示

    原因:样式与id引用了其他的css或者js,删除其他样式,改变id就可以了

  7. Access to the path 'C:\inetpub\wwwroot\mysite\images\savehere' is denied.

    访问路径被拒绝 我解决了这个设置: IIS>应用程序池> [您的站点]>高级设置...>标识>内置帐户> LocalSystem

  8. java并发:CAS算法和ABA问题

    CAS算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令. CAS用于管理对共享数据的并发访问. java的并发包中,AQS.原子操作类等都是基于CAS实现的. CAS 是一种 ...

  9. linux分区之gpt(大于2T的分区)

    1.文件系统限制: ext3块尺寸 最大文件尺寸 最大文件系统尺寸1KiB  16GiB  2TiB2KiB  256GiB  8TiB4KiB  2TiB  16TiB8KiB  16TiB  32 ...

  10. redis中multi和pipeline区别以及效率(推荐使用pipeline)

    手册得知 pipeline 只是把多个redis指令一起发出去,redis并没有保证这些指定的执行是原子的:multi相当于一个redis的transaction的,保证整个操作的原子性,避免由于中途 ...