目录

简介

钩子

khook的用法

原理分析

khook结构

链接脚本

STUB

内核指令操作函数

查找符号表

初始化流程


简介

本文介绍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 addressstruct {const char    *name;        // target symbol namechar        *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 appinsn_get_length() - Get the length of instruction
@insn:    &struct insn containing instructionIf 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_64x86_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);
#elsekhook_arch_lde.init(&insn, p, x86_64);
#endifkhook_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_SIZElong 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_hooksstop_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;
}

linux 内核钩子 khook 项目介绍相关推荐

  1. 【转】LINUX内核编译步骤详细介绍

    from: http://blog.csdn.net/do2jiang/article/details/4201203 LINUX内核编译步骤详细介绍 到www.kernel.org 下载 Linux ...

  2. LINUX内核编译步骤详细介绍

    LINUX内核编译步骤详细介绍 到www.kernel.org 下载 Linux-2.6.18.2.tar.bz2 tar –jxvf  Linux-2.6.18.2.tar.bz2 -C /usr/ ...

  3. Linux内核的整体架构介绍

    1. 前言 本文是"Linux内核分析"系列文章的第一篇,会以内核的核心功能为出发点,描述Linux内核的整体架构,以及架构之下主要的软件子系统.之后,会介绍Linux内核源文件的 ...

  4. linux内核源码系统调用有多少个,Linux内核源码目录介绍

    Linux的内核源代码可以从很多途径得到,我一般常常去kernel.org看看.一般来讲,在安装的linux系统下,usr/src/linux目录下的东西就是内核源代码.源码的版本号有一个非常简单的编 ...

  5. linux上有趣的项目,介绍几款有趣的开源项目

    Craft Minecraft这款游戏相信不少人都玩过吧,此游戏的简单开源版本,适用Windows, Mac OS X 和 Linux,有兴趣的可以去研究一番. verlet-js verlet-js ...

  6. linux内核的一些常识介绍

    以下内容源于朱友鹏嵌入式课程的学习,如有侵权,请告知删除. linux内核下载地址:www.kernel.org 一.内核功能.内核发行版 1.到底什么是操作系统 (1)linux.windows.a ...

  7. Linux内核源码目录介绍

    Linux内核目录如下: arch: 不同平台体系结构的相关代码 block:设备驱动 certs:与认证和签名相关代码 crypto:内核常用压缩算法.常用加密算法等等源代码 document:描述 ...

  8. linux内核hz,linux内核中的HZ介绍

    时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 值来设定,HZ 是一个体系依赖的值,在 中定义或该文件包含的某个子平台相关文件中.作为通用的规则,即便如果知道 HZ 的值,在编程时 ...

  9. ipxe u盘启动linux内核,iPXE的使用介绍

    iPXE的使用介绍.md 概述 PXE是Intel提出的,用以网卡启动.通过DHCP获取IP以及TFTP获取启动文件. gPXE/iPXE是PXE的扩展版,支持HTTP等多种获取手段.iPXE由gPX ...

最新文章

  1. Python知识点4——if分支与while循环
  2. 微信小程序转百度小程序修改
  3. .NET引用类型与值类型
  4. 【长沙集训】2017.10.10
  5. 余额宝上线新功能,可以跟亲朋好友一起攒钱了
  6. android 获取sd卡视频文件名,android – 如何获取SD卡上的视频列表
  7. Citrix 客户端登录出现wfshell.exe - 应用程序错误的解决方法
  8. FinTech领域实践:乐维监控助力西南某上市城商行IT运维转型升级!
  9. 零基础物联网开发,踩坑无数,得到这份宝典 | 原力计划
  10. undefined reference to symbol 'dlsym@@GLIBC_2.17' libdl.so: error adding symbols: DSO missing from c
  11. 批量修改图片格式类型
  12. eeupdate 更新MAC地址
  13. DPDK Release 20.05
  14. 解决VMware虚拟机宿主机与虚拟机通讯慢
  15. 亚马逊后台付款表(Custom Transaction)详解
  16. Matlab 方位角计算
  17. K8S 完全安装手册
  18. 2012年10月【美国】自驾游
  19. apktools使用
  20. Bitmap高效加载

热门文章

  1. 人生,是一种无法抗拒的前进
  2. 字段缩写ti表示什么_【滴水研究】解码TI(一)
  3. Linux Kernel TCP/IP Stack — L2 Layer — Linux VLAN device for 802.1.q(虚拟局域网)
  4. Strongswan — IPSec 的 Linux 软件实现
  5. 5G NGC — SMF 会话管理功能
  6. DPDK — 在 NFV 中的应用
  7. Linux_KVM虚拟机
  8. error while loading shared libraries: libz.so.1错误
  9. PowerShell批量设置PATH环境变量
  10. ARMS V4.3发布,应用监控全新支持内存快照分析,全息排查等功能。