系统调用日志收集程序_x86_64环境3.14版本内核

1 简介

1.1 背景

系统调用是用户程序与系统打交道的唯一入口,因此系统调用的安全直接关系到系统的安全。如果一个用户恶意地不断调用fork()将导致系统负载增加,因此我们有必要收集一些有危险的系统调用记录,将有利于系统管理进行事后追踪,从而提高系统的安全性。

1.2 原理

前提:本程序在x86_64构架ubuntu14.04麒麟系统上测试通过。系统自带内核版本为3.13,实验使用在www.kernel.org下载的3.14.57版本内核。

源程序是《linux操作系统原理与应用 第二版》– (陈莉君 康华 编著) 中的第6章程序,本文对其进行修改以适配64位系统和3.14内核。

系统调用日志收集程序结构图如下:

系统调用日志收集程序结构图

  • 当用户态程序调用fork,open函数时,在系统调用入口system_call会拦截到此次调用并调用sys_syscall_audit函数,记录此次调用。这里只列举了两个系统调用,程序中还拦截了close,execv,getpid,clone调用。
  • 当myaudit模块插入内核时,syscall_audit和sys_audit函数分别被挂在钩子函数my_sysaudit和my_audit上。这样,sys_syscall_audit函数调用钩子函数my_sysaudit时,实际就是调用myaudit模块里的syscall_audit函数,从而对此次拦截的系统调用信息进行记录,存入audit_buf数组里。
  • 当用户运行日记查看程序user_audit时,通过系统调用号317,调用到sys_myaudit函数,通过钩子函数sys_myaudit触发myaudit模块里的sys_audit函数,将日志信息取出。

从结构图中,我们可以总结,程序大致分为四个部分。

  1. 添加两个系统调用,sys_syscall_audit和sys_myaudit,分别用于接收拦截到的系统调用和取出系统调用日志。并且在两个函数里调用钩子函数,以便myaudit模块接入。
  2. 修改系统调用入口system_call,在x86_64下该入口位于arch/x86/kernel/entry_64.S。我们需要在入口添加调用sys_syscall_audit函数来记录系统调用。
  3. myaudit模块的编写,用来处理系统调用日志。
  4. 用户态程序user_audit的编写,将日志信息输出到控制台。

2 准备工作

在编写这个程序之前,从上面总结的四个部分可以知道,我们需要编写用户态程序,内核模块,添加系统调用和修改系统调用入口。
其中添加系统调用和修改系统调用入口意味着我们要修改内核并且重新编译内核,才能使用。所以先介绍内核编译,内核能够编译成功运行之后再修改内核。

2.1 内核编译

2.1.1 下载解压内核

首先可以使用wget命令在kernel.org网站下载linux-3.14.57.tar.xz内核压缩包,之后使用xz -d命令解压得到.tar文件,使用tar -xvf命令解压得到内核文件夹,cd进入内核源代码根目录。
注意:把内核源代码文件夹改名为linux-headers-3.14.57并放在/usr/src/下,这样之后编译模块时就不用更改内核路径了。

2.1.2 简化编译配置

在编译之前,我们需要配置编译参数,标记需要哪些模块,哪些功能。
linux内核提供了很多种配置方法,包括make config,make menuconfig,make xconfig等等。

更多详细信息参考Linux内核编译初学者指南

而我们这里使用make localmodconfig进行配置,这是2.6.32版本之后引入的简化内核的配置方式。该方式仅编译当前内核已加载的模块,没有使用的模块则不编译。有些当前内核没有的特性会提示选择,我直接回车,因为我这是实验环境,不影响。
对于生产环境,使用localmodconfig配置之后,仍然可以使用make menuconfig来做修改。使用menuconfig之前需要安装ncurses,对于ubuntu可以直接通过apt-get install ncurses-dev从源安装。
所以配置内核我只用了一步使用make localmodconfig

题外话:我先前使用make menuconfig默认配置编译内核,结果编译了接近一个小时,并且整个文件夹占用大小有7,8个G。很明显编译了很多不需要的模块及驱动进去,而且还用不了。而使用localmodconfig最简配置之后,配合make -j8并发编译支持,编译一遍内核只需要8分钟。再使用了后面的ccache之后,重新编译只需要不到1分钟。

2.1.3 ccache编译加速

ccache(“compiler cache”的缩写)是一个编译器缓存,该工具会高速缓存编译生成的信息,并在编译的特定部分使用高速缓存的信息。
也就是说ccache会在你重新编译时,只编译修改的部分,相同的部分从缓存读取,这样就大大地加快了重新编译的速度。
ubuntu上可以直接使用apt-get install ccache通过源安装。
内核编译开启ccache支持,我们只需要修改源代码根目录的Makefile文件。由于内核大部分都是C语言编写,所以只要缓存gcc编译的内容。所以在Makefile文件中找到

HOSTCC       = gcc

我们修改成

HOSTCC       = ccache gcc

CC      = $(CROSS_COMPILE)gcc

改成

CC      = ccache $(CROSS_COMPILE)gcc

这样就完成了ccache的配置。

2.1.4 编译

配置了内核文件之后,通常通过make命令来编译内核。也可以把make命令分成两步,make bzImagemake modules分别编译内核和模块驱动。
在多核心的CPU下,我们通过-j参数开启并发任务同时编译。比如我的机器是4核8线程,使用-j8开启8个并发任务。不建议使用make -j不加数字,代表不限制任务数,虚拟机使用该参数导致崩溃。
对于我的情况,编译的命令就是make -j8

需要重新配置并编译时,先使用make mrproper命令删除之前的临时文件和配置文件。

2.1.5 使用

在当前系统上使用成功编译的内核需要执行make modules_installmake install分别安装模块驱动和内核镜像。在执行make install时,会自动更新grub引导,默认从新的内核镜像启动,并且保留旧的内核引导。

当重新启动系统,发现新内核出错时,可以在开机时,长按shift,进入grub菜单,从更多选项里选择旧的内核启动。

3 程序分析

我们按照之前总结的程序的四个部分依次分析。

3.1 添加两个系统调用

添加系统调用有三步,添加系统调用函数,添加系统调用号,添加声明。

3.1.1 添加系统调用函数

myaudit.c文件

#include <asm/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/init.h>
#include <linux/types.h>
#include <asm/current.h>
#include <linux/sched.h>

void (* my_audit)(int, int) = 0;
asmlinkage void sys_syscall_audit(int syscall,int  return_status)
{
    if(my_audit)    //如果钩子函数没有挂在则输出printk信息
        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 sysaudit() working\n");
    return 0;
}

EXPORT_SYMBOL(my_audit);
EXPORT_SYMBOL(my_sysaudit);

对于系统调用对应的内核函数一定要以sys_开头,我没有用sys_开头时,发现系统调用失败。
对于2.6及以后的内核,如果在其他模块需要使用全局函数,则需要使用EXPORT_SYMBOL导出函数,否则不能使用。但在其他模块使用该函数时编译配置有改动,后面会详细说明。
我将文件myaudit.c放在arch/x86/kernel/myaudit.c。
自己添加的文件需要编译进内核时,可以修改当前目录下的Makefile文件,在相应位置添加,如:

     obj-y               += vsmp_64.o
     obj-y               += myaudit.o    #添加的行

这样内核就会把myaudit.c文件编译进内核。

3.1.2 添加系统调用号

3.14内核的x86_64系统调用号在arch/x86/syscalls/syscall_64.tbl文件,因为本次实验环境是64位的系统,所以是syscall_64.tbl文件。如果是32位环境则是syscall_32.tbl文件。
我们只需要在最后一个系统调用号下添加即可:

315 common  sched_getattr       sys_sched_getattr
# 下面是添加的两个系统调用号
316 common  mysyscall       sys_syscall_audit
317 common  myaudit         sys_myaudit

3.1.3 添加声明

修改头文件include/linux/syscalls.h,在文件最后添加两个函数声明。

//add myaudit
asmlinkage void sys_syscall_audit(int syscall,int return_status);
asmlinkage int sys_myaudit(u8 type, u8 * us_buf, u16 us_buf_size, u8 reset);

3.1.4 编译测试

在添加完两个系统调用之后,编译使用新内核,并对系统调用进行测试。
在内核编译成功,重启之后可以使用uname -r查看是否是新内核的版本号。
可以编写一个简单的用户态小程序,测试系统调用是否成功。
如:

#include <stdio.h>
int main()
{
    syscall(316,11,12);
    return 0;
}

运行之后,通过dmesg命令查看内核输出是否正常。

3.2 修改系统调用入口

3.2.1 说明

在64位的机器上linux内核的系统调用入口是arch/x86/kernel/entry_64.S文件。
这里借用一张系统调用图来说明。
系统调用入口
在x86_64构架的系统上,系统调用入口system_call位于entry_64.S。并且当64位系统运行32位程序时,进入ia32entry.S模拟32位环境。可以看到新的内核的系统调用已经不再通过INT 0X80中断进入,通过MSR寄存器。

来源于 [翻譯] 系統呼叫(system call)的剖析(下)

我的位置文件/usr/src/linux-headers-3.14.57/arch/x86/kernel/entry_64.S
在文件中首先找到ENTRY(system_call),即系统调用入口。在入口里找到了两处调用sys_call_table系统调用表的地方,两处是不同情况下的处理。所以,我们对两处都进行拦截。
我们要拦截fork,open,close,execv,getpid,clone这几个调用,先通过之前提到过的syscall_64.tbl文件查看调用号是多少。然后在调用完sys_call_table之后,依次判断调用号是否是这几个,如果是则跳转到myauditsys处理。在myauditsys调用call sys_syscall_audit函数,记录这次系统调用。
在64位构架上,栈顶寄存器由esp变为rsp,存系统调用号和函数返回值的eax变为rax。
具体请看代码注释

参考 调度时机分析之被动调度(之系统调用返回) 调度机时分析之被动调度(之中断处理返回)

3.2.2 代码

第一处修改:

    ENABLE_INTERRUPTS(CLBR_NONE)
    SAVE_ARGS 8,0
    movq  %rax,ORIG_RAX-ARGOFFSET(%rsp)    //存入系统调用号,可以看到在栈中调用号的地址是ORIG_RAX-ARGOFFSET
    movq  %rcx,RIP-ARGOFFSET(%rsp)
    CFI_REL_OFFSET rip,RIP-ARGOFFSET
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
    jnz tracesys
system_call_fastpath:
#if __SYSCALL_MASK == ~0
    cmpq $__NR_syscall_max,%rax
#else
    andl $__SYSCALL_MASK,%eax
    cmpl $__NR_syscall_max,%eax
#endif
    ja badsys
    movq %r10,%rcx
    call *sys_call_table(,%rax,8)  # XXX:    rip relative
    movq %rax,RAX-ARGOFFSET(%rsp)    //存储系统调用返回值,返回值在栈中的位置是RAX-ARGOFFSET
//----------------下面的是添加的-----------------
/*
 *add myaudit1
*/
    cmpq $57,ORIG_RAX-ARGOFFSET(%rsp)     //比较调用号是否相等,57是fork的调用号
    je myauditsys1    //是fork调用则跳转到myauditsys1
    cmpq $2,ORIG_RAX-ARGOFFSET(%rsp)    //open
    je myauditsys1
    cmpq $3,ORIG_RAX-ARGOFFSET(%rsp)    //close
    je myauditsys1
    cmpq $59,ORIG_RAX-ARGOFFSET(%rsp)    //execv
    je myauditsys1
    cmpq $39,ORIG_RAX-ARGOFFSET(%rsp)    //getpid
    je myauditsys1
    cmpq $56,ORIG_RAX-ARGOFFSET(%rsp)    //clone
    je myauditsys1
myaudit1:

第二处修改

    movq %r10,%rcx  /* fixup for C */
    call *sys_call_table(,%rax,8)
    movq %rax,RAX-ARGOFFSET(%rsp)
    /* Use IRET because user could have changed frame */
//----------------下面的是添加的-----------------
/*
 *add myaudit2
*/
    cmpq $57,ORIG_RAX-ARGOFFSET(%rsp)  
    je myauditsys2
    cmpq $2,ORIG_RAX-ARGOFFSET(%rsp)
    je myauditsys2
    cmpq $3,ORIG_RAX-ARGOFFSET(%rsp)
    je myauditsys2
    cmpq $59,ORIG_RAX-ARGOFFSET(%rsp)
    je myauditsys2
    cmpq $39,ORIG_RAX-ARGOFFSET(%rsp)
    je myauditsys2
    cmpq $56,ORIG_RAX-ARGOFFSET(%rsp)
    je myauditsys2
myaudit2:

第三处修改

/*
 *add myauditsys1
*/
myauditsys1:
    pushq %rax        //依次压栈,以便程序执行完从栈中恢复
    pushq %rdi        //64位上依次使用%rdi,%rsi,%rdx,%rcx,%r8,%r9存储参数
    pushq %rsi
    movq ORIG_RAX-ARGOFFSET+8*3(%rsp),%rdi    //参数1 系统调用号,8*3是因为之前pushq入栈了三次,rsp移动了24位
    movq RAX-ARGOFFSET+8*3(%rsp),%rsi    //参数2 系统调用返回值
    call sys_syscall_audit    //调用函数
    popq %rsi        //还原之前的值
    popq %rdi
    //popq %rax
    popq %rax
    jmp myaudit1        //跳转回去
/*
 *add myauditsys2
*/
myauditsys2:
    pushq %rax        //依次压栈,以便程序执行完从栈中恢复
    pushq %rdi
    pushq %rsi
    movq ORIG_RAX-ARGOFFSET+8*3(%rsp),%rdi    //参数1 系统调用号
    movq RAX-ARGOFFSET+8*3(%rsp),%rsi    //参数2 系统调用返回值
    call sys_syscall_audit    //调用函数
    popq %rsi
    popq %rdi
    //popq %rax
    popq %rax
    jmp myaudit2        //跳转回去
//----------------上面的是添加的-----------------
END(system_call)

对于寄存器的使用以及如何变化的,参考了如下文章进行学习:

《coredump问题原理探究》windows版3.1节函数桢
X86-64寄存器和栈帧

3.2.3 编译测试

在完成了系统调用入口的修改之后,同样要编译测试。如果出错,则查找错误原因。直到正确再进行下一步。
用新内核进入系统之后,我们可以通过dmesg命令查看内核输出。如果修改成功,应该能看到结果。因为我们现在没有插入myaudit模块,所以按照sys_syscall_audit逻辑直接会输出。
修改系统调用入口

3.3 myaudit模块的编写

3.3.1 程序说明

函数syscall_audit存储本次系统调用,sys_audit则取出调用记录。
由于在当前内核下xtime不再导出,所以暂时屏蔽记录时间。
uid需要通过current_uid()取出,current里没有uid变量。

3.3.2 代码

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/time.h>
#include <linux/cred.h>
#include <asm/current.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

#define COMM_SIZE 16

struct syscall_buf {
    u32 serial;
    //u32 ts_sec;
    //u32 ts_micro;
    u32 syscall;
    u32 status;
    pid_t pid;
    uid_t uid;
    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_pos < AUDIT_BUF_SIZE) {
        ppb_temp = &audit_buf[current_pos];
        ppb_temp->serial = 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;
        //ppb_temp->uid = current->uid;
        ppb_temp->uid = current_uid().val;

        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("Eror:claer_user\n");
            return 0;
        }
        printk("IN MOUDLE_systemcall:starting...\n");
        ret = wait_event_interruptible(buffer_wait, current_pos >= AUDIT_BUF_SIZE*8/10);
        printk("IN MOUDLE_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 - 1;
        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");

3.3.3 Makefile

由于使用了my_audit和my_sysaudit,来自内核的全局函数。当我们的模块需要使用这些内核导出的函数时,在make时需要加入内核根目录的Module.symvers,加载这些额外的符号对应地址,否则无法使用,提示no symbol version for。
在Makefile加入

KBUILD_EXTRA_SYMBOLS += $(LINUX_KERNEL_PATH)/Module.symvers
export KBUILD_EXTRA_SYMBOLS

参考 关于内核模块挂载出现“no symbol version for”问题的研究

3.3.4 编译测试

编译挂载到内核,查看dmesg是否出错。
插入模块
- myaudit: module verification failed: signature and/or required key missing - tainting kernel信息只是说该模块签名有问题,可能不可信,但是内核仍然允许模块运行。
- Starting System Call Auditing则说明内核插入成功。

最后需要配合用户态程序来检查结果是否正确了。

3.4 用户态程序user_audit的编写

在用户态程序中我直接使用syscall函数调用317号系统调用,即调用myaudit。

//user_audit.c
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/syscall.h>
#include <sys/types.h>

typedef unsigned char u8;
typedef unsigned int u32;

#define COMM_SIZE 16

struct syscall_buf {
    u32 serial;
    u32 syscall;
    u32 status;
    pid_t pid;
    uid_t uid;
    u8 comm[COMM_SIZE];
};

#define AUDIT_BUF_SIZE 100*sizeof(struct syscall_buf)

int main(int argc, char *argv[])
{
    u8 col_buf[AUDIT_BUF_SIZE];
    unsigned char reset = 1;
    int num = 0;
    struct syscall_buf *p;
    while(1) {
        num = syscall(317, 0, col_buf, AUDIT_BUF_SIZE, reset);
        printf("num:%d\n",num);
        u8 j = 0;
        int i;
        p = (struct syscall_buf *)col_buf;
        for(i = 0; i < num; i++) {
            printf("num[%d], serial:%d\t",i, p[i].serial);
            printf("syscall:%d\n", p[i].syscall);
            printf("status:%d\n", ((struct syscall_buf *)col_buf)[i].status);
            printf("pid:%d\n", ((struct syscall_buf *)col_buf)[i].pid);
            printf("uid:%d\n", ((struct syscall_buf *)col_buf)[i].uid);
            printf("comm:%s\n", ((struct syscall_buf *)col_buf)[i].comm);
        }
    }
    return 1;
}

4 实验结果

做完了所有的工作之后。我们可以先插入myaudit内核模块,然后打开用户态程序user_audit。通过dmesg命令查看结果。
这里以一个test_fork小程序为例,查看结果。
结果截图:
结果
serial代表模块总共输出数,syscall代表系统调用号,六十四位系统上56代表fork。
status代表返回值,3220正是fork向父进程返回的子进程pid,pid代表此系统调用的进程号。
uid代表用户id,当前为1000,我们通过id + 用户名命令可以看到,当前用户id为1000。
用户ID
comm代表此调用的进程名。

展开阅读全文

没有更多推荐了,返回首页