【原创】Android 5.1 Art Hook 技术分享

6,031 阅读10分钟

Hi,大家好,很多次的在各种技术论坛上看到大牛的分享,学到了很多。本着共建社区,共享知识的目的,在这里我和大家分享一下我最近研究到的关于Android5.1的ART HOOK方案。还是demo阶段,请大家多多指正。可以加我QQ 313199058一起探讨。

首先简单介绍一下hook。所谓hook就是通过一些手段改变一个函数的执行逻辑,比如在函数调用前更改一下参数或者在调用后修改返回值,甚至直接返回,不执行原函数。这篇文章就是介绍Android ART虚拟机上的关于java 函数的hook技术。

废话不说,切入正题。
之前看过低端码农关于ART HOOK的思路,有了启发,大家可以先到他的博客上看看他对于ART虚拟机的理解以及他做的hook方案。
http://blog.csdn.net/l173864930/article/details/45035521
他做的是基于android 4.4的hook方案,但是是停留在仅仅打log的阶段,在我后面的测试中发现这其实离真正的hook应用还相去甚远。下面我罗列了一些我需要解决的问题。这些问题,低端码农有解决过一些,有的没有;xposed有解决过一些,有的没有。
我们需要解决如下的几个问题:
1. 如何hook到一个函数
2. 如何处理参数
3. 如何处理返回值

0x00
老调重弹,我先简单介绍一下ART虚拟机关于方法的调用方式。不同于dalvik虚拟机,ART其实包含了两种调用方式——解释执行和机器码执行,首先他没有完全丢弃解释执行的调用方式,因为有些情况下还是需要通过解释执行完成一个函数的运行;接着ART不同于dalvik是因为引入了机器码的运行方式,其实就是在dex opt的时候dex里的一个函数体被优化成了汇编语言编写的机器码,这样运行效率当然高了。
下面看一下oatdump出的某函数片段
4: void com.example.atry.MainActivity.onClick(android.view.View) (dex_method_idx=18)
    DEX CODE:
      0x0000: const/4 v0, #+2
      0x0001: const-wide/16 v2, #+5
      0x0003: invoke-virtual {v4, v0, v2, v3}, void com.example.atry.MainActivity.nativeTest(int, long) // method@17
      0x0006: return-void
    OAT DATA:
      frame_size_in_bytes: 64
      core_spill_mask: 0x00008060 (r5, r6, r15)
      fp_spill_mask: 0x00000000 
      vmap_table: 0xf722d58a (offset=0x0000258a)
      v3/r5, v4/r6, v65535/r15
      mapping_table: 0xf722d584 (offset=0x00002584)
      gc_map: 0xf722d590 (offset=0x00002590)
    CODE: 0xf722d51d (offset=0x0000251d size=104)...
      0xf722d51c: f8d9c010  ldr.w   r12, [r9, #16]  ; stack_end_
      0xf722d520: e92d4060  push    {r5, r6, lr}
      0xf722d524: f2ad0e34  subw    lr, sp, #52
      0xf722d528: 45e6      cmp     lr, r12
      0xf722d52a: f0c08024  bcc.w   +72 (0xf722d576)
      0xf722d52e: 46f5      mov     sp, lr
      0xf722d530: 9000      str     r0, [sp, #0]
      0xf722d532: 1c0e      mov     r6, r1
      0xf722d534: 9212      str     r2, [sp, #72]
      0xf722d536: 2202      movs    r2, #2
      0xf722d538: 9208      str     r2, [sp, #32]
      0xf722d53a: 2305      movs    r3, #5
      0xf722d53c: f04f0c00  mov.w   r12, ThumbExpand(0)
      0xf722d540: e9cd3c0a          
      0xf722d544: 9b0b      ldr     r3, [sp, #44]
      0xf722d546: 1c31      mov     r1, r6
      0xf722d548: f8d1e000  ldr.w   lr, [r1, #0]
      0xf722d54c: 9304      str     r3, [sp, #16]
      0xf722d54e: 9304      str     r3, [sp, #16]
      0xf722d550: f8dee034  ldr.w   lr, [lr, #52]
      0xf722d554: 9b0a      ldr     r3, [sp, #40]
      0xf722d556: 2202      movs    r2, #2
      0xf722d558: f8de0544  ldr.w   r0, [lr, #1348]
      0xf722d55c: f8d0e028  ldr.w   lr, [r0, #40]
      0xf722d560: 47f0      blx     lr
      suspend point dex PC: 0x0003
…

包含了smali代码和汇编代码。

由于ART是这种大杂烩的执行函数的方式,因此他就要确定一个函数是通过解释执行来运行,还是通过机器码来运行,所以在4.4及以后的版本的art上出现了bridge的概念,他可以被理解为解释执行方式跳转到机器码执行方式或者机器码执行方式跳转到解释执行方式的桥梁。举例说明,就是本来a,b,c,d四个函数都是顺序执行在机器码执行的方式下,突然在调用e这个函数的时候发现需要跳转到解释执行的方式,这就需要一个bridge。
下面结合代码看一下,首先是art_method.h(忽略了无关代码)
class MANAGED ArtMethod : public Object {
…
protected:
    Class* declaring_class_;
    uint32_t access_flags_;
    uint32_t code_item_offset_;
    const void* entry_point_from_compiled_code_;
    EntryPointFromInterpreter* entry_point_from_interpreter_;
….
}

这里entry_point_from_compiled_code_和entry_point_from_interpreter_就是2个bridge。一个是说从code(机器码)转来的,去哪里由这个bridge决定,一个是说从interpreter转来的,去哪里由这个bridge决定。其实每次函数调用,调用者都是执行被调用者的bridge。举例说明,如果一个函数是在机器码执行流程里,他调用下一个函数的时候会调用被调用者的成员接口entry_point_from_compiled_code_(意思是告诉被调用者,这是来自机器码的执行流程),如果被调用者的这个接口被设为机器码的执行入口,那么被调用者就直接被执行了,也就是是说被调用者也是在机器码执行流程中;否则,这个接口如果被设为一个解释执行函数的入口函数,被调用者就会在解释执行中被运行了。下面介绍的一个就是一个解释执行的入口函数。
ENTRY art_quick_to_interpreter_bridge
    SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME
    mov     r1, r9                 @ pass Thread::Current
    mov     r2, sp                 @ pass SP
    blx     artQuickToInterpreterBridge    @ (Method* method, Thread*, SP)
    ldr     r2, [r9, #THREAD_EXCEPTION_OFFSET]  @ load Thread::Current()->exception_
    ldr     lr,  [sp, #44]         @ restore lr
    add     sp,  #48               @ pop frame
    .cfi_adjust_cfa_offset -48
    cbnz    r2, 1f                 @ success if no exception is pending
    bx    lr                       @ return on success
1:
    DELIVER_PENDING_EXCEPTION
END art_quick_to_interpreter_bridge

可以看到最终调用到artQuickToInterpreterBridge中去了,在那里就会对这个函数进行了解释执行。

把之前的com.example.atry.MainActivity.onClick函数的内容再拿来分析一下(去掉无关代码)
4: void com.example.atry.MainActivity.onClick(android.view.View) (dex_method_idx=18)
    CODE: 0xf722d51d (offset=0x0000251d size=104)...
      0xf722d51c: f8d9c010  ldr.w   r12, [r9, #16]  ; stack_end_
      0xf722d520: e92d4060  push    {r5, r6, lr}
      0xf722d524: f2ad0e34  subw    lr, sp, #52
      0xf722d528: 45e6      cmp     lr, r12
      0xf722d52a: f0c08024  bcc.w   +72 (0xf722d576) 
//上面都是检查是否调用函数层数太多,防止栈溢出。
      0xf722d52e: 46f5      mov     sp, lr
      0xf722d530: 9000      str     r0, [sp, #0]
      0xf722d532: 1c0e      mov     r6, r1
      0xf722d534: 9212      str     r2, [sp, #72]
      0xf722d536: 2202      movs    r2, #2
      0xf722d538: 9208      str     r2, [sp, #32]
      0xf722d53a: 2305      movs    r3, #5
      0xf722d53c: f04f0c00  mov.w   r12, ThumbExpand(0)
      0xf722d540: e9cd3c0a          
      0xf722d544: 9b0b      ldr     r3, [sp, #44]
      0xf722d546: 1c31      mov     r1, r6
      0xf722d548: f8d1e000  ldr.w   lr, [r1, #0]
      0xf722d54c: 9304      str     r3, [sp, #16]
      0xf722d54e: 9304      str     r3, [sp, #16]
      0xf722d550: f8dee034  ldr.w   lr, [lr, #52]
      0xf722d554: 9b0a      ldr     r3, [sp, #40]
      0xf722d556: 2202      movs    r2, #2//上面都是在构造参数,准备调用下个函数
      0xf722d558: f8de0544  ldr.w   r0, [lr, #1348] //找到了被调用函数nativeTest
      0xf722d55c: f8d0e028  ldr.w   lr, [r0, #40]//取出被调用函数首地址偏移40的地址
      0xf722d560: 47f0      blx     lr//跳转到偏移40处的地址
      …

从上的代码可以看到,机器码直接跳转到被调用函数偏移40的位置,而那就是被调用函数的entry_point_from_compiled_code_接口。(4.4是偏移40,5.1是偏移44)

机器码执行的函数在初始化的时候会设置entry_point_from_compiled_code_为机器码执行入口;而如果这个函数需要解释执行,则entry_point_from_compiled_code_会被设为art_quick_to_interpreter_bridge(绝大多数的).

说了这么多,就引出了第一个问题的答案,如何hook一个函数?我们可以把一个函数偏移40处的地址存的值设为我们自己写的函数地址,这样,一个函数的执行流程就被hook到了。
代码示例:
static jint hook_zposed_method(JNIEnv* env, jobject thiz, jobject method) {
  jmethodID methid = (*env)->FromReflectedMethod(env, method);
  int artmeth = (int) methid;
  int* quick_entry_32 = (int*) (artmeth + 40);
  jint ptr = (jint)* quick_entry_32;
  *quick_entry_32 = (int) (&art_quick_proxy);
/*
  int* access_flag = (int*) (artmeth + METHOD_ACCESS_FLAG);
  *access_flag = *access_flag | kAccNative;

  int* mapping_table = (int*) (artmeth + METHOD_MAPPING_TABLE);
  *mapping_table = 0;*/
  return ptr;
}

art_quick_proxy就是我们自己写的函数,事实证明在调用被hook函数的时候,调用的其实是art_quick_proxy。

0x01
如何处理参数
处理参数问题就和主流的参数处理方法一致了,我这里就是遍历堆栈,获取参数,然后通过调用java函数对基本类型装箱,最后由一个Object数组的形式封装所有的参数。下面具体介绍一下。
首先要介绍一下art虚拟机上参数是如何传递的。在汇编层面,参数组织如下:
r0 = method
r1 = this
r2 = arg0
r3 = arg1
[sp] = N/A
[sp + 4] = N/A
[sp + 8] = N/A
[sp + 12] = N/A
[sp + 16] = arg2
需要注意的就是堆栈中的前4个存储单元里存的东西未知,不管是什么,肯定是我们不需要的,但是又不建议妄自修改的东西。
然后我们可以从r2寄存器开始遍历,取出所有的参数。基本参数的装箱就是指将int, short等类型转换为Integer, Short这样的Object,java里已经有这样的函数供我们使用了:
Integer.valueOf(int);
Short.valueOf(short);


0x02
如何处理返回值
我想到的办法就是,为每一个被hook的函数都分配一个与其返回值对应的hook处理函数,由于返回值类型是确定的(8种基本类型加Object),所有我枚举的构造了9种不同返回值的hook处理函数(直接java编写)。
    private static int onHookInt(Object artmethod, Object receiver, Object[] args) {
        return (Integer) HookManager.onHooked(artmethod, receiver, args);
    }

    private static long onHookLong(Object artmethod, Object receiver, Object[] args) {
        return (Long) HookManager.onHooked(artmethod, receiver, args);
    }
...

大概都是以上的形式,不一一列举了。
而HookManager.onHooked返回的是Object类型,对于基本类型来说,我们只要对其拆箱就可以了。

0x03
结束
至此,关于android5.1上的hook就完成了,本文主要是为了解决前辈们做的hook demo遗留下来的一些问题,立志于对这一体系做一种补充,朋友们如果有其他具体问题想探讨可以加我qq313199058,感谢大家