• linux内核钩子--khook


    简介

    本文介绍github上的一个项目khook,一个可以在内核中增加钩子函数的框架,支持x86。项目地址在这里:https://github.com/milabs/khook

    本文先简单介绍钩子函数,分析这个工具的用法,然后再分析代码,探究实现原理

    钩子

    假设在内核中有一个函数,我们想截断他的执行流程,比如说对某文件的读操作。这样就可以监控对这个文件的读操作。这就是钩子。通过插入一个钩子函数,可以截断程序正常的执行流程,做自己的想做的操作,可以仅仅只做一个监控,也可以彻底截断函数的执行。

    khook的用法

    引入头文件

    #include "khook/engine.c"

    在kbuild/makefile中加入,这是一个链接控制脚本,后面会具体说明这个脚本的内容

    ldflags-y += -T$(src)/khook/engine.lds

    使用khook_init()和khook_cleanup()对挂钩引擎进行初始化和注销

    在内核中的函数有两种

    • 一种是在某一个头文件中已经被包含了,也就是内核已经定义了函数声明,这样只需要包含内内容的头文件就可以使用该函数
    • 另一种是没有声明,只是.c文件内部使用的函数

    对于已知原型的函数,包含头文件后,使用下面的代码就可以定义一个钩子函数

    #include <linux/fs.h> // has inode_permission() proto
    KHOOK(inode_permission);
    static int khook_inode_permission(struct inode *inode, int mask)
    {
            int ret = 0;
            ret = KHOOK_ORIGIN(inode_permission, inode, mask);
            printk("%s(%p, %08x) = %d
    ", __func__, inode, mask, ret);
            return ret;
    }

    对于原型未知的函数,则需要使用下面的方式(这里的头文件不是函数原型所在的文件,是参数所用结构体定义的位置)

    #include <linux/binfmts.h> // has no load_elf_binary() proto
    KHOOK_EXT(int, load_elf_binary, struct linux_binprm *);
    static int khook_load_elf_binary(struct linux_binprm *bprm)
    {
            int ret = 0;
            ret = KHOOK_ORIGIN(load_elf_binary, bprm);
            printk("%s(%p) = %d
    ", __func__, bprm, ret);
            return ret;
    }

    可以函数,假设原函数名字为fun,则自定义的fun的钩子函数名字必须为khook_fun,然后根据函数类型不同使用不同钩子定义方式

    原理分析

    先上作者github上的两张图

    未加入钩子之前的正常执行流程

    CALLER
    | ...
    | CALL X -(1)---> X
    | ...  <----.     | ...
    ` RET       |     ` RET -.
                `--------(2)-'

    加入钩子之后的执行流程

    CALLER
    | ...
    | CALL X -(1)---> X
    | ...  <----.     | JUMP -(2)----> STUB.hook
    ` RET       |     | ???            | INCR use_count
                |     | ...  <----.    | CALL handler -(3)------> HOOK.fn
                |     | ...       |    | DECR use_count <----.    | ...
                |     ` RET -.    |    ` RET -.              |    | CALL origin -(4)------> STUB.orig
                |            |    |           |              |    | ...  <----.             | N bytes of X
                |            |    |           |              |    ` RET -.    |             ` JMP X + N -.
                `------------|----|-------(8)-'              '-------(7)-'    |                          |
                             |    `-------------------------------------------|----------------------(5)-'
                             `-(6)--------------------------------------------'

    好,分析第二张图,X的第一条指令被替换成JUMP的跳转指令,另外,还可以知道多了3个部分STUB.hook、HOOK.fn、STUB.orig,他们的含义分别是

    STUB.hook:框架自定义的钩子函数模板,有4部分,除了引用的维护,还有3一条跳转,8一条返回。3是跳转到HOOK.fn

    HOOK.fn:这是使用者自定义的钩子函数,在上面的例子中,这个函数被定义成khook_inode_permission、khook_load_elf_binary。这里的4就是KHOOK_ORIGIN,钩子替换下来的原函数地址,一般来说,自定义的钩子函数最后也会调用原函数,用来保证正常的执行流程不会出错

    STUB.orig:框架自定义的钩子函数模板,由于X的第一条指令被替换成JUMP的跳转指令,要正常执行X,则需要先执行被替换的几个字节,然后回到X,也就是图中的过程5

    所以说,整体的思路就是,替换掉需要钩掉的函数的前几个字节,替换成一个跳转指令,让X开始执行的时候跳转到框架自定义的STUB代码部分,STUB再调用用户自定义的钩子函数。然后又会执行原先被跳转指令覆盖的指令,最后回到被钩掉的函数的正常执行逻辑

    源码分析

    khook结构

    先看一个结构体,khook,表示一个钩子,比较难理解的就是addr_map,因为我们需要对函数的内容进行重新,需要将这个函数的内容映射到一个可以访问的虚拟地址,addr_map就是这个虚拟地址,后面覆盖为jump就需要向这个地址写

    /*
    代表一个内核钩子
    fn:钩子函数
    name:符号名字
    addr:符号地址
    addr_map:符号地址被映射的虚拟地址
    orig:原函数
    */
    typedef struct {
        void            *fn;        // handler fn address
        struct {
            const char    *name;        // target symbol name
            char        *addr;        // target symbol addr (see khook_lookup_name)
            char        *addr_map;    // writable mapping of target symbol
        } target;
        void            *orig;        // original fn call wrapper
    } khook_t;

    先从用户定义钩子函数的入口开始分析,也就是KHOOK和KHOOK_EXT

    /*
    格式规定
    假设原函数名字为fun
    则自定义的fun的钩子函数名字必须为khook_fun
    */
    #define KHOOK_(t)                            
        static inline typeof(t) khook_##t; /* forward decl */        
        khook_t                                
        __attribute__((unused))                        
        __attribute__((aligned(1)))                    
        __attribute__((section(".data.khook")))                
        KHOOK_##t = {                            
            .fn = khook_##t,                    
            .target.name = #t,                    
        }
    /*
    有两种类型的函数
    1、头文件中包含了函数原型,则在代码中包含头文件就行了
    2、写在.c文件,但是.h文件中没有定义,则需要通过KHOOK_EXT来定义钩子函数
    */
    #define KHOOK(t)                            
        KHOOK_(t)
    #define KHOOK_EXT(r, t, ...)                        
        extern r t(__VA_ARGS__);                    
        KHOOK_(t)

    __attribute__((unused)表示可能不会用到

    __attribute__((aligned(1)))表示一字节对齐

    __attribute__((section(".data.khook")))表示这个结构需要被分配到.data.khook节中

    可以明白KHOOK就是做了一个格式规定,然后保证这个结构被分配到.data.khook节中

    KHOOK_EXT则是加入一个函数声明,这样未声明的函数就可以被使用了

    在上面的钩子函数中,还用到了一个宏,含义根据khook就可以明白

    /*
    传入原函数的名字和参数,KHOOK_ORIGIN就可以当做原函数来执行
    */
    #define KHOOK_ORIGIN(t, ...)                        
        ((typeof(t) *)KHOOK_##t.orig)(__VA_ARGS__)

    链接脚本

    关注一个问题,使用说明中,有一个条件,加入一个链接脚本

    ldflags-y += -T$(src)/khook/engine.lds

    这里看看这个链接脚本

    SECTIONS
    {
        .data : {
            KHOOK_tbl = . ;
            *(.data.khook)
            KHOOK_tbl_end = . ;
        }
    }

    engine.c中看到所有的钩子都被分配到.data.khook节中
    下面这个脚本的含义是将所有.data.khook的内容都放在.data节之中
    .这个字符表示的是当前定位器符号的位置,所以KHOOK_tbl指向的是.data.khook开头,KHOOK_tbl_end指向的是KHOOK_tbl_end的结尾

    以下脚本将输出文件的text section定位在0×10000, data section定位在0×8000000:

    SECTIONS
    {
    . = 0×10000;
    .text : { *(.text) }
    . = 0×8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
    }

    解释一下上述的例子:
    . = 0×10000 : 把定位器符号置为0×10000 (若不指定, 则该符号的初始值为0).
    .text : { *(.text) } : 将所有(*符号代表任意输入文件)输入文件的.text section合并成一个.text section, 该section的地址由定位器符号的值指定, 即0×10000.
    . = 0×8000000 :把定位器符号置为0×8000000
    .data : { *(.data) } : 将所有输入文件的.data section合并成一个.data section, 该section的地址被置为0×8000000.
    .bss : { *(.bss) } : 将所有输入文件的.bss section合并成一个.bss section,该section的地址被置为0×8000000+.data section的大小.
    连接器每读完一个section描述后, 将定位器符号的值*增加*该section的大小. 注意: 此处没有考虑对齐约束.

    综上所述,这个链接脚本定义了两个变量表示钩子表的起始和结束地址,KHOOK_tbl和KHOOK_tbl_end

    STUB

    然后看另一个结构体,STUB

    typedef struct {
    #pragma pack(push, 1)
        union {
            unsigned char _0x00_[ 0x10 ];
            atomic_t use_count;
        };
        union {
            unsigned char _0x10_[ 0x20 ];
            unsigned char orig[0];
        };
        union {
            unsigned char _0x30_[ 0x40 ];
            unsigned char hook[0];
        };
    #pragma pack(pop)
        unsigned nbytes;
    } __attribute__((aligned(32))) khook_stub_t;

    根据上一节介绍的原理可以知道,一个钩子函数一定会有一个STUB

    而这个STUB会被初始化为stub.inc或stub32.inc。也就是stub的模板。

    内核指令操作函数

    用到了两个内核中操作指令的函数,两个函数的功能是获取某个地址的指令,用struct insn表示,和获取这个指令的长度

    /**
     下面是内核关于这两个函数的说明
     insn_init() - initialize struct insn
     @insn:    &struct insn to be initialized
     @kaddr:    address (in kernel memory) of instruction (or copy thereof)
     @x86_64:    !0 for 64-bit kernel or 64-bit app
    
    insn_get_length() - Get the length of instruction
    @insn:    &struct insn containing instruction
    
    If necessary, first collects the instruction up to and including the
    immediates bytes.
    */
    static struct {
        typeof(insn_init) *init;
        typeof(insn_get_length) *get_length;
    } khook_arch_lde;
    
    //寻找到这两个函数的地址
    static inline int khook_arch_lde_init(void) {
        khook_arch_lde.init = khook_lookup_name("insn_init");
        if (!khook_arch_lde.init) return -EINVAL;
        khook_arch_lde.get_length = khook_lookup_name("insn_get_length");
        if (!khook_arch_lde.get_length) return -EINVAL;
        return 0;
    }
    
    //获取地址p的指令的长度,先调用insn_init获得insn结构,然后调用get_length得到指令长度,结果存放在insn的length字段
    static inline int khook_arch_lde_get_length(const void *p) {
        struct insn insn;
        int x86_64 = 0;
    #ifdef CONFIG_X86_64
        x86_64 = 1;
    #endif
    #if defined MAX_INSN_SIZE && (MAX_INSN_SIZE == 15) /* 3.19.7+ */
        khook_arch_lde.init(&insn, p, MAX_INSN_SIZE, x86_64);
    #else
        khook_arch_lde.init(&insn, p, x86_64);
    #endif
        khook_arch_lde.get_length(&insn);
        return insn.length;
    }

    查找符号表

    内核中有一个全局的符号表kallsyms,可以通过/proc/kallsyms来查询,也可以通过system.map来获取内核编译时期形成的静态符号表。

    在内核中,同样可以使用函数kallsyms_on_each_symbol来查询符号表,这个函数被封装成了下面两个部分

    //查询符号表的函数
    static int khook_lookup_cb(long data[], const char *name, void *module, long addr)
    {
        int i = 0; while (!module && (((const char *)data[0]))[i] == name[i]) {
            if (!name[i++]) return !!(data[1] = addr);
        } return 0;
    }
    /*
    利用kallsyms_on_each_symbol可以查询符号表,只需要传入查询函数就可以了
    data[0]表示要查询的地址
    data[1]表示结果
    */
    static void *khook_lookup_name(const char *name)
    {
        long data[2] = { (long)name, 0 };
        kallsyms_on_each_symbol((void *)khook_lookup_cb, data);
        return (void *)data[1];
    }

    前面说到,由于是需要符号符号执行的内存,所以需要给这个符号执行的地址分配一个虚拟地址,这个操作封装在下面这个函数中

    //为符号所在的物理内存建立一个虚拟地址的映射
    static void *khook_map_writable(void *addr, size_t len)
    {
        struct page *pages[2] = { 0 }; // len << PAGE_SIZE
        long page_offset = offset_in_page(addr);
        int i, nb_pages = DIV_ROUND_UP(page_offset + len, PAGE_SIZE);
    
        addr = (void *)((long)addr & PAGE_MASK);
        for (i = 0; i < nb_pages; i++, addr += PAGE_SIZE) {
            if ((pages[i] = is_vmalloc_addr(addr) ?
                 vmalloc_to_page(addr) : virt_to_page(addr)) == NULL)
                return NULL;
        }
    
        addr = vmap(pages, nb_pages, VM_MAP, PAGE_KERNEL);
        return addr ? addr + page_offset : NULL;
    }

    初始化流程

    要使用框架,先要调用khook_init函数,它定义在engine.c中

    int khook_init(void)
    {
        void *(*malloc)(long size) = NULL;
    
        //为所有钩子的stub分配内存
        malloc = khook_lookup_name("module_alloc");
        if (!malloc || KHOOK_ARCH_INIT()) return -EINVAL;
    
        khook_stub_tbl = malloc(KHOOK_STUB_TBL_SIZE);
        if (!khook_stub_tbl) return -ENOMEM;
        memset(khook_stub_tbl, 0, KHOOK_STUB_TBL_SIZE);
    
        //从kallsyms寻找到每个钩子的地址
        khook_resolve();
    
        //建立映射
        khook_map();
        //停止所有机器,执行khook_sm_init_hooks
        stop_machine(khook_sm_init_hooks, NULL, NULL);
        khook_unmap(0);
    
        return 0;
    }

    这个函数,做了以下几件事

    1、分配所有STUB需要用到的内存

    2、查找符号表,获得所有需要钩住的函数的地址。然后建立虚拟地址的映射

    3、执行khook_sm_init_hook,建立好STUB和khook的关联,保证他们的跳转逻辑

    查找符号的地址函数很简单,看下面

    //对KHOOK_tbl中每一个钩子都获得他们在内核中的地址
    static void khook_resolve(void)
    {
        khook_t *p;
        KHOOK_FOREACH_HOOK(p) {
            p->target.addr = khook_lookup_name(p->target.name);
        }
    }

    同样建立映射的函数

    //为钩子建立好虚拟地址的映射
    static void khook_map(void)
    {
        khook_t *p;
        KHOOK_FOREACH_HOOK(p) {
            if (!p->target.addr) continue;
            p->target.addr_map = khook_map_writable(p->target.addr, 32);
            khook_debug("target %s@%p -> %p
    ", p->target.name, p->target.addr, p->target.addr_map);
        }
    }

    最重要的就是第3步

    static int khook_sm_init_hooks(void *arg)
    {
        khook_t *p;
        KHOOK_FOREACH_HOOK(p) {
            if (!p->target.addr_map) continue;
            khook_arch_sm_init_one(p);
        }
        return 0;
    }

    核心实现在下面的函数

    static inline void khook_arch_sm_init_one(khook_t *hook) {
        khook_stub_t *stub = KHOOK_STUB(hook);
        //E9是相对跳转。FF是绝对跳转。
        if (hook->target.addr[0] == (char)0xE9 ||
            hook->target.addr[0] == (char)0xCC) return;
    
        BUILD_BUG_ON(sizeof(khook_stub_template) > offsetof(khook_stub_t, nbytes));
        memcpy(stub, khook_stub_template, sizeof(khook_stub_template));
        //设置第3步
        stub_fixup(stub->hook, hook->fn);
    
        //一条相对跳转指令为5,所以必须保存下至少5个字节的指令
        while (stub->nbytes < 5)
            stub->nbytes += khook_arch_lde_get_length(hook->target.addr + stub->nbytes);
    
        memcpy(stub->orig, hook->target.addr, stub->nbytes);
        //设置第5步
        x86_put_jmp(stub->orig + stub->nbytes, stub->orig + stub->nbytes, hook->target.addr + stub->nbytes);
        //设置第2步
        x86_put_jmp(hook->target.addr_map, hook->target.addr, stub->hook);
        hook->orig = stub->orig; // the only link from hook to stub
    }

    可以看到这就是设置stub的内容。

    1、先是用khook_stub_template的内容填充stub,这就是stub.inc

    2、第3步中stub是需要跳转到自定义钩子函数的,stub_fixup填充这个地址

    3、保存函数的前一部分内容,这一部分必须大于5个字节

    4、设置返回到原函数的地址

    5、用跳转指令覆盖原函数的内容

    然后用到的几个辅助函数在这里

    // place a jump at addr @a from addr @f to addr @t
    static inline void x86_put_jmp(void *a, void *f, void *t)
    {
        *((char *)(a + 0)) = 0xE9;
        *(( int *)(a + 1)) = (long)(t - (f + 5));
    }
    
    //这个数组的内容写在stub.inc或是stub32.inc中,表示一个stub的模板
    static const char khook_stub_template[] = {
    # include KHOOK_STUB_FILE_NAME
    };
    
    //看stub32.inc中,后部有几个连续的0xca,从这之后再写入value,钩子函数地址
    static inline void stub_fixup(void *stub, const void *value) {
        while (*(int *)stub != 0xcacacaca) stub++;
        *(long *)stub = (long)value;
    }
  • 相关阅读:
    静态路由
    ARP攻击原理及解决方法
    JS post提交表单
    ARP攻击
    三层交换机与路由器的比较
    动态域名解析软件——花生壳(peanuthull)
    DateTime.Now.Ticks 属性
    我的WCF之旅(1):创建一个简单的WCF程序
    window程序或服务的工作原理
    内网外网同时上的方法静态路由
  • 原文地址:https://www.cnblogs.com/likaiming/p/10970543.html
Copyright © 2020-2023  润新知