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\n", __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\n", __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\n", 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; }