实战分享:用Kprobe和Jprobe在Ubuntu 22.04上安全地Hook内核函数(附完整代码)
2026/5/23 22:01:43 网站建设 项目流程

实战分享:用Kprobe和Jprobe在Ubuntu 22.04上安全地Hook内核函数(附完整代码)

在Linux内核开发和安全分析领域,动态追踪技术正逐渐取代传统的直接修改内存方式。本文将带你探索如何利用Kprobe和Jprobe这两种内核官方支持的机制,在Ubuntu 22.04上实现安全、可靠的内核函数Hook。

1. 为什么选择Kprobe/Jprobe而非传统Hook方式

传统的内核Hook方法通常涉及直接修改系统调用表(sys_call_table)或函数指针,这种方式虽然直观,但存在诸多隐患:

  • 稳定性风险:直接内存修改可能导致内核崩溃
  • 安全限制:现代内核的写保护机制(CR0.WP)会阻止此类操作
  • 维护困难:不同内核版本需要不同的补丁方法

相比之下,Kprobe/Jprobe提供了以下优势:

特性传统HookKprobe/Jprobe
安全性
稳定性
内核版本兼容性优秀
调试支持有限完善
性能开销中等

提示:Kprobe允许在任意内核指令处设置断点,而Jprobe专门用于捕获函数调用和参数

2. 环境准备与依赖安装

在Ubuntu 22.04上使用Kprobe需要以下准备工作:

  1. 安装必要的开发工具和内核头文件:
sudo apt update sudo apt install build-essential linux-headers-$(uname -r)
  1. 验证内核配置支持Kprobe:
grep CONFIG_KPROBES /boot/config-$(uname -r)

输出应为CONFIG_KPROBES=y

  1. 准备示例工作目录:
mkdir kprobe_example && cd kprobe_example touch kprobe_example.c Makefile

3. 编写Kprobe模块完整代码

下面是一个监控do_fork函数的完整示例(适用于5.x内核):

#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> static struct kprobe kp = { .symbol_name = "do_fork", }; static int handler_pre(struct kprobe *p, struct pt_regs *regs) { printk(KERN_INFO "do_fork called by process %d\n", current->pid); return 0; } static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { printk(KERN_INFO "do_fork execution completed\n"); } static int __init kprobe_init(void) { int ret; kp.pre_handler = handler_pre; kp.post_handler = handler_post; ret = register_kprobe(&kp); 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");

配套的Makefile内容:

obj-m := kprobe_example.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

4. Jprobe实战:捕获系统调用参数

Jprobe更适合需要获取函数参数的场景。以下示例监控open系统调用:

#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/fs.h> static long jdo_open(const char __user *filename, int flags, umode_t mode) { char fname[256]; long copied = strncpy_from_user(fname, filename, sizeof(fname)-1); if (copied > 0) { fname[copied] = 0; printk(KERN_INFO "process %d opening file: %s\n", current->pid, fname); } jprobe_return(); return 0; } static struct jprobe jp = { .entry = jdo_open, .kp = { .symbol_name = "do_sys_open", }, }; static int __init jprobe_init(void) { int ret = register_jprobe(&jp); if (ret < 0) { printk(KERN_INFO "register_jprobe failed, returned %d\n", ret); return ret; } printk(KERN_INFO "Planted jprobe at %p\n", jp.kp.addr); return 0; } static void __exit jprobe_exit(void) { unregister_jprobe(&jp); printk(KERN_INFO "jprobe at %p unregistered\n", jp.kp.addr); } module_init(jprobe_init); module_exit(jprobe_exit); MODULE_LICENSE("GPL");

注意:从Linux 4.17开始,Jprobe已被废弃,建议使用Kprobe结合pt_regs获取参数

5. 编译、加载与测试

编译模块:

make

加载模块并查看输出:

sudo insmod kprobe_example.ko dmesg | tail -n 10

测试Jprobe示例时,可以尝试打开文件:

touch testfile cat testfile

卸载模块:

sudo rmmod kprobe_example

6. 高级技巧与性能优化

在实际生产环境中使用Kprobe时,需要考虑以下优化策略:

  • 减少打印频率:频繁的printk会影响性能
  • 使用静态缓冲区:避免在探测处理程序中动态分配内存
  • 选择性监控:通过PID过滤目标进程
  • 批量注册:使用register_kprobes一次注册多个探测点

性能对比测试方法:

perf stat -e 'probe:do_fork' -a sleep 10

7. 常见问题排查

问题1:register_kprobe返回-2(ENOENT)

  • 解决方案:检查符号名是否正确,使用sudo cat /proc/kallsyms | grep 函数名验证

问题2:模块导致系统不稳定

  • 解决方案:确保处理程序中没有阻塞操作,简化处理逻辑

问题3:无法捕获预期函数

  • 可能原因:函数被内联优化,尝试禁用编译器优化:
EXTRA_CFLAGS += -O0

8. 安全注意事项

虽然Kprobe/Jprobe比传统Hook安全,但仍需注意:

  • 避免在生产环境关键路径上设置过多探测点
  • 探测处理程序应尽可能简单快速
  • 卸载模块前确保所有探测点已注销
  • 使用权限控制限制模块加载

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询