本文将记录在linux操作系统中实现系统调用监测功能的原理和操作过程,涉及到了内核编译、添加自定义系统调用、修改系统调用入口、添加内核函数、添加编译内核模块等内容。主要参考资料《Linux操作系统原理与应用(第2版)》
系统调用是linux系统中提供给用户态进程的一组可以与硬件设备(如CPU、磁盘、打印机等)进行交互的接口。对于用户态程序员来说它就和一个API函数差不多,同样关注的是传入参数以及返回值,但实际上一些涉及到硬件设备操作的c库API就是使用了系统调用来实现的,API只是对系统调用的再加工和封装。
对于每一个系统调用,在内核中都有一个内核函数用来实现这个系统调用的主要功能,该内核函数叫这个系统调用的服务例程;内核函数被执行于内核空间,用户空间中调用系统调用的进程要想传入参数拿到运行结果还需要一些准备工作,这些工作在用户空间的部分叫封装例程,主要是将参数以合适的方式存储在堆栈中,称之为该系统调用的封装例程;在内核空间的部分是系统调用处理程序,它其实就是中断处理程序,产生一个中断让CPU陷入内核态,然后保存堆栈,处理参数,调用服务例程,执行完后返回参数,这些操作定义在系统内核文件夹下的arch/x86/kernel/entry_32(不同内核版本可能有差异,64位系统用的是entry_64)。
内核文件夹中arch/x86/include/asm/unistd_32.h(64位系统用的是unistd_64.h,后面的也类似)中定义了系统调用号,内核执行一个系统调用时将从这个文件中查找系统调用的名称和系统调用号之间的关系;内核文件夹中arch/x86/kernel/syscall_table_32.S中是系统调用表,内核将根据系统调用号作为下标读取这个文件获取服务例程函数。
系统调用监测要完成以下几个基本功能:
(1)记录系统调用日志,将其写入缓冲区(内核中),以便用户读取。
(2)建立新的系统调用,以便将内核缓冲区中的系统调用日志返回到用户空间。
(3)循环利用系统调用,以便能实时地返回系统调用的日志。
功能(1)编写一个内核函数来实现;功能(2)新建立系统调用来实现;功能(3)在用户空间循环调用(2)中的系统调用。下面依次实现这些功能:
功能(1):在内核文件夹中arch/x86/kernel/entry_32.S中含有系统调用入口,每个系统调用的执行都会从这里开始,在这个文件中添加汇编代码调用我们自己定义的内核函数,名为syscall_audit。
在/arch/x86/kernel/目录下添加自己编写的myaudit.c文件,该文件包含了syscall_audit函数的实现,但这是个内核函数,编译调试需要重新编译内核,太麻烦,为了简化调试过程,我们在myaudit.c文件中不要直接写函数的实现,而是用钩子函数作替身,钩子函数的实现放到模块中。模块的编写后面再介绍,myaudit.c中这样写:
#include
#include
#include
#include
#include
#include
asmlinkage int sys_mysyscall(int number){
printk("hello,world!\n");
current->uid=0;
return number;
}
void (*my_audit)(int,int)=0;
asmlinkage void syscall_audit(int syscall,int return_status);{
if(my_audit)return (*my_audit)(syscall,return_status);
printk("IN KERNEL:%s(%d),syscall:%d,return:%d\n",current->comm,current->pid,syscall,return_status);
return;
}
int (*my_sysaudit)(u8,u8*,u16,u8)=0;
asmlinkage int sys_myaudit(u8 type,u8 * us_buf,u16 us_buf_size,u8 reset){
if(my_sysaudit)return (*my_sysaudit)(type,us_buf,us_buf_size,reset);
printk("IN KERNEL: my system call sys_myaudit() working\n");
return 1;
}
第一个sys_mysyscall()函数是顺手加的一个可以输出hello,world和使用户uid=0的系统调用,第二个syscall_audit()就是系统调用入口中将会调用的内核函数,第三个sys_myaudit是功能(2)中新建立的系统调用将要使用的内核函数。
功能(2):新建立一个系统调用读取内核缓冲区中的内容,过程是在系统调用号和系统调用表中添加新的系统调用的信息,比较简单,参考资料也很多,不再赘述,而添加服务例程的代码上面已给出。
下面给出功能(1)和(2)中用到的两个内核函数的真正实现:
新建一个文件夹mod,新建audit.c文件,内容如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define COMM_SIZE 16
struct syscall_buf{
u32 serial;
u32 ts_sec;
u32 ts_micro;
u32 syscall;
u32 status;
pid_t pid;
u8 comm[COMM_SIZE];
};
DECLARE_WAIT_QUEUE_HEAD(buffer_wait);
#define AUDIT_BUF_SIZE 100
static struct syscall_buf audit_buf[AUDIT_BUF_SIZE];
static int current_pos=0;
static u32 serial=0;
void syscall_audit(int syscall,int return_status){
struct syscall_buf *ppb_temp;
if(current_posserial=serial++;
//ppb_temp->ts_sec=xtime.tv_sec;
//ppb_temp->ts_micro=xtime.tv_usec;
ppb_temp->syscall=syscall;
ppb_temp->status=return_status;
ppb_temp->pid=current->pid;
memcpy(ppb_temp->comm,current->comm,COMM_SIZE);
if(++current_pos==AUDIT_BUF_SIZE*8/10){
printk("IN MODULE_audit: yes, it near full\n");
wake_up_interruptible(&buffer_wait);
}
}
}
int sys_audit(u8 type, u8 * us_buf, u16 us_buf_size, u8 reset){
int ret=0;
if(!type){
if(__clear_user(us_buf, us_buf_size)){
printk("Error:clear_user\n");
return 0;
}
printk("IN MODULE_systemcall: starting...\n");
ret=wait_event_interruptible(buffer_wait,current_pos>=AUDIT_BUF_SIZE*8/10);
printk("IN MODULE_systemcall: over,current_pos is %d\n",current_pos);
if(__copy_to_user(us_buf,audit_buf,(current_pos)*sizeof(struct syscall_buf))){
printk("Error: copy error\n");
return 0;
}
ret=current_pos;
current_pos=0;
}
return ret;
}
extern void(*my_audit)(int,int);
extern int(*my_sysaudit)(unsigned char,unsigned char*,unsigned short, unsigned char);
static int __init audit_init(void){
my_sysaudit=sys_audit;
my_audit=syscall_audit;
printk("Starting System Call Auditing\n");
return 0;
}
static void __exit audit_exit(void){
my_audit=NULL;
my_sysaudit=NULL;
printk("Exiting System Call Auditing\n");
return;
}
module_init(audit_init);
module_exit(audit_exit);
MODULE_LICENSE("GPL");
在模块初始化时让钩子函数my_sysaudit指向模块函数sys_audit,sys_audit()就是sys_myaudit的真正实现了,从代码可以看到它的功能就是从缓冲区里读取信息然后copy_to_user;另一个就是syscall_audit的真正实现了,功能是将当前系统调用保存到缓冲区。模块退出时解除钩子函数的绑定。
为了在模块中可以顺利引用到钩子函数my_sysaudit和my_audit,还需要在arch/x86/kernel/i386_ksyms_32.c文件的末尾添加以下内容:
extern void(*my_audit)(int,int);
EXPORT_SYMBOL(my_audit);
extern int(*my_sysaudit)(unsigned char, unsigned char*, unsigned short, unsigned char);
EXPORT_SYMBOL(my_sysaudit);
以上步骤做完就可以编译内核了,编译完后系统中就已经多了两个系统调用,一个是hello,world系统调用,另一个是读取内核缓冲区的,可以测试一下hello,world能不能用。这时候继续把模块编译一下,然后insmod加载模块。涉及到内核的部分就结束了。
功能(3):在用户空间编程,编写文件user_audit.c,内容如下:
#include
#include
#include
#include
#include
#include
#include
struct syscall_buf{
int serial;
int syscall;
int status;
pid_t pid;
char comm[16];
};
#define AUDIT_BUF_SIZE 100*sizeof(struct syscall_buf)
int main(){
char col_buf[AUDIT_BUF_SIZE];
unsigned char reset=1;
int num=0;
struct syscall_buf *p;
while(1){
num=syscall(__NR_myaudit, 0, col_buf, AUDIT_BUF_SIZE, reset);
printf("num: %d\n");
char j=0;
int i;
p=(struct syscall_buf *)col_buf;
for(i=0;i
编译测试监测效果。
以上代码适用于内核版本2.6.28,或者不要超过太多,否则会增加调BUG的难度。