修改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
现象
- 通过kprobe获取
kallsyms_lookup_name地址,成功定位sys_call_table,读取的sys_read地址与/proc/kallsyms对比一致。 - 修改
__NR_read对应的表项为自定义函数后,调试打印确认sys_call_table表项已变更,但触发sys_read时dmesg无预期的printk输出,系统也未崩溃。 - 额外通过kprobe检测
sys_read地址,发现仍指向原地址。
疑问
- 代码中遗漏了什么关键步骤?
- 通过修改
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的方法是否还可行?
仅在关闭内核安全机制的测试场景下可行,具体需要:
- 编译内核时关闭以下配置选项:
CONFIG_RODATA_FULL_DEFAULT_ENABLED(只读数据段强制保护)CONFIG_SYSCALL_TABLE_PROTECTED(sys_call_table专属保护)- x86平台关闭
CONFIG_PAGE_TABLE_ISOLATION,ARM64平台关闭CONFIG_UNMAP_KERNEL_AT_EL0
- 启动内核时添加参数
nopti nospectre_v2(关闭页表隔离类防御)
这种方式仅适合学习测试,生产环境绝对不推荐,现代内核官方推荐使用eBPF或kprobes/uprobes实现系统调用拦截,这些是合法且受支持的方案。
代码中的其他问题
- Makefile目标不匹配:
hello_world-objs := sct.c应改为sct-objs := sct.c,否则编译会因模块名与目标文件不匹配失败。 - 未恢复sys_call_table:模块卸载函数
kprobe_exit中没有将__NR_read表项恢复为原始地址,若后续内核机制变化,会直接导致内核崩溃。 - 系统调用入口命名:6.x内核x86_64平台的系统调用入口为
__x64_sys_read,需确认__NR_read对应的表项索引是否与实际入口匹配。
测试验证方法
若需验证修改sys_call_table的可行性,可下载Ubuntu内核源码,关闭上述安全选项后重新编译内核,使用新内核启动系统后加载模块,此时挂钩应能正常触发printk输出。
内容的提问来源于stack exchange,提问作者Jelal
相关产品推荐
相关产品推荐

