19内核审计
19.1 概述
程序员通常对相对底层的信息感兴趣,而管理员则更需要一个高层的视图:哪个进程打开了网络连接?哪个用户启动了程序?内核在何时授予或拒绝了特定的权限?(这包括检查是否有(以及哪个)用户比较游手好闲,去窥视他们无权访问的文件。) 为回答这些问题,内核提供了审计子系统。
审计机制有如下两个关键性的约束:
- 用于选择所记录事件类型的规则,必须
能够动态改变
。特别是,不能要求重启系统或插入/移除内核模块 - 在使用审计特性时,
系统性能不能下降太多
。而禁用审计机制,也不应该对系统性能带来负面影响。
下图给出了审计子系统总体设计的略图。内核包含了一个规则数据库,其规则用于指定记录哪些事件。该数据库由用户层借助auditctl
工具填写。如果特定事件发生,而内核根据数据库判断必须审计该事件,则会向auditd守护进程发送一个消息。该守护进程可以将消息存储到一个日志文件,供进一步检查。用户层和内核之间的通信(规则操作和消息传输)借助一个netlink 套接字进行
(这种连接机制在第12章讨论过)。审计机制的内核和用户层部分是彼此依赖的。因为如果只记录出现得相对不那么频繁的事件,审计对内核的影响会降到最低限度,其实现亦称为轻量级审计框架(lightweight auditing framework)。
为进一步降低对系统性能的影响,审计机制会区分两种类型的审计事件
,如下所述:
- 系统调用审计:允许在内核进入或退出系统调用时进行记录。尽管可以指定附加约束,限制所记录事件的数目(如限制到特定的UID),但系统调用发生的频率仍然太高。因而,如果采用系统调用审计,对系统性能造成一定的影响是不可避免的
- 所有不直接关联到系统调用的其他类型事件,都会单独处理。完全可以禁用系统调用审计,只记录特定类型的事件。这对系统负荷仅有极轻微的影响。
重要的是理解审计和更规范性的技术如系统调用追踪之间的差别(和关系)。如果一个被审计的进程通过分支创建子进程,那么与审计相关的属性将被继承。这允许建立审计线索(audit trail),对从整体上观察某个应用程序的行为,或跟踪特定用户的操作,都是很重要的。通常,与纯粹的系统调用追踪(由ptrace实现)相比,审计机制允许以一种更面向任务的方式(即从一个更高层的视角)来跟踪(受信任的)应用程序。各种产生审计事件的挂钩发布在内核中各处,但几乎内核所有部分都可以通过代码进行扩展,来发送特定的审计消息。
尽管审计是一个相当通用的机制,但该特性最值得注意的使用者是SELinux和AppArmor
(SELinux的竞争者,未包含在官方的内核源代码中,但被OpenSUSE采用)。
如何设置约束,对特定的事件类型生成审计日志记录?这是借助审计规则完成的,本章将讨论审
计规则的格式和用途。但读者还应该查阅与审计框架相关的手册页,以获得更多信息,特别是
auditctl(8)。通常,一个审计规则由以下几部分组成
19.2 审计规则
auditctl(8)查看设计框架。通常,一个审计规则由以下几部分组成:
- 基本信息由一个过滤器/值对给出的。过滤器表示该规则所属的事件的种类。例如,对系统调用入口来说,过滤器是entry,对创建进程来说,过滤器是task
- 值可以是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个文件中
- kernel/audit.c提供了核心的审计机制
- kernel/auditsc.c实现了系统调用审计
- kernel/auditfilter.c包含了过滤审计事件的机制
另一个文件是kernel/audit_tree.c
,其中包含了一些数据结构和例程,可以对整个目录树进行审计。由于这个方面需要相当多代码,而实现带来的收益相对较小,所以为简单起见本章不进一步讨论该选项。
所用记录格式的详细文档、相关工具的用法描述等信息,都可以在开发者的网站http://people.redhat.com/peterm/audit
上找到,相应的手册页也进行了描述。记住这些,读者就可以深入到本节所描述的实现细节之中了!
19.3.1 数据结构
审计机制使用的数据结构分为3个主要的类别
。首先,需要向进程中加入一个各任务数据结构。其次,审计事件、过滤规则等都需要在内核中表示出来。最后,需要与用户层实用程序建立一种通信机制
下图说明了各种不同数据结构的关联,这些形成了审计机制的核心。task_struct中加入了审计上下文存储与系统调用相关的所有数据,建立了一个包含所有审计规则的数据库。这里,对用于在内核和用户空间传输审计数据的数据结构不是特别感兴趣,因而下图中没有包括进去。
-
对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)。
-
记录、规则和过滤
//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_skb
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
结束该审计记录,消息将排队传输到审计守护进程。
-
开始审计 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 申请审计缓冲区
- audit_log_start 开始审计
-
写入记录消息 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参数列表给出的参数进行填充,结果串写入到与审计缓冲区相关的套接字缓冲区的数据空间。
-
结束审计记录 audit_log_end
在所有必要的记录消息都已经写入到审计缓冲区之后,需要调用audit_log_end确保将审计记录发送给用户空间守护进程。该函数的代码流程图如下图所示。
在进行数据发送速率的检查后(如果消息发送太频繁,那么当前的消息会丢失,且会将一个“数据发送速率超限”的消息发送给守护进程),与该审计缓冲区相关联的套接字缓冲区就被放置到一个队列上,稍后由kauditd处理:
- audit_log_end结束审计记录
- audit_rate_check 检查数据发送速率
- skb_queue_tail 将审计发给用户层的审计消息放入队列,稍后审计进程会处理
- wake_up_interruptible 唤醒等待队列中的审计线程
- audit_log_end结束审计记录
//kernel/audit.c
//浓缩了 开始审计记录,写入消息,结束记录
void audit_log(struct audit_context *ctx, gfp_t gfp_mask, int type,
const char *fmt, ...)
19.3.5 系统调用审计
到现在为止,已经描述了系统调用审计所需的所有数据结构和机制,本节将描述系统调用审计的实现。系统调用审计不同于基本的审计机制,它依赖task_struct中扩展出的审计上下文
-
分配审计上下文 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为子进程
- audit_alloc 分配进程的审计上下文结构体实例,在fork函数中调用
-
系统调用事件
在系统调用
进入和完成时会涉及审计子系统,进入时将调用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
- 释放不再使用的资源
- do_syscall_trace
-
访问向量缓存审计
有些情况下,审计会成为相当重要的功能需求,一个突出的例子就是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
- avc_audit SELinux授予或拒绝权限,由 avc_has_perm 调用
- avc_has_perm
-
为审计添加额外信息 sys_socketcall
尽管对大多数系统调用来说,只记录进入和退出就足够了,但有些系统调用为审计子系统提供了更多信息。19.3.1节提到过,审计上下文提供了存储辅助数据的能力,有几个系统调用利用了该特性。在所有情形中,实现这些的方法几乎都是相同的,因此这里只以sys_socketcall
为例进行说明。- sys_socketcall
- audit_socketcall
- ax = kmalloc
- context->aux = (void *)ax
- audit_socketcall
- sys_socketcall
总结
审计机制使用了内核与应用层的通信机制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
,存着预先分配的审计缓存