1、什么是系统调用?/O操作),所以有些功能必须由内核完成。内核向应用层提供系统调用,完成一些在用户态无法完成的工作。说白了,系统调用其实就是函数调用,只不过调用的是内核态函数。但与普通函数调用不同的是,系统调用不能使用call指令调用,而需要使用软中断来调用。在Linux系统中,一般使用int0x80指令(x86)或syscall指令(x64)调用系统调用。下面以int0x80指令(x86)的调用方式为例,说明系统调用的原理。2、系统调用的原理在Linux内核中,sys_call_table数组用于存放所有的系统调用。sys_call_table数组的每个元素代表一个系统调用的入口,定义如下:typedefvoid(*sys_call_ptr_t)(void);constsys_call_ptr_tsys_call_table[__NR_syscall_max+1]={...};当应用程序需要调用系统调用时,首先需要将要调用的系统调用号(即系统调用所在的sys_call_table数组的索引)放入eax寄存器,然后使用int0x80指令触发Call0x80软中断服务。0x80的软中断服务会通过如下代码调用系统调用,如下:...call*sys_call_table(,%eax,8)...以上代码会根据eax中的值调用正确的系统register调用,流程如下图所示:3.系统调用拦截了解了系统调用的原理之后,拦截系统调用就非常简单了。那么如何拦截呢?方法是:我们只需要将sys_call_table数组的系统调用替换成自己写的函数入口即可。比如我们要拦截write()系统调用,只需要将sys_call_table数组的第一个元素替换成我们写的函数即可(因为write()系统调用在sys_call_table数组中的索引为1).修改sys_call_table数组元素的值,步骤如下:1.获取sys_call_table数组的地址修改sys_call_table数组元素的值,一般需要通过内核模块完成。因为由于内存保护机制,用户态程序无法改写内核态的数据。内核模块运行在内核模式下,因此可以跳过这个限制。要修改sys_call_table数组元素的值,首先要获取sys_call_table数组的虚拟内存地址(由于sys_call_table变量不是导出符号,内核模块不能直接使用)。获取sys_call_table数组的虚拟内存地址有两种方法:第一种方法:从System.map文件中读取System.map。System.map是一个内核符号表,包含变量名和函数名在内核中的地址。编译内核时自动生成。获取sys_call_table数组的虚拟地址,使用如下命令:sudocat/boot/System.map-`uname-r`|grepsys_call_table结果如下图所示:从上图中,sys_call_table的虚拟地址数组是:ffffffff818001c0。第二种方法:通过kallsyms_lookup_name()函数从System.map文件中读取的方法不是很优雅,所以内核提供了一个叫做kallsyms_lookup_name()的函数来获取内核变量和内核函数的虚拟内存地址。#includevoidfunc(){...unsignedlong*sys_call_table;//获取sys_call_table的虚拟内存地址sys_call_table=(unsignedlong*)kallsyms_lookup_name("sys_call_table");...}2.设置sys_call_table数组在可写状态下获取sys_call_table数组的虚拟地址后是否可以修改其元素的值?这不是那么简单。由于sys_call_table数组在写保护区内,所以不能直接修改其内容。但暂时关闭写保护有两种方法,如下:第一种方法:将cr0寄存器的第16位置零。cr0控制寄存器的第16位是写保护位。如果设置为零,则允许超级权限向内核写入数据。这样我们就可以在修改sys_call_table数组的值之前,将cr0寄存器的第16位清零,这样就可以修改sys_call_table数组的内容了。修改完成后,再次恢复该位。代码如下:/**将cr0寄存器的第16位设置为0*/unsignedintclear_and_return_cr0(void){unsignedintcr0=0;unsignedintret;/*将cr0寄存器的值移到rax寄存器,输出到同时cr0变量*/asmvolatile("movq%%cr0,%%rax":"=a"(cr0));ret=cr0;cr0&=0xfffeffff;/*清除cr0变量值的第16位为0,并写入修改后的值cr0寄存器*//*将cr0的值读到rax寄存器,然后将rax寄存器的值放入cr0*/asmvolatile("movq%%rax,%%cr0"::“一个”(cr0));returnret;}/**恢复cr0寄存器的值到val*/voidsetback_cr0(unsignedintval){asmvolatile("movq%%rax,%%cr0"::"a"(val));}方法二:set虚拟地址对应页表项的读写属性由于x86CPU的内存保护机制是通过虚拟内存页表实现的(可以参考这篇文章:浅谈内存映射),所以我们只需要将sys_call_table数组放在虚拟内存页表项中清除保护标志位即可,代码如下:/**设置虚拟内存地址为可写*/intmake_rw(unsignedlongaddress){unsignedintlevel;//查找页表addresswherethevirtualaddresspte_t*pte=lookup_address(address,&level);if(pte->pte&~_PAGE_RW)//设置页表读写属性pte->pte|=_PAGE_RW;return0;}/**设置虚拟内存地址为只读*/intmake_ro(unsignedlongaddress){unsignedintlevel;pte_t*pte=lookup_address(address,&level);pte->pte&=~_PAGE_RW;//设置只读属性return0;}3.修改sys_call_table数组的内容已准备就绪。我们只欠东风,我们已经完成了准备工作。现在我们只需要将sys_call_table数组中的系统调用入口替换成我们写的函数入口即可。我们可以在内核模块初始化函数中修改sys_call_table数组的值,然后在内核模块退出函数中改回原来的值。完整代码如下:/**File:syscall.c*/#include#include#include#include#include#include#include#includeunsignedlong*sys_call_table;unsignedintclear_and_return_cr0(void);voidsetback_cr0(unsignedintval);staticintsys_hackcall(void);unsignedlong*sys_call_table=0;/*定义一个函数指针来保存原来的Systemcall*/staticint(*orig_syscall_saved)(void);/**将cr0寄存器的第16位设置为0*/unsignedintclear_and_return_cr0(void){unsignedintcr0=0;unsignedintret;/*将cr0寄存器的值移动到rax寄存器,同时输出到cr0变量*/asmvolatile("movq%%cr0,%%rax":"=a"(cr0));ret=cr0;cr0&=0xfffeffff;/*将cr0变量值中的第16位清0,将修改后的值写入cr0寄存器*//*读取值将cr0的值写入rax寄存器,然后将rax寄存器的值放入cr0*/asmvolatile("movq%%rax,%%cr0"::"a"(cr0));returnret;}/**恢复cr0寄存器的值到val*/voidsetback_cr0(unsignedintval){asmvolatile(";movq%%rax,%%cr0"::"a"(val));}/**自己写的系统调用函数*/staticintsys_hackcall(void){printk("Hacksyscallissuccessful!!!\n");return0;}/**模块的初始化函数,模块的入口函数,模块加载时调用*/staticint__initinit_hack_module(void){intorig_cr0;printk("Hacksyscallisstarting...\n");/*getsys_call_table的虚拟内存地址*/sys_call_table=(unsignedlong*)kallsyms_lookup_name("sys_call_table");/*保存原来的系统调用*/orig_syscall_saved=(int(*)(void))(sys_call_table[__NR_perf_event_open]);orig_cr0=clear_and_return_cr0();/*设置cr0寄存器第16位为0*/sys_call_table[__NR_perf_event_open]=(unsignedlong)&sys_hackcall;/*替换成我们写的函数*/setback_cr0(orig_cr0);/*恢复cr0的值register*/return0;}/**模块退出函数,调用*/staticvoid__exitexit_hack_module(void){intorig_cr0;orig_cr0=clear_and_return_cr0();sys_call_table[__NR_perf_event_open]=(unsignedlong)orig_syscall_saved;/*设置为原来的系统调用*/setback_cr0(orig_cr0);printk("Hackedcallis"...\n");}module_init(init_hack_module);module_exit(exit_hack_module);MODULE_LICENSE("GPL");在上面的代码中,我们将perf_event_open()系统调用替换成了自己的函数注意:测试时最好使用冷门的系统调用,否则可能会导致系统崩溃。4.编写Makefile为了编译方便,我们编写一个Makefile进行编译,如下:obj-m:=syscall.oPWD:=$(shellpwd)KERNELDIR:=/lib/modules/$(shelluname-r)/buildEXTRA_CFLAGS=-O0all:make-C$(KERNELDIR)M=$(PWD)modulesclean:make-C$(KERNELDIR)M=$(PWD)clean要注意加EXTRA_CFLAGS=-O0关闭gcc优化选项以避免插入模块错误。5.测试程序现在,我们编写一个测试程序来测试系统调用拦截是否成功,代码如下:#include#include#includeintmain(void){unsignedlongret=syscall(__NR_perf_event_open,NULL,0,0,0,0);printf("%d\n",(int)ret);return0;}6.运行结果第一步:安装拦截内核模块使用如下命令安装内核模块:root#insmodsyscall.ko然后通过dmesg命令观察系统日志,可以看到如下输出:...[133.564652]Hacksyscallisstarting...这说明我们的内核模块安装成功.第二步:运行测试程序接下来,我们运行刚刚编写的测试程序,然后观察系统日志,输出如下:...[532.243714]Hacksyscallissuccessful!!!这意味着拦截系统调用成功。