Linux OS 中的隐藏文件驱动程序






4.44/5 (13投票s)
用于 Linux OS 的简单驱动程序,可从系统中隐藏选定的文件
目录
引言
一般信息
为文件父目录新增 filldir()
函数
新增文件操作结构
用于挂钩 inode 操作、文件操作并获取指定文件 inode 号的函数
用于备份指定文件 inode 指针的函数
驱动的设备文件
隐藏驱动构建系统
正在运行的隐藏文件驱动
参考文献列表
介绍
在本文中,我将描述在 Linux 操作系统中开发一个隐藏文件驱动模块的过程(您可以 此处 阅读关于 Windows 操作系统隐藏驱动的文章)。此外,我还将涉及以下问题
- 虚拟文件系统 (VFS)
- inode 和 dentry 结构的工作
本文涉及 Linux 内核版本 2.6.32,因为其他内核版本可能具有修改过的 API,这与示例或构建系统中使用的 API 不同。本文档适合已有 Linux 驱动开发经验的人员。简单的 Linux 驱动的创建 此处 已有描述。
一般信息
虚拟文件系统是内核中的软件层,它为用户空间程序提供文件系统接口。它还在内核中提供了一个抽象,允许不同的文件系统实现共存。[1]
VFS 实现 open
、stat
、chmod
和类似的系统调用。传递给它们的 pathname
参数由 VFS 用于在目录项缓存(也称为 dentry 缓存或 dcache)中进行搜索。这提供了一种非常快速查找机制,可以将 pathname
(filename
)转换为特定的 dentry。Dentry 存储在 RAM 中,永不保存到磁盘:它们仅出于性能目的而存在。[1]
单个 dentry 通常指向一个 inode。Inode 是文件系统对象,例如普通文件、目录。它们要么存储在磁盘上(对于块设备文件系统),要么存储在内存中(对于伪文件系统)。当需要时,存储在磁盘上的 inode 会被复制到内存中,并且对 inode 的更改会写回磁盘。多个 dentry 可以指向同一个 inode(例如,硬链接就是这样做的)。每个文件都由其自己的 inode 结构表示。[1]
每个 inode 结构都有一个对于当前挂载的文件系统唯一的 inode 号。
我们的驱动程序将挂钩指定文件及其父目录的 inode 和文件操作。为此,我们将修改 inode 和文件操作结构中的指针,指向我们自己的函数。这将使我们能够从系统中隐藏文件。
为文件父目录新增 filldir()
函数
要从系统中隐藏文件,我们将挂钩父目录的 filldir()
函数。该函数由显示目录中文件的 readdir()
函数调用。该函数的第二个和第三个参数是显示文件的名称和名称长度。为了改变 filldir()
的调用,我们将创建我们自己的 readdir()
函数,并从它调用我们的 filldir()
函数。
int parent_readdir (struct file * file, void * dirent, filldir_t filldir) { g_parent_dentry = file->f_dentry; real_filldir = filldir; return file->f_dentry->d_sb->s_root->d_inode->i_fop->readdir(file, dirent, new_filldir); }
g_parent_dentry
是一个指向父目录 dentry 的指针,我们将在我们的 filldir()
函数中使用它来搜索指定文件的 inode。
由于我们只需要覆盖父目录 file_operations
结构中的 readdir()
函数,因此我们将创建一个静态的 file_operations
结构。
/***********************File OPERATIONS of the parent directory*****************************/ static struct file_operations new_parent_fop = { .owner = THIS_MODULE, .readdir = parent_readdir, };
此代码创建了一个 file_operations
结构,其中我们将使用自定义的 parent_readdir()
函数来覆盖 readdir()
函数。
这种语法告诉我们,结构中未显式赋值的任何成员都将被 gcc
初始化为 NULL
。[3, 第 4.1.1 章]。
我们可以使用 d_lookup()
函数,它返回当前目录中文件的 dentry。d_lookup()
接收父目录 dentry 和文件,以及包含文件名和名称长度的 qstr
结构。我们将在 readdir()
函数中保存父目录的 dentry。我们可以在 filldir()
函数中创建 qstr
结构并填充它。
因此,有了当前显示文件的 dentry,我们就可以将其 inode 号与我们要隐藏的文件(我们稍后会获取它们)的 inode 号进行比较。 如果文件被隐藏,filldir()
函数返回 0。如果文件未被隐藏,则调用原始的 filldir()
函数。
static int new_filldir (void *buf, const char *name, int namelen, loff_t offset,u64 ux64, unsigned ino) { unsigned int i = 0; struct dentry* pDentry; struct qstr current_name; current_name.name = name; current_name.len = namelen; current_name.hash = full_name_hash (name, namelen); pDentry = d_lookup(g_parent_dentry, ¤t_name); if (pDentry != NULL) { for(i = 0; i <= g_inode_count - 1; i++) { if (g_inode_numbers[i] == pDentry->d_inode->i_ino) { return 0; } } } return real_filldir (buf, name, namelen, offset, ux64, ino); }
g_inode_numbers
– 要隐藏文件的 inode 号数组。
新增文件操作结构
改变 filldir()
函数只能让我们从目录中隐藏文件。但是,对于该文件,诸如 read
或 write
操作之类的系统调用仍然可以工作。为了防止这种情况,我们将更改文件的 inode 和文件操作。我们的驱动程序将为所有操作返回 -2 值,这意味着:“指定的文件不存在。”
/********************************FILE OPERATIONS*************************/ static struct file_operations new_fop = { .owner = THIS_MODULE, .readdir = new_readdir, .release = new_release, .open = new_open, .read = new_read, .write = new_write, .mmap = new_mmap, }; … ssize_t new_read (struct file * file1, char __user * u, size_t t, loff_t * ll) { return -2; } ssize_t new_write (struct file * file1, const char __user * u, size_t t, loff_t *ll) { return -2; } … int new_open (struct inode * old_inode, struct file * old_file) { return -2; } /********************************INODE OPERATIONS*************************/ static struct inode_operations new_iop = { .getattr = new_getattr, .rmdir = new_rmdir, }; int new_rmdir (struct inode *new_inode,struct dentry *new_dentry) { return -2; } int new_getattr (struct vfsmount *mnt, struct dentry * new_dentry, struct kstat * ks) { return -2; }
用于挂钩 inode 操作、文件操作并获取指定文件 inode 号的函数
我们将使用 path_lookup()
函数来挂钩 inode 和文件操作。该函数将为作为第一个参数指定的文件的 nameidata
结构进行填充。
unsigned long hook_functions(const char * file_path) { int error = 0; struct nameidata nd; error = path_lookup (file_path, 0, &nd); if (error) { printk( KERN_ALERT "Can't access file\n"); return -1; }
在获取了填充好的 nameidata
结构后,我们为包含旧 inode 指针和 inode 号的数组分配内存。
void reallocate_memmory() { /*Realloc memmory for inode number*/ g_inode_numbers = (unsigned long*)krealloc(g_inode_numbers,sizeof(unsigned long*)*(g_inode_count+1), GFP_KERNEL); /*Realloc memmory for old pointers*/ g_old_inode_pointer = (void *)krealloc(g_old_inode_pointer , sizeof(void*) * (g_inode_count + 1), GFP_KERNEL); g_old_fop_pointer = (void *)krealloc(g_old_fop_pointer, sizeof(void*) * (g_inode_count + 1), GFP_KERNEL); g_old_iop_pointer = (void *)krealloc(g_old_iop_pointer, sizeof(void*) * (g_inode_count + 1), GFP_KERNEL); g_old_parent_inode_pointer = (void *)krealloc(g_old_parent_inode_pointer, sizeof(void*) * (g_inode_count + 1), GFP_KERNEL); g_old_parent_fop_pointer = (void *)krealloc(g_old_parent_fop_pointer, sizeof(void*) * (g_inode_count + 1), GFP_KERNEL); }
g_old_inode_pointer
– 指向隐藏文件原始 inode 结构的指针数组。
g_old_fop_pointer
– 指向隐藏文件原始 file_operations
结构的指针数组。
g_old_iop_pointer
– 指向隐藏文件原始 inode_opearations
结构的指针数组。
g_old_parent_inode_pointer
– 指向包含隐藏文件的父目录原始 inode 结构的指针数组。
g_old_parent_fop_pointer
– 指向包含隐藏文件的父目录原始 file_operations
结构的指针数组。
Nameidata
结构包含指定路径的 dentry。使用接收到的 dentry,我们可以操作指向文件和 inode 操作的指针。在我们获得指定路径的 dentry 和 inode 结构后,我们保存旧指针并将其更改为指向我们结构的指针。
unsigned long hook_functions(const char * file_path) { … /************************Old pointers**********************************/ /*Save old pointers*/ g_old_inode_pointer [g_inode_count] = nd.path.dentry->d_inode; g_old_fop_pointer[g_inode_count] = (void *)nd.path.dentry->d_inode->i_fop; g_old_iop_pointer[g_inode_count] = (void *)nd.path.dentry->d_inode->i_op; g_old_parent_inode_pointer[g_inode_count] = nd.path.dentry->d_parent->d_inode; g_old_parent_fop_pointer[g_inode_count] = (void *)nd.path.dentry->d_parent->d_inode->i_fop; /*Save inode number*/ g_inode_numbers[g_inode_count] = nd.path.dentry->d_inode->i_ino; g_inode_count = g_inode_count + 1; reallocate_memmory(); /*filldir hook*/ nd.path.dentry->d_parent->d_inode->i_fop = &new_parent_fop; /* Hook of commands for file*/ nd.path.dentry->d_inode->i_op = &new_iop; nd.path.dentry->d_inode->i_fop = &new_fop; return 0; }
用于备份指定文件 inode 指针的函数
文件被隐藏后,最好能够恢复它在系统中的可见性。我们可以通过调用一个函数来组织这一点,该函数在驱动程序被从系统中删除后恢复指向原始 inode 和文件操作的指针。
/*Function to backup inode pointers of the specified file*/ unsigned long backup_functions() { int i=0; struct inode* pInode; struct inode* pParentInode; for (i=0; i<g_inode_count; i++) { pInode=g_old_inode_pointer [(g_inode_count-1)-i]; pInode->i_fop=(void *)g_old_fop_pointer[(g_inode_count-1)-i]; pInode->i_op=(void *)g_old_iop_pointer[(g_inode_count-1)-i]; pParentInode=g_old_parent_inode_pointer[(g_inode_count-1)-i]; pParentInode->i_fop=(void *)g_old_parent_fop_pointer[(g_inode_count-1)-i]; } kfree(g_old_inode_pointer ); kfree(g_old_fop_pointer); kfree(g_old_iop_pointer); kfree(g_old_parent_inode_pointer); kfree(g_old_parent_fop_pointer); kfree(g_inode_numbers); return 0; }
驱动的设备文件
到目前为止,我们有一个可以通过指定路径隐藏文件的驱动程序,但如何从用户模式将文件路径传递给驱动程序呢?我们将为此使用设备文件。您可以在上面提到的文章 “A Simple Driver for Linux OS” 中了解有关设备文件及其为字符设备注册的详细信息。对于我们的设备文件,我们将实现 device_file_write()
函数,该函数在用户空间向文件写入数据时被调用。
static struct file_operations hide_driver_fops = { .owner = THIS_MODULE, .write = device_file_write, .open = device_file_open, .release = device_file_release, };
在此函数中,我们将使用 strncpy_from_user()
将文件路径字符串从用户空间复制到内核空间,并移除字符串末尾的“\n”,如果存在的话,因为某些控制台命令可能会附加它。
完成这些操作后,我们将拥有一个内核空间中的文件路径字符串,可以将其传递给我们的 hook_functions()
,该函数将按指定路径隐藏文件。
static ssize_t device_file_write ( struct file *file_ptr , const char *buffer , size_t length , loff_t *offset) { char* pFile_Path; pFile_Path = (char *)kmalloc(sizeof(char *) * length,GFP_KERNEL); if ( strncpy_from_user(pFile_Path, buffer, length) == -EFAULT) { printk( KERN_NOTICE "Entered in fault get_user state"); length=-1; goto finish; } if (strstr(pFile_Path,"\n")) { pFile_Path[length - 1] = 0; printk( KERN_NOTICE "Entered in end line filter"); } printk( KERN_NOTICE "File path is %s without end line symbol", pFile_Path); if (hook_functions(pFile_Path) == -1) { length = -2; } finish: kfree(pFile_Path); return length; }
隐藏驱动构建系统
要构建隐藏驱动,我们可以使用 “A Simple Driver for Linux OS” 文章中的简单 makefile
示例。
TARGET_MODULE:=HideFiles # If we run by kernel building system ifneq ($(KERNELRELEASE),) $(TARGET_MODULE)-objs := main.o device_file.o module.o obj-m := $(TARGET_MODULE).o # If we run without kernel build system else BUILDSYSTEM_DIR?=/lib/modules/$(shell uname -r)/build PWD:=$(shell pwd) all : # run kernel build system to make module $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) modules clean: # run kernel build system to cleanup in current directory $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) clean load: insmod ./$(TARGET_MODULE).ko unload: rmmod ./$(TARGET_MODULE).ko endif
此 make 文件将使用 main.o、device_file.o 和 module.o 创建目标 HideFiles.ko。Make load
命令会将我们的驱动加载到系统中。Make unload
会将 HideFiles.ko 模块从系统中移除。
正在运行的隐藏文件驱动
首先,让我们在测试目录中创建一个空的测试文件(例如,文件名:testfile,文件路径:~/test/testfile)。
现在让我们打开控制台,并在驱动程序源代码所在的目录中执行 make
命令。
$>sudo make
驱动程序构建完成后,我们执行 make load
命令,将我们的模块作为设备插入系统。
$>sudo make load
如果 make load
命令执行成功,我们可以在 /proc/devices 文件中看到我们的驱动程序。
$>cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS … 250 Hide_Driver
现在我们将为我们的设备创建具有主设备号 250 和次设备号 0 的设备文件。
$>sudo mknod /dev/HideDriver c 250 0
最后,让我们从系统中隐藏我们的测试文件。
$>sudo sh -c "echo ~/test/testfile>/dev/HideDriver"
检查我们的文件
$>ls ~/test total 0 $>cat ~/test/testfile cat: ~/test/testfile: No such file or directory
现在,让我们从系统中移除我们的驱动程序,并检查我们的测试文件。
$>sudo make unload $>ls ~/test Testfile
正如您所见,我们已成功地从系统中隐藏了我们的 Testfile,然后又恢复了它。
参考文献列表
- Richard Gooch 的《Linux 虚拟文件系统概述》“Overview of the Linux Virtual File System”。
- “适用于 Linux 操作系统的简单 Linux 驱动程序”。
- Peter Jay Salzman Ori Pomerantz 的《Linux 内核模块编程指南》The Linux Kernel Module Programming Guide