【原创】Android5.1 Art Hook 技术分享
Hi,大家好,很多次的在各种技术论坛上看到大牛的分享,学到了很多。本着共建社区,共享知识的目的,在这里我和大家分享一下我最近研究到的关于Android5.1的ART HOOK方案。还是demo阶段,请大家多多指正。可以加我QQ 313199058一起探讨。
废话不说,切入正题。
之前看过低端码农关于ART HOOK的思路,有了启发,大家可以先到他的博客上看看他对于ART虚拟机的理解以及他做的hook方案。
http://blog.csdn.net/l173864930/article/details/45035521
他做的是基于android 4.4的hook方案,但是是停留在仅仅打log的阶段,在我后面的测试中发现这其实离真正的hook应用还相去甚远。下面我罗列了一些我需要解决的问题。这些问题,低端码农有解决过一些,有的没有;xposed有解决过一些,有的没有。
我们需要解决如下的几个问题:
1. 如何hook到一个函数
2. 如何回避在输出调用栈时的虚拟机崩溃
3. 如何处理参数
4. 如何处理返回值
5. 如何在不替换libart.so的情况下完成hook
6. 如何在不引用android源码头文件及库的情况下,用NDK直接编译出我们的so
0x00
老调重弹,我先简单介绍一下ART虚拟机关于方法的调用方式。不同于dalvik虚拟机,ART其实包含了两种调用方式——解释执行和机器码执行,首先他没有完全丢弃解释执行的调用方式,因为有些情况下还是需要通过解释执行完成一个函数的运行;接着ART不同于dalvik是因为引入了机器码的运行方式,其实就是在dex opt的时候dex里的一个函数体被优化成了汇编语言编写的机器码,这样运行效率当然高了。
下面看一下oatdump出的某函数片段
4: voidcom.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}, voidcom.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
GC map objects: v4 (r6), v5 ([sp + #72])
0xf722d562: 3c01 subs r4, #1
0xf722d564: f0008003 beq.w +6 (0xf722d56e)
0xf722d568: b00d add sp, sp, #52
0xf722d56a: e8bd8060 pop {r5, r6, pc}
0xf722d56e: f8d9e25c ldr.w lr, [r9, #604] ; pTestSuspend
0xf722d572: 47f0 blx lr
suspend point dex PC: 0x0006
0xf722d574: e7f8 b -16 (0xf722d568)
0xf722d576: f8dde008 ldr.w lr, [sp, #8]
0xf722d57a: b003 add sp, sp, #12
0xf722d57c: f8d9c274 ldr.w r12, [r9, #628] ; pThrowStackOverflow
0xf722d580: 4760 bx r12
0xf722d582: 0000 lsls r0, r0, #0
包含了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_taccess_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] @ loadThread::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 ispending
bx lr @ return on success
1:
DELIVER_PENDING_EXCEPTION
END art_quick_to_interpreter_bridge
可以看到最终调用到artQuickToInterpreterBridge中去了,在那里就会对这个函数进行了解释执行。
把之前的com.example.atry.MainActivity.onClick函数的内容再拿来分析一下(去掉无关代码)
4: voidcom.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}, voidcom.example.atry.MainActivity.nativeTest(int, long) // method@17
0x0006: return-void
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, jobjectthiz, 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
下面我们谈谈如何回避输出调用栈时虚拟机会crash的问题。
相信大多数做ART HOOk的朋友都遇到过这样的问题,如果在调用流程中类似
Log.d(TAG, "test", new Exception());或者
e.printStackTrace();等函数被调用,
虚拟机就会崩溃。崩溃的位置在StackVisitor::WalkStack函数里。经过我的实验发现,在调用上面类型的函数时,函数会回溯调用栈,如果发现在调用栈里出现了没有和dex对应的汇编指令(就是我们自己定义的跳转函数等)就会报错。
所以我们解决这个问题的办法需要走2步:
1. 坚决杜绝在调用hook处理函数前,调用到输出堆栈类型的函数;并且在调用到hook处理函数前要做好堆栈和寄存器的处理,保证在回溯的时候发现不了任何跳转函数的足迹
2. 去掉dex和机器码之间的mapping关系。在4.4上的art_method类里是有一个mapping table的成员,我的做法是直接将其值为Null。但是还没有在5.1上发现类似的成员,所以5.1上的hook可能出现类似的问题,不过既然知道了方向,想解决也不难。
下面我们看一下一个hook的示例
ENTRY art_quick_dispatcher
push {r4,r5, lr} @ sp -12
…
blx artQuickToDispatcher
pop {r4,r5, pc} @ success,r0andr1 hold the result
END art_quick_dispatcher
上面一段的意思就是说一个被hook的函数被调用后,其实先调用了art_quick_dispatcher这个函数,接着这个函数又调用了artQuickToDispatcher。
那么这就出现了问题,如果原函数或者你自己写的hook处理函数中出现了输出调用栈的代码,那么我们预期的调用关系会是:调用者->art_quick_dispatcher->artQuickToDispatcher->被调用者。可是art_quick_dispatcher和artQuickToDispatcher是没有java代码与其对应的,结果就是虚拟机直接崩溃。
所以我对这种代码做了一个升级。要达到的目的就是每次调用一个我们自己写的函数,都是有java代码与其对应的。举例说来就是上面的调用者->art_quick_dispatcher->artQuickToDispatcher->被调用者关系,其中的art_quick_dispatcher,artQuickToDispatcher都有真实的java代码对应。所以我就预写了相关的java函数代码,然后取出其art_method的首地址,在hook后直接在汇编代码层进行调用。这样对于虚拟机来说就是正常的函数调用关系了。
调用形式如下:
ENTRY art_quick_proxy
push {r0-r7}
mov r7, lr
mov r0, #9//标号为9的函数是我预写的java函数
bl exe_switch_entry//执行这个java函数,exe_switch_entry也是我写的一段汇编代码
…
mov lr, r7
pop {r0-r7}
…
这里保存了所有寄存器和返回地址,在调用了想要调用的java函数后,所有寄存器和堆栈以及返回地址都恢复正常,这样对于虚拟机来说,就相当于正常的java调用。
取消mapping关系的方法,在4.4上我将art_method类mapping table置为Null,方法如下:
//下面的define都是4.4上的,5.1上没有mappingtable,还没有研究
#define METHOD_ACCESS_FLAG 20
#define METHOD_MAPPING_TABLE 60
#define kAccNative 0x0100
#define kAccStatic 0x0008
…
int* access_flag = (int*) (artmeth + METHOD_ACCESS_FLAG);
*access_flag = *access_flag | kAccNative;
int* mapping_table = (int*) (artmeth + METHOD_MAPPING_TABLE);
*mapping_table= 0;
…
0x02
如何处理参数
处理参数问题就和主流的参数处理方法一致了,我这里就是遍历堆栈,获取参数,然后通过调用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);
…
0x03
如何处理返回值
我想到的办法就是,为每一个被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, Objectreceiver, Object[] args) {
return (Long) HookManager.onHooked(artmethod,receiver, args);
}
private static double onHookDouble(Object artmethod,Object receiver, Object[] args) {
return (Double) HookManager.onHooked(artmethod,receiver, args);
}
private static char onHookChar(Object artmethod, Objectreceiver, Object[] args) {
return (Character) HookManager.onHooked(artmethod,receiver, args);
}
private static short onHookShort(Object artmethod, Objectreceiver, Object[] args) {
return (Short) HookManager.onHooked(artmethod,receiver, args);
}
private static float onHookFloat(Object artmethod, Objectreceiver, Object[] args) {
return (Float) HookManager.onHooked(artmethod,receiver, args);
}
private static Object onHookObject(Objectartmethod, Object receiver, Object[] args) {
return HookManager.onHooked(artmethod,receiver, args);
}
private static boolean onHookBoolean(Objectartmethod, Object receiver, Object[] args) {
return (Boolean) HookManager.onHooked(artmethod,receiver, args);
}
private static byte onHookByte(Object artmethod, Objectreceiver, Object[] args) {
return (Byte) HookManager.onHooked(artmethod,receiver, args);
}
而HookManager.onHooked返回的是Object类型,对于基本类型来说,我们只要对其拆箱就可以了。
0x04
结束
至此,关于android5.1上的hook就完成了,本文主要是为了解决前辈们做的hook demo遗留下来的一些问题,立志于对这一体系做一种补充,感谢大家。