Skip to content

Intercept a system call using a loadable kernel module.

Notifications You must be signed in to change notification settings

bawejakunal/Intercept-System-Calls

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Intercept System Calls

AIM: To intercept a system call using Loadable Kernel Module(LKM)

CONCEPT: Fundamentally system calls are to be implemented as part of a kernel and each time a system call is added, the kernel image needs to be recompiled after modifying the static system call table which keeps track of all function pointers to all system calls implemented.

So our aim here is to “intercept” the behaviour of an existing system call via a loadable kernel module

STEPS TO FOLLOW:

  1. Loadable Kernel Modules (LKM) are simply extensions to the basic kernel in your operating system that may be loaded/unloaded on the fly, without a need to recompile the kernel or reboot the system. Open up a text editor of your choice and start with writing a simple loadable kernel module by adding two header files:
  #include <linux/version.h> //this adds required headers for versioning
  #include <linux/module.h>  //macros for module development
  1. For writing any kernel module we need to implement initialization and cleanup routines. Those modules can be implemented as follows:
  static int __init init_my_module(void)	  //initialise the kernel module
  {
	  printk(KERN_INFO "Inside kernel space\n");
	  return 0;
  }

  static void __exit cleanup_my_module(void)	//cleanup the module on unloading
  {
	  	printk(KERN_INFO "Exiting kernel space\n");
	  return;
  }

  module_init(init_my_module);		//macro to identify initialiser
  module_exit(cleanup_my_module);	//macro to identify cleanup routine
  MODULE_LICENSE("GPL");	//crucial macro for availability of some kernel functions
  1. As a final step towards making a kernel module we need to write a Makefile with the following contents before we compile and load the kernel module.
obj-m := intercept.o	     //assume we have named our module as intercept here (intercept.c)
KDIR := /lib/modules/`uname -r`/build	//location of current kernel tree
PWD := `pwd`	// present working directory
default:
	make -C $(KDIR) M=$(PWD) modules

i) Save the Makefile and execute make command. Among the many files created there is one named intercept.ko which is our loadable kernel module.

ii) Load the module by executing sudo insmod intercept.ko and check the output of lsmod command to check the details of all the modules loaded in kernel (intercept should be present in it if we have loaded the module successfully)

iii) Now unload the module by executing sudo rmmod intercept and then execute dmesg | tail to check the output of printk() statements in kernel ring.

###Simple Miscellaneous Character Device

Add the following header to you source file intercept.c:

			#include <linux/miscdevice.h>  //important struct miscdevice is declared here
			#include <linux/fs.h>  //struct file_operations declared here

Now add global variables and device handler functions to the code.

	int in_use = 0; //set to 1 in open handler and reset to 0 in release handler
	/*Device open handler*/
  static int my_open(struct inode *inode, struct file *file)
  {
	  		/*Do not allow multiple processes to open this device*/
	  if(in_use)
		  		return -EBUSY;
	  in_use++;
	  	printk("MyDevice opened\n");
	  return 0;
  }
  /* This function, in turn, will be called when a process closes our device */
  static int my_release(struct inode *inode, struct file *file)
  {
  			in_use--;
  	printk("MyDevice closed\n");
  			return 0;
  }

	/*This static function handles ioctl calls performed on MyDevice*/
  static int my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
  {
	  int retval = 0;
	  // This part needs to filled later as shown in the same tutorial
	  return retval;
  }

Now we are ready to populate the two most important data structures, file_operations and miscdevice, which determine the working of MyDevice.

//populate data struct for file operations
static const struct file_operations my_fops = {
	.owner = THIS_MODULE,
	.open = &my_open,
	.release = &my_release,
	.unlocked_ioctl = (void*)&my_ioctl,
	.compat_ioctl = (void*)&my_ioctl
};
//populate miscdevice data structure
static struct miscdevice my_device = {
	MISC_DYNAMIC_MINOR,  //assigns random minor number to MyDevice
	"MyDevice",
	&my_fops
};

As a final step for device driver writing: i) Add int retval = misc_register(&my_device); to init_my_module() and change return 0; to return retval; ii) Add misc_deregister(&my_device); to the cleanup subroutine.

Finally execute make command again and load the module using insmod.

Now to check whether our device has been successfully registered execute the command as follows: ls -l /dev/MyDevice

If the output is like crw------- 1 root root 10, 56 Dec 9 23:43 /dev/MyDevice , it means our device has been successfully registered with read and write permission granted only for the root user which we can change by executing chmod a+r+w /dev/MyDevice.

Now you may open the device for I/O via open() system call.

Here numbers 10 and 56 denote major and minor device numbers respectively.

###Intercepting open() syscall in kernel For the third and final part we again start by adding two header files:

#include <linux/highmem.h>
#include <asm/unistd.h>

#include <linux/highmem.h> is needed due to the fact that system call table is located in read only memory area in modern kernels and we will have to modify the protection attributes of the memory page containing the address of the system call that we want to intercept.

#include <asm/unistd.h> is needed for implementing modified system calls.

In kernel 2.6 and above sys_call_table is not an exported symbol so we need to hard code the sys_call_table address.

Get that by copying the address from the output of: sudo cat /boot/System.map-3.13.0-40-generic | grep sys_call_table

Store in a variable unsigned long *sys_call_table = (unsigned long*)0xffffffff81801400;which should be declared as global.

Now we define two global values, which would be used as argument to our_ioctl function. One will tell us to patch the table, another one will tell us to fix it by restoring the original value.

/* IOCTL commands */
#define IOCTL_PATCH_TABLE 0x00000001
#define IOCTL_FIX_table   0x00000004

Add variable int is_set=0; to use as a flag to distinguish between real & modified system call.

Add following lines to implement the modified open() system call.

  //function pointer to original sys_open system call
  asmlinkage int (*real_open)(const char* __user, int, int);

  //Replacement of original call with modified system call
  asmlinkage int custom_open(const char* __user file_name, int flags, int mode)
  {
	  printk("interceptor: open(\"%s\", %X, %X)\n", file_name,flags,mode);
	  return real_open(file_name,flags,mode);
  }

Another couple of crucial functions is the set that will allow us to modify the memory page protection attributes directly.

	/*Make the page write protected*/
	int make_rw(unsigned long address)
  {
	  unsigned int level;
	  pte_t *pte = lookup_address(address, &level);
	  if(pte->pte &~ _PAGE_RW)
		  pte->pte |= _PAGE_RW;
	  return 0;
  }

  /* Make the page write protected */
  int make_ro(unsigned long address)
  {
	  unsigned int level;
	  pte_t *pte = lookup_address(address, &level);
	  pte->pte = pte->pte &~ _PAGE_RW;
	  return 0;
  }

Now in the final stage we need to implement the my_ioctl() function, so add the following lines to it:

	switch(cmd)
  {
    case IOCTL_PATCH_TABLE:
      make_rw((unsigned long)sys_call_table);
      real_open = (void*)*(sys_call_table + __NR_open);
      *(sys_call_table + __NR_open) = (unsigned long)custom_open;
      make_ro((unsigned long)sys_call_table);
      is_set=1;
      break;
    case IOCTL_FIX_TABLE:
     make_rw((unsigned long)sys_call_table);
     *(sys_call_table + __NR_open) = (unsigned long)real_open;
     make_ro((unsigned long)sys_call_table);
     is_set=0;
      break;
    default:
     printk("sys_open not executed\n");
     break;
  }

Add the following lines to cleanup module to restore pointers in sys_call_table.

	if (is_set)
	{
		make_rw((unsigned long)sys_call_table);
		*(sys_call_table + __NR_open) = (unsigned long)real_open;
		make_ro((unsigned long)sys_call_table);
	}

After adding all the above given snippets save intercept.c, execute make and load the kernel module. Now test the driver using the following code and checking the output of dmesg | tail in terminal.

  #include <stdio.h>
  #include <sys/ioctl.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>

  /* Define ioctl commands */
  #define IOCTL_PATCH_TABLE 0x00000001
  #define IOCTL_FIX_TABLE   0x00000004

  int main(void)
  {
    int device = open("/dev/MyDevice", O_RDWR);
    printf("%d\n",device);
    ioctl(device, IOCTL_PATCH_TABLE);
    sleep(2);
    ioctl(device, IOCTL_FIX_TABLE);
    close(device);
   return 0;
  }

This brings us to the end of intercepting a system call via a kernel module

About

Intercept a system call using a loadable kernel module.

Resources

Stars

Watchers

Forks