使用kprobe进行linux内核下hook

什么是kprobe

kprobe 可以用来跟踪内核函数中某一条指令在运行前运行后的情况。详细了解请自行百度

除了kprobe,还有kretprobe(函数返回时触发,可修改返回值),jprobe(探测函数的入参值)

通俗的说,把hook点替换成int 3断点,CPU执行代码跑到这个位置时触发异常,内核捕获异常,达到hook目的

❕注意,内核需要开启 CONFIG_KPROBES=y 新版内核基本已经默认开启 本文基于内核5.15.0-46-generic

如何使用

结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct kprobe {
struct hlist_node hlist;

/* list of kprobes for multi-handler support */
struct list_head list;

/*count the number of times this probe was temporarily disarmed */
unsigned long nmissed;

/* location of the probe point */
kprobe_opcode_t *addr; // hook函数的地址,需要hook中途指令才使用这个,一般都用symbol_name

/* Allow user to indicate symbol name of the probe point */
const char *symbol_name; //符号名 要Hook的内核函数

/* Offset into the symbol */
unsigned int offset;

/* Called before addr is executed. */
kprobe_pre_handler_t pre_handler; //hook函数调用时触发

/* Called after addr is executed, unless... */
kprobe_post_handler_t post_handler; //hook函数调用后触发,注意,在这里修改返回值也没用,需要用kretprobe

/*
* ... called if executing addr causes a fault (eg. page fault).
* Return 1 if it handled fault, otherwise kernel will see it.
*/
kprobe_fault_handler_t fault_handler; //执行失败时触发

/*
* ... called if breakpoint trap occurs in probe handler.
* Return 1 if it handled break, otherwise kernel will see it.
*/
kprobe_break_handler_t break_handler; //断点时触发

/* Saved opcode (which has been replaced with breakpoint) */
kprobe_opcode_t opcode; //保存的hook前原始指令

/* copy of the original instruction */
struct arch_specific_insn ainsn;

/*
* Indicates various status flags.
* Protected by kprobe_mutex after this kprobe is registered.
*/
u32 flags;
};

关键函数:

1
2
3
4
5
6
int register_kprobe(struct kprobe *kp)      //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点
void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测

简单演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
.symbol_name = "do_fork", //要hook的函数名(内核函数)
};

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
printk(KERN_INFO "handler_pre\n");
return 0;

}
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
printk(KERN_INFO "handler_post\n");
}
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
return 0;

}
static int __init kprobe_init(void) //驱动加载时执行的函数
{
int ret;
kp.pre_handler = handler_pre; // 指派执行前hook函数
kp.post_handler = handler_post;// 指派执行后hook函数
kp.fault_handler = handler_fault; //指派执行失败函数

ret = register_kprobe(&kp); //注册hook,注册完成后addr应该被赋值,且返回0
if (ret < 0) {
printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
return ret;
}
printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
return 0;
}

static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

你已经学会了内核hook了,快去写一个hids吧(不是

使用kretprobe实现内核层文件保护

原理:

所有用户层程序都会使用openat来打开文件,走到内核层实际调用的是do_sys_openat2,修改do_sys_openat2的返回值为EBADF即可防止读取文件

话不多说 直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

#include <linux/binfmts.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/fs_struct.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/kthread.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("CyberSecurity");

#ifdef DEBUG
#define DBGINFO(m, ...) pr_debug(KBUILD_MODNAME "-dbg: " m "\n", ##__VA_ARGS__)
#else
#define DBGINFO(m, ...)
#endif

static struct kretprobe kp = {
.kp.symbol_name = "do_sys_openat2",
.data_size = PATH_MAX //kretprobe私有数据,方便在不同函数间传递数据
};

static int handler_pre(struct kretprobe_instance *p, struct pt_regs *regs) //注意,结构和kprobe稍微有点不一样
{
int slen = 0;
char *filename = NULL;
const unsigned char *ps_cwd = NULL;
const char *__user us_filename = (const void *)regs->si; // gcc x64 约定 rsi 为第二个参数
if (IS_ERR_OR_NULL(us_filename))
return -EFAULT;

slen = strnlen_user(us_filename, PATH_MAX); // strlen 内核版 , 用户空间的东西都需要单独操作,内核不可直接访问用户空间
if (!slen || slen > PATH_MAX)
return -EFAULT;

filename = kzalloc(slen + 1, GFP_ATOMIC);
if (PTR_ERR_OR_ZERO(filename))
return -EFAULT;

if (copy_from_user(filename, us_filename, slen))
{
return -EFAULT;
}

if (strstr(filename, "/run") != NULL || strstr(filename, "/proc") != NULL) //过滤非必要文件
return 0;

memcpy(p->data, filename, slen);
ps_cwd = current->mm->owner->fs->pwd.dentry->d_name.name; //获取程序执行目录,注意,openat参数为相对目录时需要自己拼接程序执行目录才能得到绝对路径
DBGINFO("OPEN AT %s By PID:%d,CWD:%s", filename, current->mm->owner->pid, ps_cwd);
kfree(filename);
return 0;
}
extern int close_fd(unsigned fd);
static int handler_post(struct kretprobe_instance *ri, struct pt_regs *regs)
{
char *filename = ri->data;
if (IS_ERR_OR_NULL(filename))
return -EFAULT;

if (strcmp(filename, "test.txt") == 0) //修改openat返回值 达到保护目的
{
unsigned int fd = regs_return_value(regs);
close_fd(fd);
regs_set_return_value(regs, -EBADF);
}
return 0;
}

static __init int kprobe_init(void)
{
int ret;
kp.handler = handler_post;
kp.entry_handler = handler_pre;
ret = register_kretprobe(&kp);
if (ret < 0)
{
printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
return ret;
}
printk("Planted return probe at %s: %p\n",
kp.kp.symbol_name, kp.kp.addr);
return 0;
}
static __exit void kprobe_exit(void)
{
unregister_kretprobe(&kp);
}

module_init(kprobe_init);
module_exit(kprobe_exit);

演示:

测试环境:Ubuntu 22.04 5.15.0-46-generic

加载驱动前
加载驱动后
日志