Logo
blank Skip to main content

Driver to hide files in Linux OS

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 in this Linux driver tutorial.

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]

hidedriverlinux1

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.

C
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.

C
/***********************File OPERATIONS of the parent directory*****************************/
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.

C
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, &current_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.โ€

C
/********************************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;
}

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.

C
  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 filen");
        return -1;
    }

After weโ€™ve got filled nameidata structure, we allocate memory for arrays with old inode pointers and inode number.

C
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 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.

C
       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;
 }

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.

C
/*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;
}

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.

C
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.

C
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.

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

This make file will create target HideFiles.ko using main.odevice_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 makecommand in the directory with driver sources. 

ShellScript
gt;sudo make

After driver has been built, we execute make load command that inserts our module into the system as device.

ShellScript
gt;sudo make load

If make load command is successfully executed, we can see our driver in /proc/devices file.

ShellScript
gt;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

ShellScript
gt;sudo mknod /dev/HideDriver cย  250 0

Finally letโ€™s hide our test file from the system:

ShellScript
gt;sudo sh -c "echo ~/test/testfile>/dev/HideDriver"

Checking our file:

ShellScript
gt;ls ~/test total 0
ShellScript
gt;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:

ShellScript
gt;sudo make unload
ShellScript
gt;ls ~/test Testfile

As you can see, we successfully hid our Testfile from the system and then restored it.

Bibliography List

Download sample sources Hide-Driver-for-Linux (ZIP, 4.6 KB)

Have a question?

Ask our expert!

Tell us about your project

Send us a request for proposal! Weโ€™ll get back to you with details and estimations.

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us