zoukankan      html  css  js  c++  java
  • Android Hook框架adbi的分析(2)--- inline Hook的实现

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

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

    Android Hook框架adbi源码中inline Hook部分的实现代码结构示意图如下所示,hijack代码部分是前面的博客中提到的root下Android跨进程注入so的注入工具,instrumentsase代码部分为inline Hook的操作实现,instrumentsexample代码部分则为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函数代码示例instrumentsexampleepoll.c中定义和实现的。

    这里写图片描述

    这里写图片描述

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

    inline Hook操作的Hook函数是在adbiinstrumentsasehook.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
    ", 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
    ", (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
    ", (unsigned long)hook_thumb)
    
            // thumb指令模式
            h->thumb = 1;
            log("THUMB using 0x%lx
    ", (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("
    ")
    
            // 覆盖被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
    "          
            "mov     r1, %1
    "
            "mov     r7, %2
    "
            "mov     r2, #0x0
    "
            "svc     0x00000000
    "
            :
            :   "r" (begin), "r" (end), "r" (syscall) // 输入列表
            :   "r0", "r1", "r7"                      // 修改寄存器列表
            );
    }

    这里写图片描述

    对目标函数进行Hook操作的时候还需要考虑对目标函数Hook的恢复还原和再次对目标函数进行Hook操作的处理。adbi的源码文件adbiinstrumentsasehook.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文件名。


    adbiinstrumentsexample目录下的实例是用第二种方法指定“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

  • 相关阅读:
    Linux面试题汇总答案
    VMWARE ESXI 虚拟硬盘的格式:精简置备、厚置备延迟置零、厚置备置零
    [Python基础知识]正则
    [代码评审1]代码评审
    [EF2]Sneak Preview: Persistence Ignorance and POCO in Entity Framework 4.0
    [EF1]POCOs(Plain Old C# Object)Entity Framework 4.x: POCOs入门
    [网站性能3]SqlServer中Profiler的使用
    [网站性能2]Asp.net平台下网站性能调优的实战方案
    [网站性能1]对.net系统架构改造的一点经验和教训
    2.1 python使用MongoDB 示例代码
  • 原文地址:https://www.cnblogs.com/csnd/p/11800611.html
Copyright © 2011-2022 走看看