审计系统在内核的实现部分主要包括是审计内核模块、kauditd内核线程、进程审计和文件系统的审计。审计系统的内核态子系统不仅要能够响应用户态所发送的请求(比如增加或删除审计规则),并且还要在内核发生审计规则所指定的事件时记录并生成日志。
本文将对审计系统在Linux内核中的实现做简单说明,重点说明审计内核模块和kuaditd内核线程的基本实现。
1.审计内核模块的实现
审计内核模块是审计系统在内核的主体程序,它主要负责响应用户态所发送的请求。比如,用户态的auditctl命令用于获取审计系统的状态,以及增加或删除审计规则,该命令对数据的获取和设置是与审计模块进行数据交互而实现的。
1.1.基本实现
1.1.1.audit_init()
审计在内核中的实现包括许多方面,不仅包括审计内核模块,还包括分布在进程、文件等子系统中的实现。审计内核模块是审计系统在内核中的主体框架,它不仅对审计系统中所需的数据结构进行了初始化,而且主要实现对用户态请求的处理。
审计模块的初始化函数为audit_init,它的主要过程如下:
1.首先通过netlink_kernel_create()创建netlink套接字audit_sock,并且注册了(针对用户态)数据的处理函数audit_receive,它将接收并处理用户态发送的netlink数据包;
2.初始化审计系统所需的链表和变量等内容,其中audit_skb_queue链表用于保存审计套接字缓冲区;
3.通过audit_log()生成本次初始化对应的审计日志;
4.初始化基于inode的审计规则所对应的哈希链表;
1.1.2.audit_receive()
audit_receive内部封装了audit_receive_skb函数,该函数的实现过程如下:
1.netlink套接字所发送的数据报文格式为nlmsghdr结构的包头+指定格式的净荷数据;因此,该函数首先按照协议取数据报头和整个数据报文长度,交由audit_receive_msg函数处理;
2.如果数据包处理完成,通过netlink_ack函数向用户态发送响应;
3.通过NLMSG_NEXT获取下一个数据报;
1.1.3.audit_receive_msg()
该函数实际上是一个消息分发处理器,根据数据报头部的nlmsg_type进行消息分发处理。具体的过程如下:
1.从netlink报文头部获取当前消息类型msg_type;
2.通过audit_netlink_ok函数检查当前的消息类型是否合法;如不合法,则退出;
3.检查kauditd内核线程是否已启动,如果没有启动则通过kthread_run()启动该线程;kauditd线程用于将内核中的审计日志发送到用户态中;
4.根据当前的套接字缓冲区skb和当前进程current获取审计所需的数据,比如pid、uid和loginuid等信息;
5.根据消息类型进行消息分发和处理;
1.2.日志的记录
审计系统在内核中通过audit_buffer结构表示一个审计日志缓冲区,它的结构如下:
struct audit_buffer { struct list_head list; struct sk_buff *skb; /* formatted skb ready to send */ struct audit_context *ctx; /* NULL or associated context */ gfp_t gfp_mask; };
list为链表节点;所有的审计日志缓冲区通过链表连接;
skb为套接字缓冲区;由于审计系统通过netlink机制实现用户态和内核态的数据交互,因此需求封装套接字缓冲区;
ctx表示审计上下文;当审计系统进行进程监控时,ctx将保存进程的相关信息;
gfp_mask是内核申请内存时的标志;表示为audit_buffer在哪片内存区申请内存;
内核通过一组接口对审计日志缓冲区进行操作。当审计事件发生时,通过audit_log_start()为该条日志分配audit_buffer结构的缓冲区;根据具体的审计事件,audit_log_vformat()将审计事件发生时要显示的数据以格式化字符串的形式写入缓冲区内;最终,audit_log_end()将缓冲区(审计缓冲区audit_buffer)加入缓冲区(审计套接字缓冲区sk_buffer)链表audit_skb_queue中。链表audit_skb_queue中的缓冲区由内核线程kauditd发送至用户态auditd进程。
1.2.1.audit_log_start()
该函数主要用于分配日志缓冲区,但是如果当前缓冲区链表中的太多,将阻塞当前进程。具体的步骤如下:
1.通过audit_filter_type()检查当前日志类型是否被指定为过滤,如果是,则直接返回;
2.如果当前缓冲区列表audit_skb_queue中存在的节点数目大于指定的audit_backlog_limit大小,则当前的进程将会被阻塞;即创建等待队列实例,改变当前进程的状态为TASK_UNINTERRUPTIBLE,并且将其加入到等待队列audit_backlog_wait中;此时,再次检查缓冲区列表中的数据是否大于所指定的audit_backlog_limit,如果是,则主动发起重新调度;否则,将删除刚才创建的等待队列实例,并且恢复进程的状态;
3.此时,通过audit_buffer_alloc()创建审计缓冲区,并且在该缓冲区中写入审计序列号和时间戳等信息;
当进程在该函数中被阻塞时,将由kauditd内核线程将其唤醒;由于当前进程被阻塞的理由是缓冲区未被发送的数据超过了限制,因此一旦kauditd发送了数据后,该进程将被唤醒。
1.2.2.audit_log_vformat()
审计数据不是一次全部产生,因此audit_log_vformat()可能被调用多次,在需要记录信息的地方调用该函数即可;所有的数据将以格式化字符串的形式进行记录;
1.2.3.audit_log_end()
该函数将结束本次审计日志的记录过程,具体的过程如下:
1.通过audit_rate_check()检查当前审计日志发送的频率是否超过了设定的限制;即如果每秒从内核向用户态发送的日志数超过了制定的数值audit_rate_limit,那么将抛弃当前的日志;
2.从当前审计日志缓冲区中提取所封装的套接字缓冲区,将其加入到audit_skb_queue链表中;
3..唤醒kauditd_wait等待队列中的一个进程。该等待队列由内核线程kauditd控制,该线程将依次发送audit_skb_queue链表中的缓冲区,如果已经无节点可发,则阻塞当前线程;此时,由于刚向audit_skb_queue中加入了新的节点,因此唤醒kauditd线程;
4.释放在audit_log_start()中申请的audit_buffer缓冲区;
1.3.审计规则的操作
audit_receive_msg函数接收并处理用户态auditctl命令发送的消息,这里针对审计规则的操作实现做简单说明。
审计规则的操作包括增加、删除和列举当前规则,这三种操作类型在内核中分别对应为AUDIT_ADD_RULE、AUDIT_DEL_RULE和AUDIT_LIST_RULES。这些操作在audit_receive_msg函数中均由audit_receive_filter函数进行具体处理,具体的过程如下:
1.对于增加和删除操作,先通过audit_log_common_recv_msg()针对本次审计规则的操作进行审计日志缓冲区的分配和部分字段的记录;
2.审计规则在内核中通过双链表进行组织,因此对审计规则的操作即是对链表的操作;
对于AUDIT_LIST_RULES操作,audit_receive_filter函数即分配缓冲区,依次遍历规则并拷贝至缓冲区,最终通过audit_send_list线程将缓冲区数据发送至内核态;具体的,netlink中从内核态发送数据至用户态通过netlink_unicast()函数完成;
对于AUDIT_ADD_RULES操作,audit_receive_filter()先通过audit_data_to_entry()将用户态发送的规则data转换成内核中使用的规则entry,其次将当前的规则添加到对应的规则链表中;由于审计系统的文件监控基于fsnotify机制完成,因此还需将规则中对应的文件加入到fsnotify监控列表中;
AUDIT_DEL_RULES操作的实现与增加是相反过程,在次不再赘述;
2.kauditd内核线程的实现
kauditd内核线程对应的执行函数为kauditd_thread(),它将审计套接字缓冲区链表audit_skb_queue中的节点发送至用户态,用户态auditd将这些日志写入审计文件(auditd.log)。该线程将循环发送缓冲区数据,当缓冲区链表有节点时就利用netlink套接字从内核发送数据到用户态,当该链表中为空时,该线程将进入等待状态,直到缓冲区有新的节点时候再被唤醒。kauditd每次具体的执行过程如下:
1.当用户态的auditd运行(内核中audit_pid标志),且审计功能随启动开启时(内核中audit_default标志),将不断将audit_skb_hold_queue链表中的节点发送至用户态;该链表中通常保存的是当auditd停止后或启动前所产生的审计日志,这些日志在auditd未正常运行时可能已经被发送至系统的syslog中,当auditd正常运行时则需要将它们保存在审计的日志文件中;
2.通过skb_dequeue()在audit_skb_queue链表中移除一个缓冲区节点;
3.唤醒audit_backlog_wait等待链表中的一个进程,表示此时可以继续生成日志了;
4.当在audit_skb_queue中移除节点成功时,将通过kauditd_send_skb()发送数据到用户态,或者(当auditd未启动时)直接printk到系统日志中;
5.当在audit_skb_queue中移除节点失败时,将创建一个等待实例,将其添加到kauditd_wait等待队列中;如果此时audit_skb_queue中确实没有节点了,那么将当前内核线程状态置为TASK_INTERRUPTIBLE,并且主动发起重新调度;否则,重新将当前进程状态恢复为TASK_RUNNING,并从等待队列中将其移除;
如果kauditd由于缓冲区链表中无数据而阻塞了,那么一旦审计系统又产生了新的套接字缓冲区时,它将会被唤醒。具体的唤醒动作在audit_log_end()中。
3.总结
审计内核模块和kauditd内核线程基本体现了审计在内核中的实现框架,前者负责处理用户态发送的命令请求,完成审计规则的操作任务;后者完成日志向用户态的合理传送。对于审计系统来说,用户更关心的是日志的产生。那么,审计日志是在内核哪个部分产生的?什么时机产生?这将涉及到审计系统中文件监控和进程监控的实现,请参考本系列下一篇文章。
参考资料:
1.Linux Audit :http://people.redhat.com/sgrubb/audit/
2.深入Linux内核架构:http://book.douban.com/subject/4843567/