0x00 背景
最近想做一个安卓的hook framework,来进行dynamic binary instrumentation。看了一些教程,完成了一部分工作。
0x01 原理
hook原理参照zhengmi大神的文章就可以理解,我简单总结一下:
首先启动一个进程,然后ptrace想要hook的目标进程。我们启动的进程不妨称之为tracer,被hook的目标进程称之为tracee。
tracer通过控制tracee的pc寄存器来使tracee调用dlopen函数来加载自己预先编译好的so文件,这个so文件里存放着我们想要注入的函数,不妨称这个so文件为libinject.so。
在tracee加载完libinject.so之后,再次利用tracer控制tracee的pc寄存器,调用libinject.so里的init函数(init函数的地址可以通过dlsym来查找)。
init函数中主要做的事是:把tracee中需要hook的函数地址的头几个字节全部替换为类似于jmp的命令,使得tracee在执行到被hook的函数时(不妨称这个函数为hookee)可以跳转到libinject中我们自己替换的函数处执行(称之为hooker)。
在hooker函数中,我们首先可以做一些自己的处理(pre_hook),然后需要将hookee的头几个字节复原,再重新调用hookee,之后可以做一些自己想做的post_hook处理。最后记得将hookee的头几个字节再次替换为jmp命令,以便下次执行到hookee的时候还能跳转到自己的hooker函数。
注意ptrace的使用方法如下图:
1. 首先tracer attach上tracee,这时tracee进程会停止执行,并且向tracer发出signal,tracer会在wait函数中陷入阻塞状态直到接收到该signal。
2. 这时由于tracee是停止的状态,tracer就可以自由控制tracee的寄存器、内存等状态了。
3. 设置完tracee的寄存器、内存等之后,tracer可以调用PTRACE_CONT使tracee继续执行,这里有个技巧,因为调用PTRACE_CONT之后,除非tracee接收到signal,不然tracee不会停止执行,所以我们在使用tracer控制tracee的寄存器和内存时,可以在想要tracee停止的地方写入触发signal的命令,或者在lr寄存器里写入非法值(比如0),使得tracee在执行完当前函数返回时触发SIGILL,这样tracee就会停止,tracer从wait中返回,又能控制tracee了。
0x02 实现时的坑
zhengmi大神的代码https://github.com/zhengmin1989/TheSevenWeapons/blob/master/LiBieGou/test1/jni/hook5.c#L268在injectSo里调用了PTRACE_GETREGS,但是在调用ptrace前没有wait,这样tracee不一定处于停止状态,所以导致有时候程序会crash。解决方法就是在调用ptrace之前一定要调用wait,确保tracee停止。
在hook thumb模式的代码时我参考了adbi的代码:https://github.com/crmulliner/adbi/blob/master/instruments/base/hook.c#L107,但是在实际hook某些thumb函数的过程中我发现程序会crash。经过debug,我发现在arm模式下,cpu会预取两条即8个字节的指令,pc寄存器的值总为当前执行指令的地址+8字节。但是在thumb模式下,cpu也是预取两条指令即4字节。但是pc并不是总是当前指令地址+4字节的。如果当前指令地址模4余2的话,pc为当前指令地址+2,如果当前指令地址模4余0的话,pc为当前指令地址+4。这样修改后的代码为:https://github.com/nevermoe/AOSHook/blob/master/jni/hook.c#L125-L132。
在zhengmi大神的代码里调用dlopen、dlsym、dlerror、dlclose函数时使用的是libc里的函数。但是我在实际调用时发现dlopen并不在libc的内存范围内,而是在/system/bin/linker这个binary的内存范围里,如果以libc的基址计算则会算出错误的地址。于是我的代码改成了这样:https://github.com/nevermoe/AOSHook/blob/master/jni/stalker.c#L150-L154 。
zhengmi大神的hook方法是将原函数的头函数改写为跳转语句,跳转到我们注入的函数头部执行,在我们注入的函数里,首先需要调用hook_unset_jump来将原函数的跳转语句给取消掉,把原函数头部给复原。这样我们的注入函数在调用原函数时就会调用一个完好的原函数。当我们的注入函数结束时,会调用hook_set_jump来将原函数的头部再次设为跳转指令,这样下次执行的原函数时就能继续跳转到我们的注入函数了。这种实现方法的好处是实现简单,不需要对cpu指令进行太多处理,不容易出错,但是缺点是如果有多个线程同时运行到原函数处,很可能会发生运行错误,因为多个线程会同时尝试改写原函数头部。
针对4所说的缺点,eleven大神使用了这样的解决方法:https://github.com/ele7enxxh/Android-Inline-Hook。在他的实现中,原函数头部的原始指令将会被复制到一个新的地址,并在这个复制的指令之后加上跳回原函数头部之后的部分继续执行。这样的好处是不需要运行时动态改.text段的代码,不会产生多线程的问题,但是原函数的头部指令在复制到新的地址后会产生大量relocation问题,因为arm的指令有很多是基于pc寄存器寻址的,所以改变指令地址很可能会导致指令失效,这样就需要大量的指令寻址修复。eleven大神给出了一个六百多行的代码来处理这些地址重定位问题,但是仍然无法解决所有的情况,比如这个issue。