内核模块函数迁移触发页错误,如何添加函数读权限?
问题详情
我编写了如下内核模块代码:
void make_function_writable(void *addr) { unsigned int cr0_register; asm volatile("mov %%cr0, %0" : "=r"(cr0_register)); cr0_register &= ~(1 << 16); asm volatile("mov %0, %%cr0" : : "r"(cr0_register)); } void modified(void) { printk(KERN_INFO "world\n"); } void hello(void) { printk(KERN_INFO "Hello\n"); } static int __init hooker_init(void) { void (*modified_addr)(void) = &modified; make_function_writable(modified_addr); memmove(modified_addr + 3, modified_addr, 0x19); printk(KERN_INFO "Move"); void (*modified_addr_new)(void) = (void (*)(void))(modified_addr + 3); (modified_addr_new)(); return 0; } static void __exit hooker_exit(void) { return; } module_init(hooker_init); module_exit(hooker_exit); MODULE_LICENSE("GPL");
运行时触发页面错误:
unable to handle page fault for address: 3cf741d0 supervisor read access in kernel mode error_code(0x0000) - not-present page
我试过用set_memory_x和set_memory_rw,但新版本内核里这些函数没法用,请问怎么给函数添加读权限?
解决方案
1. 先定位错误根源
触发的页错误核心不是权限问题,而是目标地址对应的页不存在。modified_addr + 3可能落到了未映射的内存区域,或者你设置的拷贝长度0x19不合理,导致访问越界。另外,直接修改CR0寄存器的方法在现代内核(尤其是开启SMAP/SMEP的x86系统)里不安全,也解决不了页表层面的映射问题。
2. 新版本内核的页权限修改方法
放弃直接操作CR0的方式,用内核提供的官方接口:
方法一:使用
virt_to_page+set_page_flags
先把虚拟地址转成物理页,再设置页的读写属性:#include <linux/mm.h> void set_page_rw(void *addr) { struct page *page = virt_to_page(addr); set_page_flags(page, PG_RW); }注意:仅适用于内核线性地址,且地址对应的页必须存在。
方法二:使用
change_page_attr_set(内核5.4+可用)
批量修改页属性,比直接操作CR0可靠:#include <linux/page-flags.h> int set_virt_addr_rw(void *addr) { return change_page_attr_set(virt_to_page(addr), 1, PAGE_KERNEL_RW); }
3. 修正代码逻辑错误
你的memmove操作存在风险:modified_addr + 3是函数内部偏移3字节的位置,从这里开始拷贝0x19字节可能覆盖后续内存,甚至超出当前函数所在的页,导致访问未映射区域。建议先通过反汇编确认函数的实际长度,再调整拷贝范围;或者换用更安全的函数跳转逻辑。
另外,调用修改后的modified_addr_new前,要确保该地址所在页已正确映射且有执行权限,修改完内存后记得按需恢复页的执行属性。
4. 适配现代内核安全机制
新版本内核普遍开启了**SMAP(Supervisor Mode Access Prevention)和SMEP(Supervisor Mode Execution Prevention)**这类安全特性,直接修改CR0的WP位(第16位)可能被内核拦截,或引发不可预见的问题。必须使用内核官方接口修改页属性,禁止直接操作CR0。
内容的提问来源于stack exchange,提问作者itamarsch

