高版本Linux内核系统调用Hook失效,寻求替代实现方案
内核系统调用Hook失效问题咨询
以下代码可在5.15.0-73版本内核中实现系统函数Hook,但在5.15.0-113版本中失效。现咨询以下问题:
- 在5.15.0-113、5.15.0-119内核中,替换系统调用表实现Hook后无法生效,还有哪些可行实现方式?
- 在6.8.x及5.15.100以上版本中,除替换系统调用表外,还有哪些方法能实现系统调用函数替换?(最新版本中替换系统调用表的方式已无法生效)
附上相关代码
unsigned long * sys_call_table; unsigned int clear_and_return_cr0(void); void setback_cr0(unsigned int val); static int sys_mycall(void); int orig_cr0; unsigned long *sys_call_table = 0; static int (*anything_saved)(void); unsigned int clear_and_return_cr0(void) { unsigned int cr0 = 0; unsigned int ret; asm volatile ("movq %%cr0, %%rax" : "=a"(cr0)); ret = cr0; cr0 &= 0xfffeffff; //asm volatile ("movl %%eax, %%cr0" :: "a"(cr0)); asm volatile ("movq %%rax, %%cr0" :: "a"(cr0)); return ret; } void setback_cr0(unsigned int val) { //asm volatile ("movl %%eax, %%cr0" :: "a"(val)); asm volatile ("movq %%rax, %%cr0" :: "a"(val)); } static int sys_mycall(void) { int ret = 12345; printk("My syscall is successful!\n"); return ret; } static int __init init_addsyscall(void) { printk("My syscall is starting。。。\n"); sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); printk("sys_call_table: 0x%p\n", sys_call_table); anything_saved = (int(*)(void))(sys_call_table[__NR_syscall]); orig_cr0 = clear_and_return_cr0(); sys_call_table[__NR_syscall] = (unsigned long)&sys_mycall; setback_cr0(orig_cr0); return 0; } static void __exit exit_addsyscall(void) { orig_cr0 = clear_and_return_cr0(); sys_call_table[__NR_syscall] = (unsigned long)anything_saved; setback_cr0(orig_cr0); printk("My syscall exit....\n"); }
问题解答
针对5.15.0-113、5.15.0-119内核的Hook实现方式
修复系统调用表写保护绕过逻辑
从5.15.0-91版本开始,内核增强了写保护机制,仅修改CR0的WP位已无法修改系统调用表,需要额外处理页表权限:- 通过
lookup_address获取sys_call_table对应的页表项(PTE),将其设置为可写 - 修改完成后恢复页表只读权限
示例代码片段:
#include <asm/pgtable.h> pte_t *pte = lookup_address((unsigned long)sys_call_table, &level); if (pte) { set_pte_atomic(pte, pte_mkwrite(*pte)); // 执行sys_call_table的修改操作 set_pte_atomic(pte, pte_clear_flags(*pte, _PAGE_RW)); }- 通过
使用kprobe探针实现Hook
利用内核探针机制直接挂钩目标系统调用入口,无需修改系统调用表:- 注册kprobe到目标系统调用的内核符号,在
pre_handler中插入自定义逻辑或跳转至自定义函数
示例:
#include <linux/kprobes.h> static struct kprobe kp = { .symbol_name = "__x64_sys_xxx", // 替换为目标系统调用的内核符号名 .pre_handler = my_pre_handler, }; static int my_pre_handler(struct kprobe *p, struct pt_regs *regs) { // 执行自定义逻辑,或修改regs->rip跳转到自定义函数 return 0; } // 初始化时注册 register_kprobe(&kp);- 注册kprobe到目标系统调用的内核符号,在
针对6.8.x及5.15.100+版本的系统调用替换方案
ftrace fentry探针
内核ftrace框架的fentry探针可在函数入口插入钩子,是官方推荐的兼容性方案:- 定义fentry处理函数,注册到目标系统调用符号
示例:
#include <linux/ftrace.h> static void my_syscall_hook(struct ftrace_ops *ops, struct pt_regs *regs, unsigned long func) { // 自定义逻辑,可修改regs参数或返回值,也可跳转至自定义函数 } static struct ftrace_ops my_ftrace_ops = { .func = my_syscall_hook, .flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_IPMODIFY, }; // 注册流程 ftrace_set_filter_ip(&my_ftrace_ops, (unsigned long)__x64_sys_xxx, 0, 0); register_ftrace_function(&my_ftrace_ops);- 定义fentry处理函数,注册到目标系统调用符号
eBPF跟踪点
利用eBPF的系统调用跟踪点实现拦截和修改,无需修改内核,安全性和兼容性更高:- 通过
tracepoint:syscalls:sys_enter_xxx和sys_exit_xxx跟踪点,在系统调用前后插入逻辑,修改参数或返回值
示例eBPF代码片段:
SEC("tracepoint/syscalls/sys_enter_openat") int trace_sys_enter_openat(struct trace_event_raw_sys_enter *ctx) { // 处理逻辑,例如替换输入参数 return 0; }- 通过
直接修改函数入口(结合页表权限)
若目标系统调用未被标记为__ro_after_init,可通过kallsyms_lookup_name获取函数地址,修改页表权限后直接覆盖函数入口指令。但该方法受内核编译配置限制,通用性不如前两种方案。
内容的提问来源于stack exchange,提问作者LuckyBacon
相关产品推荐
相关产品推荐

