如何用Kprobe获取Linux内核函数参数?X86_64参数异常解惑
内核Kprobe挂钩__x64_sys_openat时参数读取异常问题
问题背景
在X86_64架构的Fedora系统(Linux 6.11.10-200.fc40.x86_64)上,使用Kprobe挂钩内核函数__x64_sys_openat(int dfd, const char *path, struct open_how *how)。按照X86_64调用约定,函数参数应依次存于RDI、RSI、RDX寄存器,但读取到的寄存器值完全不符合预期:
[23374.116347] "pre 1st arg dfd :" [di]: 0xFFFFAE19081A3F58 (-90052443357352) valid 0 [23374.116351] "pre 2nd arg *path:" [si]: 0x101 (257) valid 0 [23374.116353] "pre 3th arg *how :" [dx]: 0x0 (0) valid 0
预期第一个参数dfd是小正整数,后两个是指针,但实际第一个参数是指针风格的值,第二个是整数,尝试将第二个参数作为字符串读取直接导致内核崩溃。
相关代码
/* * syscall-steal.c * * System call "stealing" sample. * * Disables page protection at a processor level by changing the 16th bit * in the cr0 register (could be Intel specific). */ #include <linux/delay.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/moduleparam.h> /* which will have params */ #include <linux/unistd.h> /* The list of system calls */ #include <linux/cred.h> /* For current_uid() */ #include <linux/uidgid.h> /* For __kuid_val() */ #include <linux/version.h> /* For the current (process) structure, we need this to know who the * current user is. */ #include <linux/sched.h> #include <linux/uaccess.h> /* If you have tried to use the syscall table to intercept syscalls and it * doesn't work, you can try to use Kprobes to intercept syscalls. * Set USE_KPROBES_PRE_HANDLER_BEFORE_SYSCALL to 1 to register a pre-handler * before the syscall. */ #include <linux/kprobes.h> #define MAX_FILENAME_SIZE 1024 /* UID we want to spy on - will be filled from the command line. */ static uid_t uid = -1; module_param(uid, int, 0644); /* * rdi: 1st argument (saved in regs->di?) * rsi: 2nd argument (saved in regs->si?) * rdx: 3th argument (saved in regs->dx?) * * __x64_sys_openat(int dfd, const char *path, struct open_how *how) * dfd => rdi * path => rsi * how => rdx * */ #define VALID_ADDRESS(desc, name) pr_info("%s [%s]: 0x%lX (%ld) valid %d\n", #desc, #name, regs->name, regs->name, virt_addr_valid(regs->name)); static int openat_pre_handler(struct kprobe *p, struct pt_regs *regs) { char filename[MAX_FILENAME_SIZE]; if (__kuid_val(current_uid()) != uid) { return 0; } strcpy(filename, "No file"); pr_info("openat called by %d\n", uid); VALID_ADDRESS("pre 1st arg dfd :", di); VALID_ADDRESS("pre 2nd arg *path:", si); VALID_ADDRESS("pre 3th arg *how :", dx); if(copy_from_user(filename, (void*)regs->si, MAX_FILENAME_SIZE-1)) { pr_info("copy user data failed\n"); } pr_info("Opening file: %s\n", filename); //dump_stack(); return 0; } static struct kprobe openat_kprobe = { .symbol_name = "__x64_sys_openat", .pre_handler = openat_pre_handler, }; static int __init syscall_steal_start(void) { pr_info("Using kprobes\n"); int err; err = register_kprobe(&openat_kprobe); if (err) { pr_err("register_kprobe() on openat failed: %d\n", err); } return err; pr_info("Spying on UID:%d\n", uid); return err; } static void __exit syscall_steal_end(void) { unregister_kprobe(&openat_kprobe); msleep(500); } module_init(syscall_steal_start); module_exit(syscall_steal_end); MODULE_LICENSE("GPL");
提问
能否从pt_regs结构体的保存寄存器中获取正确参数?为何当前方式无效?
解答
问题根源
你读取的寄存器字段完全错误。在X86_64架构的Linux内核pt_regs结构体中,64位寄存器对应的字段是rdi、rsi、rdx,而你代码中使用的di、si、dx是32位寄存器的字段,仅保留了寄存器的低32位值,这直接导致读取到的参数完全错乱:
- 原本存在64位寄存器
rdi的dfd参数,你读取di只能拿到低32位,结果变成了类似指针的大数值 - 原本是64位用户空间指针的
path参数,读取si只拿到低32位,变成了无意义的小整数,用它做copy_from_user自然会访问非法地址,引发内核崩溃
修复方案
修改代码中的寄存器字段名,将32位寄存器字段替换为对应的64位字段:
- 调整地址验证宏,同时修正用户空间地址的类型判断:
#define VALID_ADDRESS(desc, name) pr_info("%s [%s]: 0x%lX (%ld) valid %d\n", #desc, #name, regs->name, regs->name, virt_addr_valid((void __user *)regs->name));
- 修改handler中的寄存器引用:
VALID_ADDRESS("pre 1st arg dfd :", rdi); VALID_ADDRESS("pre 2nd arg *path:", rsi); VALID_ADDRESS("pre 3th arg *how :", rdx);
- 调整
copy_from_user的参数类型:
if(copy_from_user(filename, (void __user *)regs->rsi, MAX_FILENAME_SIZE-1)) { pr_info("copy user data failed\n"); }
额外优化点
模块初始化代码中有冗余的return err;语句,会导致pr_info("Spying on UID:%d\n", uid);永远不会执行,建议移除第一个return err;。
内容的提问来源于stack exchange,提问作者Erjan
相关产品推荐
相关产品推荐

