返回

解决Linux kprobe报错: Hook标记't'函数无效(-22 EINVAL)

Linux

深入解析 “can’t kprobe “t” functions” 问题及解决方案

在 Linux 内核模块开发中,kprobe 是一种强大的调试工具,允许开发者动态地 Hook 到内核函数,从而监控或修改内核行为。但有时尝试 Hook 一些在 /proc/kallsyms 中标记为 t (text) 的函数时会遇到 register_kprobe() 返回 -22 (EINVAL) 的错误,即“参数无效”。本文将深入探讨该问题的原因,并提供多种解决方案。

问题分析:t 标记函数与 kprobe

/proc/kallsyms 中函数标记为 t 表示该函数属于内核代码段(text section)。通常情况下,kprobe 可以 Hook 这些函数。但当 register_kprobe() 返回 -22 时,意味着尝试 Hook 的函数存在一些特殊性,使得 kprobe 无法正常工作。常见原因包括:

  1. 函数被内联: 编译器优化可能将函数内联到调用它的函数中,导致 kprobe 无法找到独立的函数实体。
  2. 函数被优化: 编译器可能对函数进行了各种优化,例如尾调用优化、栈帧省略等,导致 kprobe 无法正确处理。
  3. 函数地址不可探测: 在某些内核版本或配置下,一些函数地址可能无法被 kprobe 安全访问。
  4. 内核版本限制: 老旧内核版本(例如 2.6.32 和 3.x 系列)可能对 kprobe 的支持存在一些限制。
  5. 受限函数: 一些内核安全模块(如 SELinux)或内核加固机制可能限制了对某些函数的 kprobe 操作。
  6. 地址空间布局随机化 (KASLR): 虽然 KASLR 本身不影响 kprobe 的使用,但在 kprobe 使用函数地址而不是符号名称时,需要正确处理 KASLR 偏移。

解决方案:多种方法应对 kprobe 限制

针对上述问题,可以尝试以下解决方案:

  1. 禁用函数内联:

    原理: 通过编译器选项避免目标函数被内联,确保 kprobe 可以找到独立的函数实体。

    操作步骤: 修改内核模块 Makefile,添加 -fno-inline 编译选项,重新编译内核模块。

    代码示例 (Makefile):

    obj-m += gap.o
    KBUILD_CFLAGS += -Wno-unused-function
    CCFLAG-y := -g -O3 -fno-inline -flto -march=native -mtune=native -fomit-frame-pointer -funroll-loops
    
    all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    安全建议: 禁用内联可能会略微增加内核大小,并可能影响性能。仅在必要时使用此选项,并在测试完成后恢复原始编译选项。

  2. 降低优化级别:

    原理: 减少编译器对目标函数的优化,避免因优化导致的 kprobe 失败。

    操作步骤: 修改内核模块 Makefile,降低优化级别,例如将 -O3 改为 -O1-O0

    代码示例 (Makefile):

    obj-m += gap.o
    KBUILD_CFLAGS += -Wno-unused-function
    CCFLAG-y := -g -O1 -flto -march=native -mtune=native
    
    all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    安全建议: 降低优化级别可能会降低内核性能,建议仅在调试阶段使用较低优化级别,并在完成后恢复原始优化选项。

  3. 使用 kprobe 地址:

    原理: 直接通过函数地址注册 kprobe,绕过符号查找过程。但需要注意 KASLR 影响。

    操作步骤:

    • 使用 cat /proc/kallsyms | grep <function_name> 获取函数地址。
    • 在内核模块中,定义一个指向函数地址的指针。
    • 将 kprobe 的 kp.addr 设置为该指针的值。

    代码示例 (C):

    #include <linux/kprobes.h>
    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/sched.h>
    #include <linux/version.h>
    
    static int kprobe_seq_ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs);
    
    #ifdef KPROBE_ON_ADDRESS
    // Assume the address you got from cat /proc/kallsyms is ffffffff81e29760
    static unsigned long kprobe_seq_next_addr = (unsigned long)0xffffffff81e29760; 
    #endif
    
    static struct kretprobe kretseqnext =
    {
        .handler = kprobe_seq_ret_handler,
    #ifdef KPROBE_ON_ADDRESS
        .kp.addr = (kprobe_opcode_t *)kprobe_seq_next_addr,
    #else
        .kp.symbol_name = "kprobe_seq_next",
    #endif
    };
    
    static int kprobe_seq_ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
    {
        set_return_value(regs, 0);
        return 0;
    }
    
    static int __init kprobe_init(void)
    {
        int ret;
    
        ret = register_kretprobe(&kretseqnext);
        if (ret < 0) {
            printk("Failed registering kretprobe kprobe_seq_next %d\n", ret);
            return ret;
        }
    
        printk("Registering kprobe_seq_next\n");
    #ifdef KPROBE_ON_ADDRESS
            printk("kprobe_seq_next address: %px\n", (void *)kprobe_seq_next_addr);
    #endif
        return 0;
    }
    
    static void __exit kprobe_exit(void)
    {
        unregister_kretprobe(&kretseqnext);
        printk("Unregistering kretprobe kprobe_seq_next\n");
    }
    
    module_init(kprobe_init);
    module_exit(kprobe_exit);
    
    MODULE_LICENSE("GPL");
    

    修改 Makefile

    obj-m += gap.o
    KBUILD_CFLAGS += -Wno-unused-function -DKPROBE_ON_ADDRESS
    CCFLAG-y := -g -O3 -flto -march=native -mtune=native -fomit-frame-pointer -funroll-loops -finline-functions
    
    
    all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    操作步骤:

    • 获取目标函数的地址 sudo cat /proc/kallsyms | grep kprobe_seq_next
    • 根据输出,修改上面C代码段中 kprobe_seq_next_addr 变量值
    • 重新编译加载模块。

    安全建议: 使用硬编码地址可能导致模块在不同内核版本或配置下无法加载。建议结合内核版本判断或 KASLR 偏移计算来动态获取地址。

  4. 检查内核配置和安全模块:

    原理: 确认内核配置是否禁用了 kprobe 或启用了限制 kprobe 的安全模块。

    操作步骤:
    * 检查内核配置文件(如 .config)中是否包含 CONFIG_KPROBE_EVENT=y, 确认内核编译开启了 kprobe 功能
    * 确认没有其他安全模块(如 SELinux)限制对目标函数的 kprobe 操作,或者有的话,配置允许当前模块kprobe.
    * 查看是否有其他安全限制如 kernel.kptr_restrict, 若值为2或者3 , 请暂时将其修改为1, 或者0, sudo sysctl -w kernel.kptr_restrict=1, 并在完成kprobe调试后恢复原来的值。

  5. 升级内核版本:

    原理: 新版本内核通常会修复一些 kprobe 相关的 bug,并提供更好的兼容性。

    操作步骤: 考虑升级到较新的内核版本,尤其是对于