Linux 5.1x正确挂载LSM钩子及RHEL9内核崩溃排查
LSM钩子在RHEL9加载内核崩溃问题
问题描述
以下代码可在Redhat 8服务器上成功实现LSM钩子挂载,但在Redhat 9服务器上加载时会触发内核崩溃。已尝试检索hlist_add_head_rcu接口变更、Linux 5.1x版本LSM挂载方式调整相关资料,暂未找到有效解决方案。核心疑问:为何同一份代码可在Linux 4.x内核正常运行,却无法在Linux 5.1x内核上工作?
测试环境
- Redhat 8:内核版本
4.18.0-147.el8.x86_64 - Redhat 9:内核版本
5.14.0-70.13.1.el9_0.x86_64
复现代码
#include <linux/module.h> #include <linux/kallsyms.h> #include <linux/version.h> #include <linux/security.h> #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,2,0) #include <linux/lsm_hooks.h> #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0) #define KPROBE_LOOKUP 1 #include <linux/kprobes.h> static struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; #endif #define SECURITY_HOOK_ADDR_NAME "security_hook_heads" static unsigned long find_symbol_addr(const char *sym){ const char *cpsMethod = "cris test: find_symbol_addr"; unsigned long addr; #ifdef KPROBE_LOOKUP typedef unsigned long (*kallsyms_lookup_name_t)(const char *name); kallsyms_lookup_name_t kallsyms_lookup_name; register_kprobe(&kp); kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr; unregister_kprobe(&kp); #endif addr = kallsyms_lookup_name(sym); if (addr == 0) { pr_err("%s: unable to find addr\n", cpsMethod); return -EINVAL; } pr_info("%s: address is %lx\n", cpsMethod, addr); return addr; } struct security_hook_list cris_hooks[1] = { }; static int hook_execve_test(struct file *file, int mask) { pr_info("cris test: hook_execve_test\n"); return 0; } static struct security_hook_heads *cris_lsm_hook = NULL; bool hook_lsm(void){ const char *cpsMethod = "cris test: hook_lsm"; int count = 0; int i = 0; unsigned long addr1; addr1 = find_symbol_addr(SECURITY_HOOK_ADDR_NAME); if (addr1 == 0) { pr_err("%s: [Fatal] Lookup address for security hook heads failed. Can't enable execve Hook\n", cpsMethod); return false; } cris_lsm_hook = (struct security_hook_heads*)addr1; cris_hooks[0].head = &(cris_lsm_hook->file_permission); cris_hooks[0].hook.file_permission = hook_execve_test; count = ARRAY_SIZE(cris_hooks); for (i = 0; i < count; i++){ #if LINUX_VERSION_CODE < KERNEL_VERSION(4,17,0) list_add_rcu(&cris_hooks[i].list, cris_hooks[i].head); #else hlist_add_head_rcu(&cris_hooks[i].list, cris_hooks[i].head); #endif } pr_info("%s: finish hook_lsm.\n", cpsMethod); return true; } void unhook_lsm(void){ const char *cpsMethod = "cris test: unhook_lsm"; int count = 0; int i = 0; count = ARRAY_SIZE(cris_hooks); for (i = 0; i < count; i++){ #if LINUX_VERSION_CODE < KERNEL_VERSION(4,17,0) list_del_rcu(&cris_hooks[i].list); #else hlist_del_rcu(&cris_hooks[i].list); #endif } pr_info("%s: Unregister hook module\n", cpsMethod); } static int __init prsyms_init(void) { hook_lsm(); return 0; } static void __exit prsyms_exit(void) { unhook_lsm(); } module_init(prsyms_init); module_exit(prsyms_exit); MODULE_LICENSE("GPL");
RHEL9内核崩溃日志
[ 2644.871335] cris test: find_symbol_addr: address is ffffffffb3215c60 [ 2644.871354] BUG: unable to handle page fault for address: ffffffffb3215ea0 [ 2644.871361] #PF: supervisor write access in kernel mode [ 2644.871363] #PF: error_code(0x0003) - permissions violation [ 2644.871365] PGD b5a15067 P4D b5a15067 PUD b5a16063 PMD 80000000b54000e1 [ 2644.871377] Oops: 0003 [#1] PREEMPT SMP PTI [ 2644.871387] CPU: 0 PID: 2437 Comm: insmod Kdump: loaded Tainted: G S OE --------- --- 5.14.0-70.13.1.el9_0.x86_64 #1 [ 2644.871394] Hardware name: VMware, Inc. VMware7,1/440BX Desktop Reference Platform, BIOS VMW71.00V.13989454.B64.1906190538 06/19/2019 [ 2644.871396] RIP: 0010:hook_lsm.cold+0x47/0x95 [testcris] [ 2644.871416] Code: 02 00 00 48 8d 93 40 02 00 00 48 c7 05 8f 23 00 00 83 70 72 c0 48 89 15 80 23 00 00 48 89 05 69 23 00 00 48 89 15 6a 23 00 00 <48> c7 83 40 02 00 00 40 94 72 c0 48 85 c0 74 08 48 c7 40 08 40 94 [ 2644.871418] RSP: 0018:ffffb5c00260fde0 EFLAGS: 00010246 [ 2644.871421] RAX: ffffffffb3216cf8 RBX: ffffffffb3215c60 RCX: 0000000000000000 [ 2644.871423] RDX: ffffffffb3215ea0 RSI: ffff917efbc17cc0 RDI: ffff917efbc17cc0 [ 2644.871424] RBP: ffffffffc072c000 R08: 0000000000000000 R09: ffffb5c00260fc28 [ 2644.871425] R10: ffffb5c00260fc20 R11: ffffffffb3be8228 R12: ffff917dc12226f0 [ 2644.871427] R13: ffffb5c00260fe88 R14: 0000000000000003 R15: 0000000000000000 [ 2644.871428] FS: 00007f8005409740(0000) GS:ffff917efbc00000(0000) knlGS:0000000000000000 [ 2644.871444] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 2644.871447] CR2: ffffffffb3215ea0 CR3: 000000003f536003 CR4: 00000000001706f0 [ 2644.871467] Call Trace: [ 2644.871473] prsyms_init+0xa/0x1000 [testcris] [ 2644.871477] do_one_initcall+0x44/0x200 [ 2644.871500] ? load_module+0xab8/0xb80 [ 2644.871503] ? kmem_cache_alloc_trace+0x45/0x420 [ 2644.871523] do_init_module+0x5c/0x270 [ 2644.871536] __do_sys_finit_module+0xae/0x110 [ 2644.871544] do_syscall_64+0x3b/0x90 [ 2644.871592] entry_SYSCALL_64_after_hwframe+0x44/0xae
根因分析
崩溃是内核态写入只读内存触发的权限页错误,核心原因有两点:
- LSM钩子头内存保护机制升级:Linux 5.7之后的主线内核(含RHEL9使用的5.14版本)将
security_hook_heads标记为__ro_after_init,内核初始化流程结束后,该符号所在内存页会被强制设置为只读属性,防止运行时被恶意篡改。RHEL8的4.18内核未启用该保护,因此直接修改链表可以正常执行。从崩溃日志可以看到,错误码0x0003明确标注是写权限违规,出错地址正好落在security_hook_heads的内存范围内。 - 结构体偏移不匹配:编译模块时使用的头文件中
struct security_hook_heads的成员布局,和RHEL9实际运行内核的结构体布局存在差异,代码通过编译时偏移计算出的file_permission钩子头地址和实际地址不符,进一步触发了非法内存访问。
适配建议
- 临时调试方案:修改钩子链表前,调用
set_memory_rw临时将对应内存页修改为可写属性,操作完成后再通过set_memory_ro改回只读。注意不能依赖编译时的结构体偏移,需要通过运行时kallsyms解析或动态偏移计算定位实际的钩子头地址,避免访问错误内存。该方案稳定性差,不建议在生产环境使用。 - 正式适配方案:放弃直接篡改内核内部链表的挂载方式,使用内核官方提供的LSM注册接口。5.x版本内核支持通过
security_add_hooks接口注册自定义钩子,配合register_lsm将模块注册为合法LSM,不需要手动操作hlist链表,也不会触发内存保护冲突。编译out-of-tree LSM模块时需要按照内核规范定义lsm_info段,确保内核可以正确识别加载。 - 额外注意:RHEL9默认开启Kernel Lockdown内核锁定模式,即使修改页表属性,部分内核内存操作也会被拦截,测试前需要先关闭锁定模式。
内容的提问来源于stack exchange,提问作者T.Cris
相关产品推荐
相关产品推荐

