You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

修改sys_call_table挂钩sys_read系统调用无效问题求助

内核sys_call_table挂钩sys_read失败问题排查

问题背景

尝试通过定位sys_call_table并修改sys_read的表项,将其指向自定义内核函数实现系统调用挂钩,已排除kprobes方式,专注于sys_call_table的修改实现。

环境信息

  • 系统:Ubuntu 24.04(内核6.8.0-35-generic)、Ubuntu 22.04默认内核
  • 运行环境:VMware虚拟机、物理硬件

代码实现

内核模块代码

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/syscalls.h>
#include <linux/version.h>

typedef asmlinkage long (*t_syscall)(const struct pt_regs *);
unsigned long cr0;
unsigned long **__sys_call_table;
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
typedef asmlinkage int (*orig_getdents64_t)(unsigned int,
        struct linux_dirent64 *, unsigned int);   
asmlinkage long (*original_syscall)(const struct pt_regs *);
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
static kallsyms_lookup_name_t kallsyms_lookup_name_ptr;

static struct kprobe kp2 = {
    .symbol_name = "__x64_sys_read"
};

unsigned long *get_syscall_address(unsigned long *sys_call_table, int syscall_number);
asmlinkage long hooked_syscall(const struct pt_regs *regs);


#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static inline void
write_cr0_forced(unsigned long val)
{
    unsigned long __force_order;

    asm volatile(
        "mov %0, %%cr0"
        : "+r"(val), "+m"(__force_order));
}
#endif

static inline void
unprotect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    write_cr0_forced(cr0 & ~0x00010000);
#else
    write_cr0(cr0 & ~0x00010000);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
    update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
            section_size, PAGE_KERNEL);
#endif
}

static inline void
protect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    write_cr0_forced(cr0);
#else
    write_cr0(cr0);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
    update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
            section_size, PAGE_KERNEL_RO);

#endif
}

asmlinkage long hooked_syscall(const struct pt_regs *regs) {
    printk(KERN_INFO "Syscall hooked!\n");
    return original_syscall(regs);
}

static unsigned long **find_sys_call_table(void) {
    unsigned long **sct;
    sct = (unsigned long **)kallsyms_lookup_name_ptr("sys_call_table");
    return sct;
}


static int __init kprobe_init(void)
{
    int ret;
    cr0 = read_cr0();
    ret = register_kprobe(&kp);
    if (ret < 0)
        return ret;

    kallsyms_lookup_name_ptr = (kallsyms_lookup_name_t)kp.addr;

    __sys_call_table = find_sys_call_table();

    if (!__sys_call_table) {
        printk(KERN_ERR "Couldn't find sys_call_table.\n");
        return -1;
    }

    printk("__sys_call_table address : %px\n", __sys_call_table);

    unprotect_memory();
    original_syscall = (void *)__sys_call_table[__NR_read];
    printk("__NR_READ : %px\n", original_syscall);
    printk("HOOKED FUNCTION : %px\n", (unsigned long *)hooked_syscall);
    __sys_call_table[__NR_read] = (unsigned long *)hooked_syscall;
    
    /// Double check
    original_syscall = (void *)__sys_call_table[__NR_read];
    printk("__NR_READ : %px\n", original_syscall);

    protect_memory();

    // Extra check
    int ret2 = register_kprobe(&kp2);
    if (ret2 < 0)
        return ret2;

    printk("%px\n", kp2.addr);

    unregister_kprobe(&kp);
    unregister_kprobe(&kp2);

    return 0;
}

static void __exit kprobe_exit(void)
{
}

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

Makefile

# Name of the kernel module
obj-m += sct.o

# List of source files for the module
hello_world-objs := sct.c

# Path to the kernel source tree
KDIR := /lib/modules/$(shell uname -r)/build

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

现象

  1. 通过kprobe获取kallsyms_lookup_name地址,成功定位sys_call_table,读取的sys_read地址与/proc/kallsyms对比一致。
  2. 修改__NR_read对应的表项为自定义函数后,调试打印确认sys_call_table表项已变更,但触发sys_read时dmesg无预期的printk输出,系统也未崩溃。
  3. 额外通过kprobe检测sys_read地址,发现仍指向原地址。

疑问

  1. 代码中遗漏了什么关键步骤?
  2. 通过修改sys_call_table挂钩系统调用的方法在当前内核版本中是否仍可行?

问题解答

核心原因:内核安全机制拦截了sys_call_table的修改

在你使用的Ubuntu 22.04/24.04内核中,默认启用了多项防御机制,直接修改找到的sys_call_table并不会实际影响系统调用的执行路径:

  • 现代Linux内核(5.10+)默认将sys_call_table设置为只读,部分发行版还会维护影子sys_call_table,实际系统调用会走这个受保护的副本,而非你修改的原始表。
  • 你修改后打印的表项变更只是内存中的临时修改,但内核执行系统调用时会跳转到影子表的原始入口,导致挂钩不生效。

修改sys_call_table的方法是否还可行?

仅在关闭内核安全机制的测试场景下可行,具体需要:

  1. 编译内核时关闭以下配置选项:
    • CONFIG_RODATA_FULL_DEFAULT_ENABLED(只读数据段强制保护)
    • CONFIG_SYSCALL_TABLE_PROTECTED(sys_call_table专属保护)
    • x86平台关闭CONFIG_PAGE_TABLE_ISOLATION,ARM64平台关闭CONFIG_UNMAP_KERNEL_AT_EL0
  2. 启动内核时添加参数nopti nospectre_v2(关闭页表隔离类防御)

这种方式仅适合学习测试,生产环境绝对不推荐,现代内核官方推荐使用eBPF或kprobes/uprobes实现系统调用拦截,这些是合法且受支持的方案。

代码中的其他问题

  1. Makefile目标不匹配:hello_world-objs := sct.c应改为sct-objs := sct.c,否则编译会因模块名与目标文件不匹配失败。
  2. 未恢复sys_call_table:模块卸载函数kprobe_exit中没有将__NR_read表项恢复为原始地址,若后续内核机制变化,会直接导致内核崩溃。
  3. 系统调用入口命名:6.x内核x86_64平台的系统调用入口为__x64_sys_read,需确认__NR_read对应的表项索引是否与实际入口匹配。

测试验证方法

若需验证修改sys_call_table的可行性,可下载Ubuntu内核源码,关闭上述安全选项后重新编译内核,使用新内核启动系统后加载模块,此时挂钩应能正常触发printk输出。

内容的提问来源于stack exchange,提问作者Jelal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:35:55