Android Hook框架adbi的分析(2)--- inline Hook的实现
阅读原文时间:2021年09月06日阅读:1

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74452308

一、 Android Hook框架adbi源码中inline Hook实现部分的代码结构

Android Hook框架adbi源码中inline Hook部分的实现代码结构示意图如下所示,hijack代码部分是前面的博客中提到的root下Android跨进程注入so的注入工具,instruments\base代码部分为inline Hook的操作实现,instruments\example代码部分则为Android Hook框架adbi实现Hook系统调用函数epoll_wait的使用例子。

二、 adbi源码中inline Hook实现的详细步骤分析

1 .inline Hook函数被调用的时机

在so库文件加载的时候,会首先执行.init段的构造函数,因此在编写注入到Android目标进程中的so库文件时要定义该构造函数并实现在此处调用inline Hook。inline Hook实现就是在so库文件注入到Android进程中被加载调用该构造函数时被执行的。Android Hook框架adbi基于模块化的设计思想,该构造函数的编写是放在自定义Hook函数的接口中来实现的,在这里就是在Hook函数代码示例instruments\example\epoll.c中定义和实现的。

2 .inline Hook操作的Hook函数实现

inline Hook操作的Hook函数是在adbi\instruments\base\hook.c中实现的,在Hook目标pid进程的目标函数时,定义了一个全局的静态变量,保存被Hook目标函数相关的信息,用以对目标函数的Hook操作和函数还原,具体的结构定义如下:

struct hook_t {

    // arm指令模式的12字节Hook
    unsigned int jump[3];   /* 要修改的hook指令(Arm) */
    unsigned int store[3]; /* 被修改的原指令(Arm) */

    // thumb指令模式的20字节Hook
    unsigned char jumpt[20]; /* 要修改的hook指令(Thumb) */
    unsigned char storet[20]; /* 被修改的源指令(Thumb) */

    unsigned int orig; /* 被hook的目标函数地址 */
    unsigned int patch; /* hook的自定义函数地址 */

    unsigned char thumb; /* 表明被hook函数使用的指令集,1为Thumb,0为Arm */
    unsigned char name[128]; /* 被hook的函数名 */

    // 用于存放其他的数据(未使用)
    void *data;
};

在对目标进程的目标函数进行Hook之前,使用hijack注入工具中查找mprotect函数调用地址的方法,获取被Hook目标函数的调用地址,具体就是通过解析目标函数所在的so库文件中的“.symtab”或者“.dynsym”节,获取到库中所有的符号信息,查找得到目标函数的调用地址的RVA,加上目标函数所在so库文件的加载基地址就是目标函数的调用地址VA了。

// 对目标pid进程的指定函数进行Hook处理

// h为记录Hook信息的静态变量的指针,pid为被Hook的目标进程的pid,libname为被Hook函数所在的so库文件名称,
// funcname为被Hook的目标函数,hook_arm为被Hook的函数的arm指令模式的替换函数,hook_thumb为被Hook的函数的thumb指令模式的替换函数
int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
    unsigned long int addr;
    int i;

    // 在指定pid进程的指定so库中查找将被Hook的目标函数funcname的调用地址VA即addr
    if (find_name(pid, funcname, libname, &addr) < 0) {

        log("can't find funcname: %s\n", funcname)
        return 0;
    }

    log("hooking:   %s = 0x%lx ", funcname, addr)
    // 保存被Hook的目标函数的名称
    strncpy(h->name, funcname, sizeof(h->name)-1);

Arm处理器支持两种指令集,一是基本的Arm指令集,二是Thumb指令集。因此,为了正确的Hook目标函数,不至于导致被Hook的Android进程崩溃,在Hook目标进程的目标函数之前还需要判断进程当前所处的arm指令模式。判断的方法是看函数跳转地址的最后两位是不是全0,如果是,那就是Arm模式的指令,如果最后两位不全为0,那就是Thumb模式的指令。由于Hook目标函数时的跳转指令需要4字节对齐,所以对目标函数调用地址进行4字节取模来判断执行的指令集。


Arm与Thumb之间的状态切换是通过专用的跳转交换指令BX来实现。BX指令以通用寄存器(R0~R15)为操作数,通过拷贝Rn到PC实现绝对跳转。BX利用Rn寄存器中目的地址值的最后一位判断跳转后的状态,如果为“1”表示跳转到Thumb指令集的函数中,如果为“0”表示跳转到Arm指令集的函数中。而Arm指令集的每条指令是32位,即4个字节,也就是说Arm指令的地址肯定是4的倍数,最后两位必定为“00”。所以,直接就可以将从符号表中获得的调用地址模4,看是否为0来判断要修改的函数是用Arm指令集还是Thumb指令集。


上面这段解释说明引用自博主Roland_Sun的博文Android平台下hook框架adbi的研究(下),特地摘抄过来帮助分析和理解。

    // 通过判断函数跳转地址的最后两位是不是全0,来判断指令的运行模式,
    // 如果后两位全是的0,那就一定是用Arm指令,如果后两位不全为0,那一定是用Thumb指令集

    if (addr % 4 == 0)
    {
        // Arm指令模式的HooK目标函数的处理
        ······
    }
    else
    {
        // Thumb指令模式的Hook目标函数的处理
        ······
    }

Arm指令模式HooK目标函数的处理是通过12字节指令覆盖来完成的,简单的来说就是将目标函数调用地址处的前12字节的指令先保存起来,然后使用12字节的Hook跳转指令进行覆盖。


Arm指令模式下Hook目标函数的处理,先将自定义hook函数和要被hook目标函数的地址保存起来。然后生成hook的代码指令,只有3个4字节就是12个字节,第一个dword字节是代码指令“LDR pc, [pc, #0]”,由于pc寄存器读出的值实际上是当前指令地址加8,所以这里是把jump[2]的值加载进pc寄存器中,而jump[2]处保存的是自定义hook函数的地址。因此,jump[0~3]实际上保存的是跳转到自定义hook函数的代码指令。再下面,将被hook函数的前3个4字节保存下来,方便后面函数的恢复。最后,将跳转指令写到被hook目标函数的前12字节。这样以后,当要调用被hook函数的时候,实际执行的指令就是跳转到自定义hook函数处。


    // Arm指令模式的HooK目标函数的处理
    if (addr % 4 == 0) {

        log("ARM using 0x%lx\n", (unsigned long)hook_arm)

        // arm指令模式
        h->thumb = 0;
        // 自己实现的Hook函数地址
        h->patch = (unsigned int)hook_arm;
        // 被Hook目标函数的原函数地址
        h->orig = addr;

        // 用于Hook目标函数的调用地址为新地址hook_arm
        h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
        h->jump[1] = h->patch;
        // pc寄存器读出的值实际上是当前指令地址加8
        // 把jump[2]的值加载进pc寄存器
        h->jump[2] = h->patch;

        // 保存原目标函数的12字节指令,用于函数的恢复
        for (i = 0; i < 3; i++)
            h->store[i] = ((int*)h->orig)[i];

        // 覆盖目标函数的12字节指令为Hook函数指令,实现对目标函数的Hook
        for (i = 0; i < 3; i++)
            ((int*)h->orig)[i] = h->jump[i];
    }

Thumb指令模式下Hook目标函数的处理方式和arm模式下的Hook处理一样,但是基于thumb指令的长度不同,在对目标函数代码指令的覆盖上有所不同,Thumb指令模式下Hook目标函数需要20字节的Hook指令,Hook目标函数的操作是先保存目标函数的前20字节的指令,然后使用20个字节的Hook指令对目标函数进行覆盖处理。

    // Thumb指令模式的Hook目标函数的处理
    else {

        // 对自定义Hook函数的调用地址进行指令模式的判断
        if ((unsigned long int)hook_thumb % 4 == 0)
            log("warning hook is not thumb 0x%lx\n", (unsigned long)hook_thumb)

        // thumb指令模式
        h->thumb = 1;
        log("THUMB using 0x%lx\n", (unsigned long)hook_thumb)

        // 保存用于Hook目标函数的调用地址为新地址hook_thumb
        h->patch = (unsigned int)hook_thumb;
        // 保存被Hook目标函数的原函数地址
        h->orig = addr; 

        // 保存寄存器r5,r6的值用于恢复环境(r6在高地址,r5在地址)
        h->jumpt[1] = 0xb4;
        h->jumpt[0] = 0x60; // push {r5,r6}
// 将PC寄存器的值加上12赋值给r5。加上的立即数必须是4的倍数,而加上8又不够,只能加12。
// 这样的话,读出的PC寄存器的值是当前指令地址加上4,再加上12的话,那么可以算出来r5寄存器的值实际指向的是jumpt[18],而不是jumpt[16]了。
// 这里还有一点需要注意,对于Thumb的“Add Rd, Rp, #expr”指令来说,如果Rp是PC寄存器的话,那么PC寄存器读出的值应该是(当前指令地址+4)& 0xFFFFFFFC,
// 也就是去掉最后两位,算下来正好可以减去2。但这里也有个假设,就是被hook函数的起始地址必须是4字节对齐的,哪怕被hook函数是使用Thumb指令集编写的。
        h->jumpt[3] = 0xa5;
        h->jumpt[2] = 0x03; // add r5, pc, #12 (比较难理解)
        // 将保存在jumpt[16]处的hook函数地址加载到r5寄存器中
        h->jumpt[5] = 0x68;
        h->jumpt[4] = 0x2d; // ldr r5, [r5]
        // 降低栈顶,恢复到初始的状态,释放内存空间
        h->jumpt[7] = 0xb0;
        h->jumpt[6] = 0x02; // add sp,sp,#8
        // 用保存的自定义hook函数地址覆盖原来压入的r6的值,r5的值暂时不受影响
        h->jumpt[9] = 0xb4;
        h->jumpt[8] = 0x20; // push {r5}
        // 抬高栈顶,r5的值被保护
        h->jumpt[11] = 0xb0;
        h->jumpt[10] = 0x81; // sub sp,sp,#4
        // 进行出栈操作,pc寄存器得到自定义的Hook函数的地址,r5的值还是原来的
        h->jumpt[13] = 0xbd;
        h->jumpt[12] = 0x20; // pop {r5, pc}
        // 仅仅用于4字节对齐的填充,只是因为前面的add指令只能加4的倍数
        h->jumpt[15] = 0x46;
        h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary

        // 用于存放自定义Hook函数的调用地址(4字节)
        memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int));
        // sub 1 to get real address,获取到thumb指令模式下函数的真实调用地址
        unsigned int orig = addr - 1;
        // 保存被Hook目标函数的原始thumb指令
        for (i = 0; i < 20; i++) {

            h->storet[i] = ((unsigned char*)orig)[i];
            //log("%0.2x ", h->storet[i])
        }
        //log("\n")

        // 覆盖被Hook目标函数的指令为自定义的Hook函数指令
        for (i = 0; i < 20; i++) {

            ((unsigned char*)orig)[i] = h->jumpt[i];
            //log("%0.2x ", ((unsigned char*)orig)[i])
        }

    }

Thumb指令模式下Hook目标函数的Hook指令比较难理解,当初也是思考了好久才想明白了一些,主要参考的也是博主Roland_Sun的解释和分析。知道自己很多地方说不清楚,因此有关Thumb指令模式下Hook指令的理解就借用博主Roland_Sun的理解,在此分析基础上进行修改帮助理解。


和对Arm指令集的处理非常相似,只不过跳转指令换成了Thumb。和Arm的处理不同,这里是通过pop指令来修改PC寄存器的值实现函数的Hook跳转操作。

1.首先,入栈r6和r5寄存器的值,并在arm指令操作中寄存器编号大在栈的高地址编号小在栈的低地址,将r5压栈是因为后面的指令执行修改了r5寄存器的值,压栈后方便以后恢复,而将r6寄存器压栈纯粹是为了要保留一个位置。

2.接着,将PC寄存器的值加上12赋值给r5,加上的立即数必须是4的倍数,而加上8又不够,只能加12。这样的话,读出的PC寄存器的值是当前指令地址加上4,再加上12的话,那么可以算出来r5寄存器的值实际指向的是jumpt[18],而不是jumpt[16]了。

3.这里还有一点需要注意,对于Thumb模式下的“Add Rd, Rp, #expr”指令来说,如果Rp是PC寄存器的话,那么PC寄存器读出的值应该是(当前指令地址+4)& 0xFFFFFFFC,也就是去掉最后两位,算下来正好可以减去2。但这里也有个假设,就是被hook函数的起始地址必须是4字节对齐的,哪怕被hook函数使用Thumb指令集编写的。

4.再下面的指令目的就是将保存在jumpt[16]处的自定义hook函数地址覆盖r6寄存器在栈中的值,栈中r5寄存器的值不受影响,仅仅用于后面寄存器环境的恢复。所以,下面的“pop {r5, pc}”指令刚好可以完成恢复r5寄存器并且修改PC寄存器的值,从而实现跳转到自定义hook函数地址处执行。

5.接下来的指令(从jumpt[14])完全是多余的了,完全不会执行到,只是因为前面的add指令只能加4字节的倍数。最后,还有一点不同的是,因为被hook函数是Thumb指令集,所以其真正的内存映射地址是其符号地址减去1。


Hook操作覆盖目标函数的代码指令以后还需要刷新指令缓存。现代的处理器都有指令缓存,用来提高代码指令的执行效率,ARM处理器也一样也有指令缓存机制。虽然目标进程内存中被Hook目标函数的代码指令已经改变,但是cache中的代码指令可能仍为原有的代码指令,再进行代码指令执行时还是优先执行缓存中的代码指令,使得被Hook目标函数修改的指令得不到执行,所以需要手动刷新cache中的代码指令,解决的方法是触发Android系统隐藏刷新cache的系统调用。

// 调用Android系统的私有系统调用__ARM_NR_cacheflush实现缓存指令的刷新
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"                      // 修改寄存器列表
        );
}

对目标函数进行Hook操作的时候还需要考虑对目标函数Hook的恢复还原和再次对目标函数进行Hook操作的处理。adbi的源码文件adbi\instruments\base\hook.c中,hook_precall函数就是对目标函数进行Hook后的恢复还原,hook_postcall函数就是对目标函数进行恢复还原之后的再次Hook操作。

// 进行thumb或者arm模式被Hook目标函数指令的恢复即实现函数Hook的恢复
void hook_precall(struct hook_t *h)
{
    int i;

    // thumb指令模式被Hook目标函数的指令的恢复
    if (h->thumb) {

        // 获取被Hook目标函数的真实调用地址
        unsigned int orig = h->orig - 1;
        // 进行thumb指令模式被Hook指令的恢复
        for (i = 0; i < 20; i++) {

            ((unsigned char*)orig)[i] = h->storet[i];
        }

    } else {

        // 进行arm指令模式被Hook指令的恢复
        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));
}

// 进行thumb或者arm指令模式Hook目标函数的指令覆盖即实现函数的Hook
void hook_postcall(struct hook_t *h)
{
    int i;

    if (h->thumb) {

        // 获取thumb指令模式函数真实的调用地址
        unsigned int orig = h->orig - 1;
        // 进行thumb指令模式Hook目标函数指令的覆盖
        for (i = 0; i < 20; i++)
            ((unsigned char*)orig)[i] = h->jumpt[i];

    } else {

        // 进行arm指令模式Hook目标函数指令的覆盖
        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));
}

3 .自定义Hook函数Thumb模式和Arm模式的实现

很显然,在上面的分析中提到的Hook目标函数实现操作中需要提供Thumb模式和Arm模式的自定义Hook函数的实现。在我们进行Hook目标函数的操作中并不知道要被Hook的目标函数是那种模式的指令集,只能通过被Hook目标函数的调用地址来判断,因此需要提供Thumb模式和Arm模式的自定义Hook函数的实现。那么,如何控制将代码编译成Arm指令集还是是Thumb指令集呢?


Android NDK默认情况下将C代码编译成Thumb指令,如果想将C代码编译成Arm指令集,有两种方法:

1.在Android.mk文件中添加上“LOCAL_ARM_MODE := arm”,这样会默认将所有的C代码编译成Arm指令集。

2.前面的方法只能将所有代码全部编译成Arm指令集,如果想一部分代码编译成Arm,一部分编译成Thumb就力不从心了。想要达到这个目的,可以将那些你想编译成Arm指令集的C代码文件名字后面加上一个“.arm”后缀。而其它的没有加上“.arm”后缀的C文件将使用“LOCAL_ARM_MODE”指定的指令集编译,默认情况下是Thumb。注意,这里只是在“LOCAL_SRC_FILES”里列出的C文件名后加上“.arm”后缀就可以了,不要真的去改那个要编译的C文件名。


adbi\instruments\example目录下的实例是用第二种方法指定“epoll.c”编译成Thumb指令,而“epoll_arm.c”编译成Arm指令集,同时连接通过base编译出的静态库。

三、 adbi源码中inline Hook实现的流程总结

  1. 在so库文件加载注入到Android目标进程中调用so库文件的构造函数时,调用inline Hook操作Hook目标进程的目标函数;
  2. 通过遍历目标进程的内存布局信息,获取到被Hook目标函数所在的so库文件的内存加载基地址以及解析该so库文件的“.symtab”或者“.dynsym”节获取被Hook目标函数的RVA,进而获取到被Hook目标函数的调用地址;
  3. 通过判断被Hook目标函数调用地址的最后两位是不是全0,来判断被Hook目标函数的指令运行模式是Thumb模式还是Arm模式;
  4. 如果是Arm指令集模式,先保存被Hook目标函数的前12个字节的代码指令,然后使用12字节的Hook代码指令覆盖被Hook目标函数的前12个字节;
  5. 如果是Thumb指令集模式,先保存被Hook目标函数的前20个字节的代码指令,然后使用20字节的Hook代码指令覆盖被Hook目标函数的前20个字节;
  6. 被Hook目标函数的代码指令被Hook修改以后,调用Android系统的隐藏系统调用cacheflush刷新指令缓存,使inline Hook操作生效,待到下一次被Hook目标函数被调用就是调用的我们自定义的Hook函数。

本篇博文中使用到带有注释分析的Android Hook框架adbi的源码下载地址:http://download.csdn.net/detail/qq1084283172/9893002

参考链接:

Android平台下hook框架adbi的研究(下)

Android Arm Inline Hook