您的位置:首页 > 运维架构 > Linux

linux内核钩子--khook

2019-06-03 22:28 2947 查看

简介

本文介绍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;
}

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: