实战分享用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需要以下准备工作安装必要的开发工具和内核头文件sudo apt update sudo apt install build-essential linux-headers-$(uname -r)验证内核配置支持Kprobegrep CONFIG_KPROBES /boot/config-$(uname -r)输出应为CONFIG_KPROBESy准备示例工作目录mkdir kprobe_example cd kprobe_example touch kprobe_example.c Makefile3. 编写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) clean4. 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_example6. 高级技巧与性能优化在实际生产环境中使用Kprobe时需要考虑以下优化策略减少打印频率频繁的printk会影响性能使用静态缓冲区避免在探测处理程序中动态分配内存选择性监控通过PID过滤目标进程批量注册使用register_kprobes一次注册多个探测点性能对比测试方法perf stat -e probe:do_fork -a sleep 107. 常见问题排查问题1register_kprobe返回-2(ENOENT)解决方案检查符号名是否正确使用sudo cat /proc/kallsyms | grep 函数名验证问题2模块导致系统不稳定解决方案确保处理程序中没有阻塞操作简化处理逻辑问题3无法捕获预期函数可能原因函数被内联优化尝试禁用编译器优化EXTRA_CFLAGS -O08. 安全注意事项虽然Kprobe/Jprobe比传统Hook安全但仍需注意避免在生产环境关键路径上设置过多探测点探测处理程序应尽可能简单快速卸载模块前确保所有探测点已注销使用权限控制限制模块加载
实战分享:用Kprobe和Jprobe在Ubuntu 22.04上安全地Hook内核函数(附完整代码)
发布时间:2026/5/23 22:01:55
实战分享用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需要以下准备工作安装必要的开发工具和内核头文件sudo apt update sudo apt install build-essential linux-headers-$(uname -r)验证内核配置支持Kprobegrep CONFIG_KPROBES /boot/config-$(uname -r)输出应为CONFIG_KPROBESy准备示例工作目录mkdir kprobe_example cd kprobe_example touch kprobe_example.c Makefile3. 编写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) clean4. 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_example6. 高级技巧与性能优化在实际生产环境中使用Kprobe时需要考虑以下优化策略减少打印频率频繁的printk会影响性能使用静态缓冲区避免在探测处理程序中动态分配内存选择性监控通过PID过滤目标进程批量注册使用register_kprobes一次注册多个探测点性能对比测试方法perf stat -e probe:do_fork -a sleep 107. 常见问题排查问题1register_kprobe返回-2(ENOENT)解决方案检查符号名是否正确使用sudo cat /proc/kallsyms | grep 函数名验证问题2模块导致系统不稳定解决方案确保处理程序中没有阻塞操作简化处理逻辑问题3无法捕获预期函数可能原因函数被内联优化尝试禁用编译器优化EXTRA_CFLAGS -O08. 安全注意事项虽然Kprobe/Jprobe比传统Hook安全但仍需注意避免在生产环境关键路径上设置过多探测点探测处理程序应尽可能简单快速卸载模块前确保所有探测点已注销使用权限控制限制模块加载