简介

本文介绍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 = ;
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 = ;
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 -()---> X
| ... <----. | ...
` RET | ` RET -.
`--------()-'

加入钩子之后的执行流程

CALLER
| ...
| CALL X -()---> X
| ... <----. | JUMP -()----> STUB.hook
` RET | | ??? | INCR use_count
| | ... <----. | CALL handler -()------> HOOK.fn
| | ... | | DECR use_count <----. | ...
| ` RET -. | ` RET -. | | CALL origin -()------> STUB.orig
| | | | | | ... <----. | N bytes of X
| | | | | ` RET -. | ` JMP X + N -.
`------------|----|-------()-' '-------()-' | |
| `-------------------------------------------|----------------------()-'
`-()--------------------------------------------'

好,分析第二张图,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())) \
__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
{
. = ×;
.text : { *(.text) }
. = ×;
.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[];
};
union {
unsigned char _0x30_[ 0x40 ];
unsigned char hook[];
};
#pragma pack(pop)
unsigned nbytes;
} __attribute__((aligned())) 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 ;
} //获取地址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 = ;
#ifdef CONFIG_X86_64
x86_64 = ;
#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 = ; while (!module && (((const char *)data[]))[i] == name[i]) {
if (!name[i++]) return !!(data[] = addr);
} return ;
}
/*
利用kallsyms_on_each_symbol可以查询符号表,只需要传入查询函数就可以了
data[0]表示要查询的地址
data[1]表示结果
*/
static void *khook_lookup_name(const char *name)
{
long data[] = { (long)name, };
kallsyms_on_each_symbol((void *)khook_lookup_cb, data);
return (void *)data[];
}

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

//为符号所在的物理内存建立一个虚拟地址的映射
static void *khook_map_writable(void *addr, size_t len)
{
struct page *pages[] = { }; // 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 = ; 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, , KHOOK_STUB_TBL_SIZE); //从kallsyms寻找到每个钩子的地址
khook_resolve(); //建立映射
khook_map();
//停止所有机器,执行khook_sm_init_hooks
stop_machine(khook_sm_init_hooks, NULL, NULL);
khook_unmap(); return ;
}

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

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, );
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 ;
}

核心实现在下面的函数

static inline void khook_arch_sm_init_one(khook_t *hook) {
khook_stub_t *stub = KHOOK_STUB(hook);
//E9是相对跳转。FF是绝对跳转。
if (hook->target.addr[] == (char)0xE9 ||
hook->target.addr[] == (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 < )
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 + )) = 0xE9;
*(( int *)(a + )) = (long)(t - (f + ));
} //这个数组的内容写在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内核数据结构学习总结

    目录 . 进程相关数据结构 ) struct task_struct ) struct cred ) struct pid_link ) struct pid ) struct signal_stru ...

  2. Linux内核中流量控制

    linux内核中提供了流量控制的相关处理功能,相关代码在net/sched目录下:而应用层上的控制是通过iproute2软件包中的tc来实现, tc和sched的关系就好象iptables和netfi ...

  3. Linux内核IP层的报文处理流程(一)

    本文主要讲解了Linux内核IP层的整体架构和对从网卡接受的报文处理流程,使用的内核的版本是2.6.32.27 为了方便理解,本文采用整体流程图加伪代码的方式对Linxu内核中IP整体实现架构和对网卡 ...

  4. Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://bl ...

  5. linux内核netfilter模块分析之:HOOKs点的注册及调用

    转自;http://blog.csdn.net/suiyuan19840208/article/details/19684883 -1: 为什么要写这个东西?最近在找工作,之前netfilter 这一 ...

  6. 现在的 Linux 内核和 Linux 2.6 的内核有多大区别?

    作者:larmbr宇链接:https://www.zhihu.com/question/35484429/answer/62964898来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转 ...

  7. Linux内核二层数据包接收流程

    本文主要讲解了Linux内核二层数据包接收流程,使用的内核的版本是2.6.32.27 为了方便理解,本文采用整体流程图加伪代码的方式从内核高层面上梳理了二层数据包接收的流程,希望可以对大家有所帮助.阅 ...

  8. linux内核中socket的创建过程源码分析(总结性质)

    在漫长地分析完socket的创建源码后,发现一片浆糊,所以特此总结,我的博客中同时有另外一篇详细的源码分析,内核版本为3.9,建议在阅读本文后若还有兴趣再去看另外一篇博文.绝对不要单独看另外一篇. 一 ...

  9. Linux内核转发技术

    前言 在linux内核中,通常集成了带有封包过滤和防火墙功能的内核模块, 不同内核版本的模块名称不同, 在2.4.x版本及其以后的内核中, 其名称为iptables, 已取代了早期的ipchains和 ...

随机推荐

  1. BZOJ1005--[HNOI2008]明明的烦恼(树的prufer编码)

    1005: [HNOI2008]明明的烦恼 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 5768  Solved: 2253[Submit][Stat ...

  2. mysql:unknown variable 'default-character-set=utf8'

    1.修改my.cnf后,执行 service mysql restart 重启数据库失败 service mysql restart Shutting down MySQL.. SUCCESS! St ...

  3. 使用Spring基于应用层实现读写分离(一)基础版

    背景 我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案, 其中一个是主库,负责写入数据,我们称之为:写库: 其它都是从库,负责读取数据 ...

  4. Go Iris 中间件

    Iris 中间件 当我们在 iris 中讨论中间件时,我们讨论的是在HTTP请求生命周期中在主处理程序代码之前和/或之后的运行代码. 实现中间件功能,有下面这样两种方式: 方式一: 我们可以通过按顺序 ...

  5. CNN中感受野大小的计算

    1 感受野的概念 从直观上讲,感受野就是视觉感受区域的大小.在卷积神经网络中,感受野的定义是 卷积神经网络每一层输出的特征图(feature map)上的像素点在原始图像上映射的区域大小. 2 感受野 ...

  6. 关于域名解析|A记录|CNAME等

    1. A记录 又称IP指向,用户可以在此设置子域名并指向到自己的目标主机地址上,从而实现通过域名找到服务器. 说明: ·指向的目标主机地址类型只能使用IP地址: 附加说明: 1) 泛域名解析 即将该域 ...

  7. spring cloud之Eureka不能注销docker部署的实例

    1 起因 事件的起因是这样的,我们在微服务改造的过程中,选择将服务注册到eureka中,开发的时候还好,使用场景是这样的: 在idea中启动服务,成功注册到eureka,关闭服务,eureka成功注销 ...

  8. C# 后台服务 web.config 中 项“ConnectionString”已添加。问题

    是自己在一网站下建了虚拟目录.原本网站为空,后来自己改了路径,有了默认配置很久后打开原本ok的虚拟目录,坑爹了.杯具了.代码:ConfigurationManager.ConnectionString ...

  9. WampServer 下载以及安装问题 以及配置远程连接MYSQL

    WampServer 3.0 下载: http://dl.pconline.com.cn/download/52877-1.html 碰到的问题DDL无法添加,解决方法:MSVCR110.DLL fo ...

  10. MFS分布式文件系统

    一.MFS概述: MooseFS(moose 驼鹿)是一款网络分布式文件系统.它把数据分散在多台服务器上,但对于用户来讲,看到的只是一个源.MFS也像其他类unix文件系统一样,包含了层级结构(目录树 ...