android hook 框架 ADBI 如何实现so函数挂钩

上一篇 android 5 HOOK 技术研究之 ADBI 项目 02 分析了hijack.c, 这个文件编译为一个可执行程序 hijack, 该程序实现了向目标进程注入一个动态库的功能。这一篇继续研究 adbi 项目其他源码,解决真正替换目标进程函数的问题。

 

在开始之前,先看看 adbi 给出的一个例子,这个例子替换了目标进程epoll_wait函数的实现为自定义的实现:

首先,给出例子的epoll_wait自定义实现,共2个:

int my_epoll_wait_arm(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
        return my_epoll_wait(epfd, events, maxevents, timeout);
}
int my_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
        int (*orig_epoll_wait)(int epfd, struct epoll_event *events, int maxevents, int timeout);
        orig_epoll_wait = (void*)eph.orig;

        hook_precall(&eph);
        int res = orig_epoll_wait(epfd, events, maxevents, timeout);
        if (counter) {
                hook_postcall(&eph);
                log("epoll_wait() called\n");
                counter--;
                if (!counter)
                        log("removing hook for epoll_wait()\n");
        }

        return res;
}
hook(&eph, getpid(), "libc.", "epoll_wait", my_epoll_wait_arm, my_epoll_wait);
my_epoll_wait_arm 是 arm 格式指令下的 hook 函数,
my_epoll_wait 是 thumb 格式指令下的 hook 函数,
hook 函数将 libc 库的 epoll_wait 函数替换成上述函数中的一个,并将一些关键信息存放在 eph 结构里。
查看 my_epoll_wait 这个函数主要执行3步: hook_precall, orig_epoll_wait, hook_postcall。 
hook_precall 将 eph 结构保存的原始 epoll_wait 地址重新赋值回去,orig_epoll_wait 调用原始 epoll_wait, 最后hook_postcall再用自定义的my_epoll_wait 赋值为原始epoll_wait. 一般inline hook 都是这种套路,这样做可以在hook函数内部调用原始函数,以实现在原始函数之上做一些事情。
其中,eph 是 struct hook_t 结构,用于保存一次hook的相关信息,如下:
struct hook_t {
        unsigned int jump[3]; // 对应arm,12个字节,执行这部分指令可以调用 hook 函数
        unsigned int store[3]; // 对应arm, 12 个字节,执行这部分指令可以让目标函数恢复到原始状态
        unsigned char jumpt[20]; //对应 thumb,20个字节,执行这部分指令可以调用 hook 函数
        unsigned char storet[20];//对应 thumb, 20个字节,执行这部分指令可以让目标函数恢复到原始状态
        unsigned int orig; // 目标函数地址,真正发挥作用的指令,它的值可能是上述4种其一
        unsigned int patch; // hook函数地址
        unsigned char thumb; // 取1 表示指令格式是 thumb, 取 0 表示是 arm
        unsigned char name[128];// 被hook的目标函数名
        void *data;
};

 

下面逐步分析目标进程函数的劫持过程,首先第一个问题,需要找到目标函数在目标进程内的地址。

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{ unsigned
long int addr; int i; if (find_name(pid, funcname, libname, &addr) < 0) { log("can't find: %s\n", funcname) return 0; }     。。。 }

这个问题,在  android 5 HOOK 技术研究之 ADBI 项目 02  分析向目标进程注入so的过程时已经有涉及(需要找到目标进程内的 dlopen 函数, mprotect 函数)。 在 hijack.c 的实现里,是通过解析本进程和目标进程的 maps 文件,得到本进程和目标进程 libdl.so 库加载的起始地址,然后用dlsym获取本进程内 dlopen 函数的地址,算出dlopen函数到 libdl.so 加载初始地址的 offset, 然后目标进程 libdl.so 的起始地址加上刚刚算出的 offset 即得到目标进程内部 dlopen的真实地址,这里,利用的是tracer进程和 tracee 进程加载的是同一个 libdl.so ,因而同一个符号在该动态库里的offset是固定的。

 

那么,如果要hook的目标函数不是在一个动态库里,而是tracee进程静态链接的一个函数,上述方式就失效了,这时候,如何获取目标函数地址呢?

这里,adbi项目的作者给出了另外一种方式,可以解决这个问题,find_name 函数仍然是获取动态库里的函数的地址,但这个实现改一下也可以用于获取静态链接的函数的地址:

 

int find_name(pid_t pid, char *name, char *libn, unsigned long *addr)
{
        struct mm mm[1000];
        unsigned long libcaddr;
        int nmm;
        char libc[1024];
        symtab_t s;

        if (0 > load_memmap(pid, mm, &nmm)) { // load_memap 函数加载 maps 文件并将内存块解析成一个数组
                log("cannot read memory map\n")
                return -1;
        }
        if (0 > find_libname(libn, libc, sizeof(libc), &libcaddr, mm, nmm)) { // 在上述数组里使用so名字寻找对应的起                                              // 始地址
                log("cannot find lib: %s\n", libn)
                return -1;
        }
        //log("lib: >%s<\n", libc)
        s = load_symtab(libc); // 加载so文件,解析后获取符号表
        if (!s) {
                log("cannot read symbol table\n");
                return -1;
        }
        if (0 > lookup_func_sym(s, name, addr)) { // 获取目标函数在符号表里的地址值,这个值其实就是该函数相对于so起始地
                              // 址的offset log(
"cannot find function: %s\n", name); return -1; } *addr += libcaddr; // so 在maps文件里的起始地址 + 函数在so符号表里的值 = 函数在目标进程的地址 return 0; }
load_symtab, lookup_func_sym 这两个函数在  android 5 HOOK 技术研究之 ADBI 项目 02  没有出现过, load_symtab 的作用是使用 elf 格式解析 so 文件, (linux平台下, 可执行程序,动态库,.o 文件,都是elf格式的文件, android 底层是linux), 获取该elf文件的符号表。 lookup_func_sym 函数使用函数名遍历符号表,找到该符号(函数)对应的地址,对与 so 文件来说,这时候找到的地址是一个以起始地址为0计算的地址,即本质是一个offset, 用这个值加上目标进程 maps 文件里 so 加载的起始地址,即得到了目标进程里函数的地址。

如果要hook的地址不是so的函数呢,则使用 load_symtab 加载目标进程的可执行程序并获取符号表, lookup_func_sym 得到的函数地址就是真实地址。

如果目标进程对应的可执行程序,或者加载的so,符号被 strip 了(这时候其elf文件里没有符号表),如果获取函数地址 ?

 ==========

 找到要hook的函数的地址后,开始填充 hook_t 结构,并覆盖原始地址:

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
    。。。
     log("hooking:   %s = 0x%x ", funcname, addr)
        strncpy(h->name, funcname, sizeof(h->name)-1); // 被hook的函数名

        if (addr % 4 == 0) { // ARM 格式
                log("ARM using 0x%x\n", hook_arm)
                h->thumb = 0;
                h->patch = (unsigned int)hook_arm; // hook 函数先保存在 h->patch 字段
                h->orig = addr; // 目标进程的被hook函数原始地址保存在 h->orig 字段
                h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
                h->jump[1] = h->patch; // 新的hook函数地址放在 hook指令的第4到12个字节
                h->jump[2] = h->patch;
                for (i = 0; i < 3; i++)
                        h->store[i] = ((int*)h->orig)[i]; // 由于hook需要12个指令,这里先把原始12个字节的指令保存在 h-                                  //>store, unhook 时用到
                for (i = 0; i < 3; i++)
                        ((int*)h->orig)[i] = h->jump[i]; // 新的指令12个字节覆盖老的指令
        }
        else {
                if ((unsigned long int)hook_thumb % 4 == 0) //thumb格式
                        log("warning hook is not thumb 0x%x\n", hook_thumb)
                h->thumb = 1;
                log("THUMB using 0x%x\n", hook_thumb)
                h->patch = (unsigned int)hook_thumb; // hook函数地址保存在 h->patch
                h->orig = addr; // 原始地址保存在 h->orig
                h->jumpt[1] = 0xb4; // jumpt 保存实现 hook 的指令,20字节
                h->jumpt[0] = 0x30; // push {r4,r5}
                h->jumpt[3] = 0xa5;
                h->jumpt[2] = 0x03; // add r5, pc, #12
                h->jumpt[5] = 0x68;
                h->jumpt[4] = 0x2d; // ldr r5, [r5]
                h->jumpt[7] = 0xb0;
                h->jumpt[6] = 0x02; // add sp,sp,#8
                h->jumpt[9] = 0xb4;
                h->jumpt[8] = 0x20; // push {r5}
                h->jumpt[11] = 0xb0;
                h->jumpt[10] = 0x81; // sub sp,sp,#4
                h->jumpt[13] = 0xbd;
                h->jumpt[12] = 0x20; // pop {r5, pc}
                h->jumpt[15] = 0x46;
                h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
                memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));// hook函数地址赋值给 jump                                    //t 指令的第 16字节开始的位置
                unsigned int orig = addr - 1; // sub 1 to get real address
                for (i = 0; i < 20; i++) {
                        h->storet[i] = ((unsigned char*)orig)[i]; //同等数量(20字节)的原始指令保存在 h->storet 里
                        //log("%0.2x ", h->storet[i])
                }
                //log("\n")
                for (i = 0; i < 20; i++) {
                        ((unsigned char*)orig)[i] = h->jumpt[i]; // 新的指令覆盖老的指令
                        //log("%0.2x ", ((unsigned char*)orig)[i])
                }
        }
        hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));//刷新缓存
        return 1;
}

 上述注释可以看到,hook函数需要分 arm 指令和 thumb 指令实现两个分支的hook, 其中 , arm 格式需要替换 12 个字节的指令,thumb需要替换 20个字节,将原始的指令存放在 hook_t结构的 store/storet 字段,将新的指令构造好后存放在 hook_t结构的 jump/jumpt字段,hook_t结构的 orig 字段存放原始函数的地址,当执行 hook 调用时,用 jump/jumpt 指令覆盖 orig地址对应长度的内存,这时候执行 orig 函数,就会执行到 jump/jumpt指令,也就是执行到hook函数,当执行 unhook 调用时,就用 store/storet 指令覆盖 orig 地址对应长度的内存,这时候的这块指令等于原始执行,相当于执行原始函数。

void unhook(struct hook_t *h)
{
        log("unhooking %s = %x  hook = %x ", h->name, h->orig, h->patch)
        hook_precall(h);
}

unhook函数,直接调用 hook_precall, 前面分析 my_epoll_wait 函数时发现,hook函数里也调用了 hook_precall, hook_postcall ,以实现在hook函数内可以调用原始函数,下面是这两个函数的实现:

void hook_precall(struct hook_t *h)
{
        int i;

        if (h->thumb) {
                unsigned int orig = h->orig - 1;
                for (i = 0; i < 20; i++) {
                        ((unsigned char*)orig)[i] = h->storet[i];
                }
        }
        else {
                for (i = 0; i < 3; i++)
                        ((int*)h->orig)[i] = h->store[i];
        }
        hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}

void hook_postcall(struct hook_t *h)
{
        int i;

        if (h->thumb) {
                unsigned int orig = h->orig - 1;
                for (i = 0; i < 20; i++)
                        ((unsigned char*)orig)[i] = h->jumpt[i];
        }
        else {
                for (i = 0; i < 3; i++)
                        ((int*)h->orig)[i] = h->jump[i];
        }
        hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}

实现比较简单,参见上面的讲述,这里不再赘述。

 

上述hook方式本质上是一个内存的赋值操作(((unsigned char*)orig)[i] = h->jumpt[i];)赋值操作的左值orig是代码区的地址,但不意味着这个赋值操作完成后,对orig代码区的调用就马上会用到刚刚赋值的指令,这是因为,CPU执行指令时,是从缓存(cache)获取的指令,内存的指令同步到缓存需要时间,所以,adbi 在每次对目标地址的指令做内存赋值操作后,都添加了一个刷新缓存的操作,人为触发一次指令同步操作,

void inline hook_cacheflush(unsigned int begin, unsigned int end)
{
        const int syscall = 0xf0002;
        __asm __volatile (
                "mov     r0, %0\n"
                "mov     r1, %1\n"
                "mov     r7, %2\n"
                "mov     r2, #0x0\n"
                "svc     0x00000000\n"
                :
                :       "r" (begin), "r" (end), "r" (syscall)
                :       "r0", "r1", "r7"
                );
}

 

  1. #define __ARM_NR_BASE (__NR_SYSCALL_BASE+0x0f0000)   
  2. #define __ARM_NR_breakpoint (__ARM_NR_BASE+1)   
  3. #define __ARM_NR_cacheflush (__ARM_NR_BASE+2)   
  4. #define __ARM_NR_usr26 (__ARM_NR_BASE+3)   
  5. #define __ARM_NR_usr32 (__ARM_NR_BASE+4)   
  6. #define __ARM_NR_set_tls (__ARM_NR_BASE+5)

实现方式是会汇编调用一个系统调用  __ARM_NR_cacheflush (0xf0002 )

参考:http://blog.csdn.net/roland_sun/article/details/36049307 

 

Arm格式的hook指令: 12个字节,

前面4个字节是:  LDR pc, [pc, #0],pc寄存器读出的值是当前值+8,所以这一句是把jump[2]对应的地址加载到 pc 寄存器,这一句指令执行后,程序计数器执行 jump[2], 即 h->patch ,即hook函数。  jump[1] 的值是用来填充空间的,可以任意
 h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
                h->jump[1] = h->patch; // 新的hook函数地址放在 hook指令的第4到12个字节
                h->jump[2] = h->patch;

 

Thumb格式的hook指令:20个字节,

首先,hook函数的地址被存放在 jumpt数组(char型)的第16,20这4个字节处。

                h->jumpt[1] = 0xb4; 
                h->jumpt[0] = 0x30; // push {r4,r5}
                h->jumpt[3] = 0xa5;
                h->jumpt[2] = 0x03; // add r5, pc, #12
                h->jumpt[5] = 0x68;
                h->jumpt[4] = 0x2d; // ldr r5, [r5]
                h->jumpt[7] = 0xb0;
                h->jumpt[6] = 0x02; // add sp,sp,#8
                h->jumpt[9] = 0xb4;
                h->jumpt[8] = 0x20; // push {r5}
                h->jumpt[11] = 0xb0;
                h->jumpt[10] = 0x81; // sub sp,sp,#4
                h->jumpt[13] = 0xbd;
                h->jumpt[12] = 0x20; // pop {r5, pc}
                h->jumpt[15] = 0x46;
                h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
                memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));

 

posted @ 2015-02-10 16:08  cgj  阅读(5579)  评论(1编辑  收藏  举报