Download DriverToHideFiles.zip
Table of Contents
Introduction
General Information
New filldir()
function for the file parent directory
New file operations structure
Function to hook inode operations, file operations and get inode number of the specified file
Function to backup inode pointers of the specified file
Device file for driver
Hide driver build system
Driver to hide files in action
Bibliography List
Introduction
In this article, I am going to describe the process of development of a driver module to hide files in Linux OS (you can read the article about Hide Driver for Windows OS here). Besides, I will touch upon such questions:
- Virtual File System (VFS)
- The work with inode and dentry structures
The article concerns the Linux kernel version 2.6.32 because other kernel versions can have the modified API, different from the one used in examples or in the build system. Article is meant for people that already have some experience Linux driver development. Creation of a simple Linux driver was described here.
General Information
The Virtual File System is the software layer in the kernel that provides the file system interface to user space programs. It also provides an abstraction within the kernel that allows different file system implementations to coexist. [1]
The VFS implements open
, stat
, chmod
and similar system calls. The pathname
parameter, which is passed to them, is used by the VFS to search through the directory entry cache (also known as the dentry cache or dcache). This provides a very fast look-up mechanism to translate a pathname
(filename
) into a specific dentry. Dentries live in RAM and are never saved to disk: they exist only for performance purposes. [1]
An individual dentry usually has a pointer to an inode. Inodes are file system objects such as regular files, directories. They live either on the disk (for block device file systems) or in the memory (for pseudo file systems). Inodes, which live on the disk, are copied into the memory when required, and inode changes are written back to disk. Several dentries can point to a single inode (hard links, for example, do this). Each file is represented by its own inode structure. [1]
Each inode structure has its inode number that is unique for the currently mounted file system.
Our driver will hook inode and file operations for the specified file and its parent directory. For this purpose, we will change pointers on inode and file operation structures to our own functions. That will allow us to hide file even from the system.
New filldir()
function for the file parent directory
To hide a file from the system we will hook filldir()
function for parent directory. This function is called from readdir()
function that displays files in the directory. The second and third parameters of this function are name and name length of the file that is displayed. To change the call of filldir()
we will create our own readdir()
function and call our filldir()
function from it.
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
is a pointer to a parent directory dentry that we will use to search specified file inode in our filldir()
function.
As we need to override only readdir()
function from file_operations
structure for the parent directory, we will create static file_operations
structure.
static struct file_operations new_parent_fop =
{
.owner = THIS_MODULE,
.readdir = parent_readdir,
};
This code creates file_operations
structure, where we will override readdir()
function by the custom parent_readdir()
function.
Such syntax tells us that any member of the structure that you don't explicitly assign will be initialized to NULL
by gcc
. [3, chapter 4.1.1.]
We can use d_lookup()
function, which returns dentry of the file in the current directory. d_lookup()
receives dentry of the parent directory with file and qstr
structure, which contains name and name length of the file. We will save dentry of the parent directory in the readdir()
function. We can create qstr
structure and fill it in filldir()
function.
So, having dentry of the currently displayed file, we can compare its inode number with inode number of the file(s), which we want to hide (we will get them later). If file is hidden, filldir()
function returns 0. If file isn’t hidden, we call original filldir()
function.
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
– array of inode numbers for files, which we will hide.
New file operations structure
Changing of filldir()
function allows us only to hide file from the directory. But system calls, for example read
or write
operations, for the file they will work. To prevent it, we will change inode and file operations for the file. Our driver will return -2 value for all operations, that means: “Specified file doesn’t exist.”
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;
}
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;
}
Function to hook inode operations, file operations and get inode number of the specified file
We will use path_lookup()
function to hook inode and file operations. This function fills nameidata
structure for the file specified as the first parameter.
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;
}
After we’ve got filled nameidata
structure, we allocate memory for arrays with old inode pointers and inode number.
void reallocate_memmory()
{
g_inode_numbers = (unsigned long*)krealloc(g_inode_numbers,sizeof(unsigned long*)*(g_inode_count+1), GFP_KERNEL);
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
is array of pointers to original inode structure for hidden files.
g_old_fop_pointer
is array of pointers to original file_operations
structure for hidden files.
g_old_iop_pointer
is array of pointers to original inode_opearations
structure for hidden files.
g_old_parent_inode_pointer
is array of pointers to original inode structure for parent directory that contains hidden files.
g_old_parent_fop_pointer
is array of pointers to original file_operations
structure for parent directory that contains hidden files.
Nameidata
structure contains dentry of the specified path. Using received dentry, we can operate with pointers to file and inode operations. After we get dentry and inode structures for specified path, we save old pointers and change them to the pointers to our structures.
unsigned long hook_functions(const char * file_path)
{
…
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;
g_inode_numbers[g_inode_count] = nd.path.dentry->d_inode->i_ino;
g_inode_count = g_inode_count + 1;
reallocate_memmory();
nd.path.dentry->d_parent->d_inode->i_fop = &new_parent_fop;
nd.path.dentry->d_inode->i_op = &new_iop;
nd.path.dentry->d_inode->i_fop = &new_fop;
return 0;
}
Function to backup inode pointers of the specified file
After the file has been hidden, it would be great to have a possibility to restore it visibility in the system. We can organize it by calling a function that restores pointers to the original inode and file operations after driver is deleted from the system.
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;
}
Device file for driver
As yet, we have driver that hides file by specified path, but how to pass file path to the driver from user mode? We will use device file for that. You can learn detailed information about device file and registering it for character device from above-mentioned article “A Simple Driver for Linux OS”. For our device file, we will implement device_file_write()
function that is called when data is written to the file from user space.
static struct file_operations hide_driver_fops =
{
.owner = THIS_MODULE,
.write = device_file_write,
.open = device_file_open,
.release = device_file_release,
};
In this function, we will apply strncpy_from_user()
to copy file path string from user space to kernel space and remove “\n” from the end of the string, if it exists as some console commands can append it.
After this operations, we will have file path string in kernel space that we can pass to our hook_functions(),
which hides file by specified path.
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;
}
Hide driver build system
To build hide driver, we can use simple example of makefile
from “A Simple Driver for Linux OS” article.
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
This make file will create target HideFiles.ko using main.o device_file.o module.o. Make load
command will mount our driver into the system. Make unload
will remove HideFiles.ko module from the system.
Driver to hide files in action
First of all, let’s create an empty test file in our test directory (for example, file name: testfile, file path: ~/test/testfile).
Now let’s open console and execute make
command in the directory with driver sources.
$>sudo make
After driver has been built, we execute make load
command that inserts our module into the system as device.
$>sudo make load
If make load
command is successfully executed, we can see our driver in /proc/devices file.
$>cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
…
250 Hide_Driver
Now we will create device file for our device with major number 250 and minor number 0
$>sudo mknod /dev/HideDriver c 250 0
Finally let’s hide our test file from the system:
$>sudo sh -c "echo ~/test/testfile>/dev/HideDriver"
Checking our file:
$>ls ~/test
total 0
$>cat ~/test/testfile
cat: ~/test/testfile: No such file or directory
Now let’s remove our driver from the system and check our test file:
$>sudo make unload
$>ls ~/test
Testfile
As you can see, we successfully hid our Testfile from the system and then restored it.
Bibliography List