19内核审计

19内核审计

19.1 概述

程序员通常对相对底层的信息感兴趣,而管理员则更需要一个高层的视图:哪个进程打开了网络连接?哪个用户启动了程序?内核在何时授予或拒绝了特定的权限?(这包括检查是否有(以及哪个)用户比较游手好闲,去窥视他们无权访问的文件。) 为回答这些问题,内核提供了审计子系统。

审计机制有如下两个关键性的约束:

  1. 用于选择所记录事件类型的规则,必须能够动态改变。特别是,不能要求重启系统或插入/移除内核模块
  2. 在使用审计特性时,系统性能不能下降太多。而禁用审计机制,也不应该对系统性能带来负面影响。

下图给出了审计子系统总体设计的略图。内核包含了一个规则数据库,其规则用于指定记录哪些事件。该数据库由用户层借助auditctl工具填写。如果特定事件发生,而内核根据数据库判断必须审计该事件,则会向auditd守护进程发送一个消息。该守护进程可以将消息存储到一个日志文件,供进一步检查。用户层和内核之间的通信(规则操作和消息传输)借助一个netlink 套接字进行(这种连接机制在第12章讨论过)。审计机制的内核和用户层部分是彼此依赖的。因为如果只记录出现得相对不那么频繁的事件,审计对内核的影响会降到最低限度,其实现亦称为轻量级审计框架(lightweight auditing framework)。

在这里插入图片描述

为进一步降低对系统性能的影响,审计机制会区分两种类型的审计事件,如下所述:

  1. 系统调用审计:允许在内核进入或退出系统调用时进行记录。尽管可以指定附加约束,限制所记录事件的数目(如限制到特定的UID),但系统调用发生的频率仍然太高。因而,如果采用系统调用审计,对系统性能造成一定的影响是不可避免的
  2. 所有不直接关联到系统调用的其他类型事件,都会单独处理。完全可以禁用系统调用审计,只记录特定类型的事件。这对系统负荷仅有极轻微的影响。

重要的是理解审计和更规范性的技术如系统调用追踪之间的差别(和关系)。如果一个被审计的进程通过分支创建子进程,那么与审计相关的属性将被继承。这允许建立审计线索(audit trail),对从整体上观察某个应用程序的行为,或跟踪特定用户的操作,都是很重要的。通常,与纯粹的系统调用追踪(由ptrace实现)相比,审计机制允许以一种更面向任务的方式(即从一个更高层的视角)来跟踪(受信任的)应用程序。各种产生审计事件的挂钩发布在内核中各处,但几乎内核所有部分都可以通过代码进行扩展,来发送特定的审计消息。

尽管审计是一个相当通用的机制,但该特性最值得注意的使用者是SELinux和AppArmor(SELinux的竞争者,未包含在官方的内核源代码中,但被OpenSUSE采用)。

如何设置约束,对特定的事件类型生成审计日志记录?这是借助审计规则完成的,本章将讨论审
计规则的格式和用途。但读者还应该查阅与审计框架相关的手册页,以获得更多信息,特别是
auditctl(8)。通常,一个审计规则由以下几部分组成

19.2 审计规则

auditctl(8)查看设计框架。通常,一个审计规则由以下几部分组成:

  1. 基本信息由一个过滤器/值对给出的。过滤器表示该规则所属的事件的种类。例如,对系统调用入口来说,过滤器是entry,对创建进程来说,过滤器是task
  2. 值可以是NEVER或ALWAYS。后者用于启用规则,而前者用于禁止产生审计事件。这是非常有意义的,因为给定过滤器类型的所有规则都保存在一个列表中,匹配的第一个规则将被应用。通过将一个NEVER规则放在前面,可以(暂时)禁止对可产生审计事件的规则的处理

过滤器将可审计事件的集合划分为更小的类别,但这些类别所涉及的内容仍然十分宽泛。需要更多的约束,才能选择可实际控制的事件子集。这可以通过指定若干字段/比较器/值元组来约束。字段是内核可以观测的量。例如,这可以是某个UID、进程标识符、设备号或某个系统调用的参数。比较器和值可以对字段指定一些条件。如果这些条件满足,则生成一条审计记录事件。否则,不生成审计事件。这里可以使用常见的比较运算符(小于、小于等于,等等)。向内核提供新规则是通过auditctl工具,通常如下调用:

root@meitner # auditctl -a filter,action -F field=value

对root用户创建新进程的所有事件进行审计:

root@meitner # auditctl -a task,always -F euid=0

在审计系统调用时,也可能(而且这是非常可取的)限制只对特定的系统调用生成审计记录。下列例子通知内核记录UID为1000的用户未能打开文件的所有事件:

root@meitner # auditctl -a exit,always -S open -F success=0 -F auid=1000

如果该用户试图打开/etc/shadow,但未能提供所需的凭据,将产生以下日志记录:

root@meitner # cat /etc/audit/audit.log
... 
type=SYSCALL msg=audit(1201369614.531:1518950): arch=c000003e syscall=2 
    success=no exit=-13 a0=71ac78 a1=0 a2=1b6 a3=0 items=1 ppid=3900 pid=8358 
    auid=4294967295 uid=1000 gid=100 euid=1000 suid=1000 fsuid=1000 egid=100 
    sgid=100 fsgid=100 tty=pts0 comm="cat" exe="/usr/bin/cat" key=(null) 
...

19.3 实现

审计实现属于内核最核心的部分(其源代码位于kernel/目录下)。这些凸显了内核开发者对该框架重要性的强调。与核心内核目录下的所有其他代码相似,开发者花费了很多心思,使得该代码紧凑、高效并尽可能干净。该代码基本上分布在以下3个文件中

  1. kernel/audit.c提供了核心的审计机制
  2. kernel/auditsc.c实现了系统调用审计
  3. kernel/auditfilter.c包含了过滤审计事件的机制

另一个文件是kernel/audit_tree.c,其中包含了一些数据结构和例程,可以对整个目录树进行审计。由于这个方面需要相当多代码,而实现带来的收益相对较小,所以为简单起见本章不进一步讨论该选项。

所用记录格式的详细文档、相关工具的用法描述等信息,都可以在开发者的网站http://people.redhat.com/peterm/audit上找到,相应的手册页也进行了描述。记住这些,读者就可以深入到本节所描述的实现细节之中了!

19.3.1 数据结构

审计机制使用的数据结构分为3个主要的类别。首先,需要向进程中加入一个各任务数据结构。其次,审计事件、过滤规则等都需要在内核中表示出来。最后,需要与用户层实用程序建立一种通信机制

下图说明了各种不同数据结构的关联,这些形成了审计机制的核心。task_struct中加入了审计上下文存储与系统调用相关的所有数据,建立了一个包含所有审计规则的数据库。这里,对用于在内核和用户空间传输审计数据的数据结构不是特别感兴趣,因而下图中没有包括进去。

在这里插入图片描述

  1. 对task_struct的扩展
    系统中的每个进程 struct task_struct 实例,这在第2章讨论过。该结构的一个指针类型的成员用于向进程增加审计上下文,如下所示:

    //include/linux/sched.h
    //进程描述符(PCB:Process Control Block进程控制块)
    struct task_struct {
        ...
        struct audit_context *audit_context;//审计机制结构体,可能为NULL,进行审计系统调用时分配
        ...
    };
    
    //kernel/auditsc.c
    struct audit_context {//审计机制结构体
        int		    in_syscall;	/*如果进程处于系统调用中,则为1*//* 1 if task is in a syscall */
        enum audit_state    state;//审计的活动级别,进程当前的审计状态
        unsigned int	    serial;     /*审计记录的序列号*//* serial number for record */
        struct timespec	    ctime;      /*系统调用进入的时间*//* time of syscall entry */
        uid_t		    loginuid;   /*登录的uid(身份)*//* login uid (identity) */
        int		    major;      /*系统调用编号*//* syscall number */
        unsigned long	    argv[4];    /*系统调用参数*//* syscall arguments */
        int		    return_valid; /*返回码是有效的?*//* return code is valid */
        long		    return_code;/*系统调用返回码*//* syscall return code */
        int		    auditable;  /*如果应该写出记录,则为1*//* 1 if record should be written */
        int		    name_count;//记录了当前处于使用中的names数组项数目
        struct audit_names  names[AUDIT_NAMES];//文件系统对象有关信息
        char *		    filterkey;	/*触发该记录的审计规则的键*//* key for rule that triggered record */
        struct dentry *	    pwd;
        struct vfsmount *   pwdmnt;
        struct audit_context *previous; /*用于嵌套的系统调用*//* For nested syscalls */
        struct audit_aux_data *aux;//存储审计上下文之外的辅助数据,实际为其他类型,如 audit_aux_data_ipcctl
        struct audit_aux_data *aux_pids;//存储审计上下文之外的辅助数据,用于注册进程的PID,这些进程将在有系统调用被审计时接收信号
    
                    /* Save things to print about task_struct */
                    /*pid、sgid、personality等成员定义在结构尾部,与task_struct中的对应成员形成对照。其值是由一个给定task_struct的实例复制而来的,以便在不访问task_struct的情况下也可获取这些值。*/
        pid_t		    pid, ppid;
        uid_t		    uid, euid, suid, fsuid;
        gid_t		    gid, egid, sgid, fsgid;
        unsigned long	    personality;
        int		    arch;
    };
    
    struct audit_names {//审计系统用于存储文件系统对象的常见属性
        const char	*name;
        int		name_len;	/* number of name's characters to log */
        unsigned	name_put;	/* call __putname() for this name */
        unsigned long	ino;
        dev_t		dev;
        umode_t		mode;
        uid_t		uid;
        gid_t		gid;
        dev_t		rdev;
        u32		osid;
    };
    
    //kernel/audit.h
    enum audit_state {
        AUDIT_DISABLED,		/*不记录系统调用*/
        AUDIT_SETUP_CONTEXT,	/*未使用*/
        AUDIT_BUILD_CONTEXT,	/*创建审计上下文,在系统调用进入时填写系统调用数据*/
        AUDIT_RECORD_CONTEXT	/*创建审计上下文,在系统调用进入时填写数据,在系统调用退出时写出审计记录*/
    };
    

    操作的名称与为state定义的常数名关系。下列代码片段来源于规则处理状态机,描述了操作名与常数名的关系(关于如何将审计规则传输到内核的更多信息,请参考19.3.1节):

    //kernel/auditsc.c
    switch (rule->action) {
    case AUDIT_NEVER:    *state = AUDIT_DISABLED;	    break;
    case AUDIT_ALWAYS:   *state = AUDIT_RECORD_CONTEXT; break;
    }
    

    审计附加数据使用:

    //kernel/auditsc.c
    //审计结构附加信息,数据结构的其他成员提供实际数据
    struct audit_aux_data {
        struct audit_aux_data	*next;//实现单链表
        int			type;//辅助数据的类型
    };
    
    struct audit_aux_data_ipcctl {//用于AUDIT_IPC和AUDIT_IPC_SET_PERM类型的辅助对象
        struct audit_aux_data	d;
        struct ipc_perm		p;
        unsigned long		qbytes;
        uid_t			uid;
        gid_t			gid;
        mode_t			mode;
        u32			osid;
    };
    

    审计辅助数据结构:

    • audit_aux_data_ipcctl(用于AUDIT_IPC和AUDIT_IPC_SET_PERM类型的辅助对象)。
    • audit_aux_data_socketcall(类型AUDIT_SOCKETCALL)。
    • audit_aux_data_sockaddr(类型AUDIT_SOCKADDR)。
    • audit_aux_data_datapath(类型AUDIT_AVC_PATH)。
    • audit_aux_data_data_execve(类型AUDIT_EXECVE)。
    • audit_aux_data_mq_{open, sendrewcv, notify, getsetattr}(类型AUDIT_MQ_{OPEN,SENDRECV, NOTIFY, GETSETATTR})。
    • audit_aux_data_fd_pair(类型AUDIT_FD_PAIR)。
  2. 记录、规则和过滤

    //kernel/audit.c
    struct audit_buffer {//审计缓冲区,用于正在格式化的审计记录
        struct list_head     list;//链表元素,用于将审计缓冲区存储在各种链表上
        struct sk_buff       *skb;	/*由于netlink 套接字用于在内核和用户层之间通信,消息使用了一个sk_buff类型的套接字缓冲区进行封装*//* formatted skb ready to send */
        struct audit_context *ctx;	/*与审计上下文的关联由ctx实现(如果因为禁用了系统调用审计,而没有审计上下文存在,该成员也可能是NULL指针)*//* NULL or associated context */
        gfp_t		     gfp_mask;//确定了从哪个内存域分配内存
    };
    

    由于审计缓冲区经常使用,内核保持了若干预分配的audit_buffer实例,随时可使用。audit_buffer_alloc负责分配和初始化新缓冲区,audit_buffer_free负责释放缓冲区,对审计缓冲区缓存的处理是由这些函数隐式进行的。其实现比较简单

    从用户空间传输到内核的审计规则,由下列数据结构表示(前一个内核版本采用了稍微简单些的struct audit_rule,它不允许使用非整数或变长的字符串数据字段。该结构仍然存在于内核中,以便向用户空间提供后向兼容性,但新代码不能使用该结构。):

    //include/linux/audit.h
    //从用户空间传输到内核的审计规则,由下列数据结构表示,audit_rule_data结构体的简化,不能使用字符串,前一个版本内核使用,用于用户空间向后兼容,新代码不能使用该结构体
    struct audit_rule {		/* for AUDIT_LIST, AUDIT_ADD, and AUDIT_DEL */
        __u32		flags;	/* AUDIT_PER_{TASK,CALL}, AUDIT_PREPEND */
        __u32		action;	/* AUDIT_NEVER, AUDIT_POSSIBLE, AUDIT_ALWAYS */
        __u32		field_count;
        __u32		mask[AUDIT_BITMASK_SIZE];
        __u32		fields[AUDIT_MAX_FIELDS];
        __u32		values[AUDIT_MAX_FIELDS];
    };
    
    struct audit_rule_data {//从用户空间传输到内核的审计规则,由下列数据结构表示
        __u32		flags;	/*表示改规则的激活时机,AUDIT_FILTER_USER 等*//* AUDIT_PER_{TASK,CALL}, AUDIT_PREPEND */
        __u32		action;	/*规则匹配时的操作,AUDIT_NEVER 等*//* AUDIT_NEVER, AUDIT_POSSIBLE, AUDIT_ALWAYS */
        __u32		field_count;//表示规则中包含的字段/值对的数目
        __u32		mask[AUDIT_BITMASK_SIZE]; /*如果启用了系统调用审计,通过位串指定了审计操作影响的系统调用*//* syscall(s) affected */
        __u32		fields[AUDIT_MAX_FIELDS];//字段,用于指定审计规则适用的条件,字段中指定的量标识内核内部的某个对象,如一个进程ID,而值则与一些比较运算符联用(例如,小于、大于,等等),指定可触发审计事件的字段值集合,fields数组项的可能值在<audit.h>中列出,如 AUDIT_PID
        __u32		values[AUDIT_MAX_FIELDS];//值对,用于指定审计规则适用的条件,指定可触发审计事件的字段值集合,该数组只能指定数值,如果要创建涉及文件名等非数值量的规则,这是不够的。因而向struct audit_rule_data尾部添加了一个字符串成员。它可以通过伪数组buf访问,字符串长度由buflen表示。
        __u32		fieldflags[AUDIT_MAX_FIELDS];//保存运算符标志
        __u32		buflen;	/*buf[0]的长度*//* total length of string fields */
        char		buf[0];	/*字符串数组,保存values的非数值量的规则,如文件名等*//* string fields buffer */
    };
    
    /* Rule flags */
    #define AUDIT_FILTER_USER	0x00	/*对用户产生的消息应用规则*//* Apply rule to user-generated messages */
    #define AUDIT_FILTER_TASK	0x01	/*在进程创建(不是系统调用)时应用规则*//* Apply rule at task creation (not syscall) */
    #define AUDIT_FILTER_ENTRY	0x02	/*在系统调用进入时应用规则*//* Apply rule at syscall entry */
    #define AUDIT_FILTER_WATCH	0x03	/*应用该规则监视文件系统*//* Apply rule to file system watches */
    #define AUDIT_FILTER_EXIT	0x04	/*在系统调用退出时应用规则*//* Apply rule at syscall exit */
    #define AUDIT_FILTER_TYPE	0x05	/*在audit_log_start时应用规则*//* Apply rule at audit_log_start */
    

    在规则匹配时,可以进行两种操作(由action表示)。AUDIT_NEVER表示什么都不做,AUDIT_ALWAYS生成审计记录(AUDIT_POSSIBLE仍然作为另一个选项列出,但该值已经废弃,不再使用)
    如果启用了系统调用审计,mask通过位串指定了审计操作影响的系统调用
    字段/值对(即fields和values两个数组)用于指定审计规则适用的条件。字段中指定的量标识内核内部的某个对象,例如,可能为一个进程ID。而值则与一些比较运算符联用(例如,小于、大于,等等),指定可触发审计事件的字段值集合。一个特定的例子就是,“在PID为0的进程打开一个消息队列时,创建一个审计记录”。fields和values数组表示这样的对儿,而运算符标志则保存在fieldflags成员中。field_count表示规则中包含的字段/值对的数目。fields数组项的可能值在<audit.h>中列出。内核为此定义了许多值,本节不可能全部详细列出,读者可以参考用户层审计工具的文档。通常,所定义的常数名称都是自明的,如 AUDIT_PID

    内核内部的审计规则表示如下:

    //kernel/audit.h
    struct audit_field {//内核内部的审计规则表示
        u32				type;
        u32				val;
        u32				op;
        char				*se_str;
        struct selinux_audit_rule	*se_rule;
    };
    struct audit_krule {//内核内部的审计规则表示
        int			vers_ops;
        u32			flags;
        u32			listnr;//数组下标,表示规则在审计过滤器链表数组的哪个成员的链表头中
        u32			action;
        u32			mask[AUDIT_BITMASK_SIZE];
        u32			buflen; /* for data alloc on list rules */
        u32			field_count;
        char			*filterkey; /* ties events to rules */
        struct audit_field	*fields;//所有的规则都包含在fields指向的数组中,每个规则由一个struct audit_field实例表示
        ...
    };
    

    其内容类似于struct audit_rule_data,但采用的数据类型能够更方便地操作和遍历。

    为在两种审计规则之间表示转换,内核提供了辅助函数audit_rule_to_entry。只需要知道,该例程获取一个struct audit_rule实例,将其转换为一个struct audit_entry实例,后者是audit_krule的容器。

    //kernel/audit.h
    struct audit_entry {//audit_krule结构体的容器,将规则存储在过滤器链表上
        struct list_head	list;//链表元素,表头为 audit_filter_list
        struct rcu_head		rcu;
        struct audit_krule	rule;
    };
    

    该容器将规则存储在过滤器链表上。audit_filter_list提供了6个不同的过滤器链表。

    //audit_filter_list提供了6个不同的过滤器链表,每个链表都对应着一个AUDIT_FILTER_宏,该宏定义了应用规则的特定时机,而满足条件的所有规则,都保存在对应的链表上
    struct list_head audit_filter_list[AUDIT_NR_FILTERS] = {
        LIST_HEAD_INIT(audit_filter_list[0]),
        LIST_HEAD_INIT(audit_filter_list[1]),
        LIST_HEAD_INIT(audit_filter_list[2]),
        LIST_HEAD_INIT(audit_filter_list[3]),
        LIST_HEAD_INIT(audit_filter_list[4]),
        LIST_HEAD_INIT(audit_filter_list[5]),
    };
    

    每个链表都对应着一个AUDIT_FILTER_宏,该宏定义了应用规则的特定时机,而满足条件的所有规则,都保存在对应的链表上。
    注意,在auditd守护进程向内核发送适当的请求时,将调用audit_add_rule添加新的规则

19.3.2 审计子系统初始化 audit_init

有一个内核命令行参数(audit)可以设置为0或1。在初始化期间,该值存储在全局变量enable_audit中。如果设置为0,则完全禁用审计。在该值设置为1时,将启用审计,但在默认情况下没有提供任何规则,仅在向内核提供了适当规则的情况下,才会生成审计事件。

  • audit_init 审计子系统初始化
    • netlink_kernel_create 创建netlink套接字用于与用户层通信,通信函数为 audit_receive

还有一个内核线程用于审计机制。但该线程并非在子系统初始化期间启动,而采用了一种有些异乎寻常的方式:一旦用户空间守护进程auditd发送第一条消息,即启动内核线程kauditd_task。该线程执行的函数是kauditd_thread,负责将准备好的消息从内核发送到用户空间守护进程。请注意,该守护进程是必要的,因为审计事件可能在中断处理程序内部结束,但netlink函数不能在该上下文中使用,完成的审计记录将被放在一个队列上,稍后由内核守护进程(请注意,这里的守护进程是指内核线程)处理,将其发送回用户空间。消息的发送和接收都是通过简单的netlink操作和标准的队列处理函数完成,如第12章讲述的。

19.3.3 处理请求 audit_receive

每当有一个新的请求通过 netlink 套接字到达时,网络子系统都会调用audit_receive。该函数的代码流程图如下图所示。
在这里插入图片描述

  • audit_receive 接受应用层发来的审计信息
    • audit_receive_skb
      • while:
      • nlmsg_hdr 提取出netlink消息首部
      • audit_receive_msg 处理审计子系统收到用户空间的单个请求
      • netlink_ack 应答消息
      • skb_pull 移除处理过的数据,确切地说,该函数没有删除数据,只是相应地设置了套接字缓冲区的数据指针

audit_receive_msg 流程图如下:
在这里插入图片描述

  • audit_receive_msg 处理审计子系统收到用户空间的单个请求

    • audit_netlink_ok 验证发送者是否允许执行该请求
    • kthread_run(kauditd_thread, NULL, “kauditd”) 启动审计线程
    • 根据不同的消息进行对应的处理
  • kauditd_thread 内核审计子系统线程处理函数

    • while:
    • skb_dequeue 从审计套接字缓冲区链表上取下缓冲区skb
    • wake_up 唤醒等待的进程
    • 有需要发送的套接字缓冲区,netlink机制向应用层发送socket数据通过 netlink_unicast 函数
    • 如果没有需要发送的套接字缓冲区,则将当前进程放入等待队列进行等待

以内核向规则数据库添加新的审计规则为例:

  • audit_receive_msg 处理审计子系统收到用户空间的单个请求
    • audit_netlink_ok 验证发送者是否允许执行该请求
    • kthread_run(kauditd_thread, NULL, “kauditd”) 启动审计线程
    • 处理 AUDIT_ADD_RULE 类型的请求,添加审计规则,调用 audit_receive_filter
      • audit_data_to_entry 获取一个来自用户空间的 struct audit_rule_data 实例,将其转换为内核空间的审计规则表示 struct audit_krule 实例
      • audit_add_rule 将新规则加入 audit_filter_list 中对应的审计规则链表上
      • audit_log_rule_change 将新增规则的消息发给用户层的审计守护进程

19.3.4 记录事件

在所有基础设施都就位之后,现在开始详细讲述审计是如何实现的。该过程分为3个阶段。首先,用audit_log_start开始记录过程。然后,audit_log_format格式化一个记录消息,最后用audit_log_end结束该审计记录,消息将排队传输到审计守护进程。

  1. 开始审计 audit_log_start
    调用audit_log_start开始审计,流程图如下:
    在这里插入图片描述

    audit_log_start的工作是建立一个audit_buffer实例,并将其返回给调用者;但在此之前,需要考虑积压队列的长度限制和发送数据的速率限制。

    积压队列(即存储完成后的审计记录的队列)的最大长度由全局变量audit_backlog_limit给出。如果队列长度大于该值(请注意,分配时没有指定_GFP_WAIT标志的审计记录,内核将认为这种审计记录更为紧急。阻止此类审计记录创建的积压队列长度阈值,比其他分配类型的阈值要高一些。),那么audit_log_start将调度一个超时定时器,以便在稍后重试该操作,预期在此期间审计记录的积压已经减轻。此外,还需要检查数据发送速率,确保每秒发送的消息数不超过特定的限制。全局变量audit_rate_limit确定了最大的数据发送速率。如果超过该速率,则向守护进程发送一个消息表明该情况,并停止分配audit_buffer实例。为避免拒绝服务攻击,这些措施是必要的,它们针对发生频率过高的审计事件提供了保护。

    如果积压队列长度和数据速率限制的检查都能够通过,即允许创建新的审计缓冲区,则使用audit_buffer_alloc来分配一个audit_buffer实例。在该缓冲区返回给调用者之前,audit_get_stamp提供了一个唯一的序列号,并向缓冲区写入一个初始的记录消息,其中包含创建时间和序列号。

    • audit_log_start 开始审计
      • 处理队列中审计消息过多,调用 schedule_timeout 休眠稍后开始审计,等待队列中审计消息减少.并检查审计消息发送速率,如果发送过快,通知应用层发送过快.结束函数放弃审计
      • audit_buffer_alloc 申请审计缓冲区
  2. 写入记录消息 audit_log_format
    audit_log_format 用于向一个给定的审计缓冲区写入一条记录消息

    //kernel/audit.c
    //向一个给定的审计缓冲区写入一条记录消息
    void audit_log_format(struct audit_buffer *ab, const char *fmt, ...)
    

    audit_log_format是printk的一个变体。它会计算fmt给出的格式串并用va_args参数列表给出的参数进行填充,结果串写入到与审计缓冲区相关的套接字缓冲区的数据空间。

  3. 结束审计记录 audit_log_end
    在所有必要的记录消息都已经写入到审计缓冲区之后,需要调用audit_log_end确保将审计记录发送给用户空间守护进程。该函数的代码流程图如下图所示。
    在这里插入图片描述

    在进行数据发送速率的检查后(如果消息发送太频繁,那么当前的消息会丢失,且会将一个“数据发送速率超限”的消息发送给守护进程),与该审计缓冲区相关联的套接字缓冲区就被放置到一个队列上,稍后由kauditd处理:

    • audit_log_end结束审计记录
      • audit_rate_check 检查数据发送速率
      • skb_queue_tail 将审计发给用户层的审计消息放入队列,稍后审计进程会处理
      • wake_up_interruptible 唤醒等待队列中的审计线程
//kernel/audit.c
//浓缩了 开始审计记录,写入消息,结束记录
void audit_log(struct audit_context *ctx, gfp_t gfp_mask, int type,
	       const char *fmt, ...)

19.3.5 系统调用审计

到现在为止,已经描述了系统调用审计所需的所有数据结构和机制,本节将描述系统调用审计的实现。系统调用审计不同于基本的审计机制,它依赖task_struct中扩展出的审计上下文

  1. 分配审计上下文 audit_alloc
    首先,需要考虑是在何种环境下分配审计上下文。因为这是一个代价很高的操作,所以仅在显式启用了系统调用审计的情况下,才会执行该操作。如果确实启用了系统调用审计,那么将在copy_process(源于fork系统调用)中调用audit_alloc来分配一个新的struct audit_context实例。下图给出了audit_alloc的代码流程图:
    在这里插入图片描述

    • audit_alloc 分配进程的审计上下文结构体实例,在fork函数中调用
      • audit_filter_task 确定是否需要对当前进程激活系统调用审计,该函数应用注册类型为 AUDIT_FILTER_TASK的过滤器.如果审计系统停用了,结束函数
      • audit_alloc_context 申请审计上下文结构体
      • 保存当前运行进程的登录UID,防止fork后导致uid为子进程
  2. 系统调用事件
    系统调用进入和完成时会涉及审计子系统,进入时将调用audit_syscall_entry,完成时将调用audit_syscall_exit。这需要底层、特定于体系结构的中断处理代码的支持。该支持集成在do_syscall_trace中,每当中断发生或中断处理完成时,底层的中断处理代码都会调用该函数(此外,还需要设置TIF_SYSCALL_AUDIT标志。在audit_alloc中,如果审计过滤器确定需要对一个进程激活审计,则设置该标志)。

    IA-32体系结构中如下:

    //arch/x86/kernel/ptrace_32.c
    __attribute__((regparm(3)))
    int do_syscall_trace(struct pt_regs *regs, int entryexit)
    
    • do_syscall_trace
      • audit_syscall_entry
      • audit_syscall_exit

    在这里插入图片描述

    如果系统调用发生在另一个系统调用被审计期间,那么可能需要将多个审计上下文连接起来—分配一个新的审计上下文,将前一个与它连接起来,新分配的审计上下文的用法与前一个类似。

    系统调用编号、传递到系统调用的参数(由a1… a4表示)及系统体系结构(如IA-32为AUDIT_ARCH_i386,还有表示其他体系结构的常数,都定义在<audit.h>中),这些都保存在审计上下文中

    根据进程的审计模式,需要使用audit_filter_syscall来进行过滤,该函数将应用内核中注册的所有适当的过滤器

    如果启用了审计,但没有定义审计规则,则context->dummy会设置为非0值。在这种情况下,过滤显然是不必要的。

    • audit_syscall_entry
      • audit_alloc_context 分配一个新的审计上下文与前一个上下文连接起来
      • audit_filter_syscall 应用内核中注册的所有适当的过滤器进行过滤

    退出时处理审计:
    在这里插入图片描述

    audit_log_exit 对审计上下文包含的信息创建了一个审计记录

    将系统调用编号、系统调用返回值和一些进程相关的一般性信息都记入到审计记录中。audit_syscall_exit必须确保将前一个审计上下文(如果存在)恢复为当前审计上下文,另外,还需要释放不再使用的资源。

    • audit_syscall_exit
      • audit_get_context 获取审计上下文
      • audit_log_exit 对审计上下文包含的信息创建了一个审计记录,audit_log_start到audit_log_end
      • 释放不再使用的资源
  3. 访问向量缓存审计
    有些情况下,审计会成为相当重要的功能需求,一个突出的例子就是SELinux访问向量缓存(access vector cache)。授予或拒绝权限由avc_audit函数执行,每当一个权限查询传递到安全服务器时,都会由avc_has_perm调用avc_audit。首先,该函数需要检查当前情况下是否需要审计(即授予或拒绝权限是否需要审计)

    //security/selinux/avc.c
    //SELinux授予或拒绝权限,由 avc_has_perm 调用
    void avc_audit(u32 ssid, u32 tsid,
                u16 tclass, u32 requested,
                struct av_decision *avd, int result, struct avc_audit_data *a)
    
    • avc_has_perm
      • avc_audit SELinux授予或拒绝权限,由 avc_has_perm 调用
        • 检查当前情况下是否需要审计,不需就结束
        • audit_log_start
        • audit_log_format
        • audit_log_end
  4. 为审计添加额外信息 sys_socketcall
    尽管对大多数系统调用来说,只记录进入和退出就足够了,但有些系统调用为审计子系统提供了更多信息。19.3.1节提到过,审计上下文提供了存储辅助数据的能力,有几个系统调用利用了该特性。在所有情形中,实现这些的方法几乎都是相同的,因此这里只以sys_socketcall为例进行说明。

    • sys_socketcall
      • audit_socketcall
        • ax = kmalloc
        • context->aux = (void *)ax

总结

审计机制使用了内核与应用层的通信机制netlink.用户层使用netlink套接字向内核注册审计规则.内核将审计消息发送给用户层auditd守护进程,该进程将消息存储到日志文件.

内核审计线程在接收到第一条用户空间的审计信息时创建

内核触发审计规则后创建审计信息,加入审计队列中

审计线程从审计队列中取出审计信息通过netlink机制向应用层发送消息

全局 audit_filter_list 数组每个元素为一个链表头,每个链表头代表一种需要审计的情景(如系统调用进入,退出,进程创建,文件系统监视等),每个情景可以自定义规则,规则放入链表头中

添加规则就是在 audit_filter_list 的链表头中增加元素

在不同情景中开始审计时都会去对应的链表中匹配规则,没有规则则不审计,有规则才组织审计信息给应用层

//kernel/audit.c
//audit_freelist中审计缓存数
static int	   audit_freelist_count;
//预分配的审计缓存链表头,存着空闲的审计缓存
static LIST_HEAD(audit_freelist);

//审计消息队列,内核根据规则生成的需要发送给用户层的审计消息都放入该队列,等待审计进程从中取消息来发送
static struct sk_buff_head audit_skb_queue;
//内核审计进程,从审计消息队列中取消息发送给用户层
static struct task_struct *kauditd_task;
//审计进程等待队列,审计进程没消息需要发送时在该队列中休眠
static DECLARE_WAIT_QUEUE_HEAD(kauditd_wait);
//积压等待队列,在审计消息队列中消息过多时,继续审计的进程会被放入该队列休眠,等待消息减少时唤醒
static DECLARE_WAIT_QUEUE_HEAD(audit_backlog_wait);

=========================================

涉及的命令和配置:

用户层 auditctl 工具设置审计规则

内核命令行参数 audit 可以设置为0或1。在初始化期间,该值存储在全局变
量enable_audit中。如果设置为0,则完全禁用审计.在该值设置为1时,将启用审计,但在默认情况下没有提供任何规则,仅在向内核提供了适当规则的情况下,才会生成审计事件

全局链表头数组 audit_filter_list,存着6个不同的过滤器链表,每个表头中存着一种过滤器的所有注册的规则
全局链表头 audit_freelist,存着预先分配的审计缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值