Freebsd audit子系统是由TrustBSD项目从Apple的XNU内核移植过来的,在freebsd6.2系统中发布。XNU内核中的audit子系统最初是由McAfee公司给apple设计的,它遵循的是solaris发明的BSM框架。
同样作为audit审计功能,freebsd的设计理念跟linux的有所不同。
1) 对于审计的对象, linux是每个系统调用,而freebsd定义的则是event事件,多个类似的event事件归属同一个class组。对于linux,在用户空间定义规则时就要指定某个具体的系统调用,freebsd则是指定的class组。
2) linux提供了更精确的rule规则列表,只针对在某种特定条件下才记录日志,它有一个规则匹配引擎,而freebsd没有提供这项功能,只是纯粹的记录日志。比如两个系统都能监控socket系统调用,freebsd会把所有的socket调用都记录,而linux可以做到只记录第一个参数domain为AF_INET,第二个参数type为SOCK_STREAM,第三个参数为IPPROTO_TCP的某次socket调用。当然linux的规则引擎也不完备,不能处理指针和结构体。规则数目较多时,系统会感到明显的卡顿,在敏感的系统调用路径里,规则审计应该做到越快越好,从这一点上来说,freebsd的做法似乎更纯粹一些。
3) 对于与用户层的通讯接口,linux使用的是netlink socket,而freebsd则是增加了若干系统调用以及/dev/audit和/dev/audit_pipe来做通讯。
4) 对于审计的入口,freebsd只在系统调用入口处处理,而linux还可以从进程fork以及文件系统等路径进行处理。
5) Freebsd没有对全部的系统调用进行审计,而linux则是全部都要审计。
6) 对于系统调用参数的记录是比较困难的, 因为不同的系统调用参数个数不同,每个参数的类型也不同,类型还可能包括指针和数据结构嵌套, 目前业界没有一个较好的算法能捕获这些参数。所以freebsd的做法是在内核大部分模块中都加入了hook,才可以保证系统调用参数的获取,而linux对这种支持很少。
7) Freebsd对MAC强制访问控制系统是不做审计的, 而linux对MAC甚至是secomp都做了审计操作。
8) Freebsd的日志格式采用的是工业界的标准BSM(basic security model),linux采用的是自定义的格式。
2.1 与用户层通讯接口
Freebsd增加了以下几个系统调用,用于从用户层与内核层的通讯,这些系统调用包括audit功能开启,参数配置等等。
sys_audit// 向内核传递用户层自定义的日志内容
sys_auditon// 用于参数和规则控制
sys_getauid// 获取audit session id
sys_setauid// 设置audit session id
sys_getaudit// 获取audit状态信息
sys_setaudit// 设置audit状态信息
sys_getaudit_addr// 获取audit状态信息, 包含一些额外信息
sys_setaudit_addr// 设置audit状态信息, 包含一些额外信息
sys_auditctl// 建立一个新的audit日志文件
这几个系统调用的实现逻辑都比较简单,笔者不在本文进行讲解,读者朋友可以自己尝试阅读下源码。
2.2 审计实现
我们同样以x86体系为例,看下freebsd audit子系统的入口是如何进入的。
amd64/amd64/exception.S:
IDTVEC(fast_syscall)
call amd64_syscall
amd64/amd64/trap.c:
amd64_syscall()->syscallenter
syscallenter(struct thread *td)
{
AUDIT_SYSCALL_ENTER(sa->code, td);[1]
error = (sa->callp->sy_call)(td, sa->args);[2]
AUDIT_SYSCALL_EXIT(error, td);[3]
}
在执行具体的系统调用[2]之前,需要在[1] 处执行审计的预处理:
security/audit/audit.c:
void
audit_syscall_enter(unsigned short code, struct thread *td)
{
event = td->td_proc->p_sysent->sv_table[code].sy_auevent;[4]
auid = td->td_ucred->cr_audit.ai_auid;[5]
if (auid == AU_DEFAUDITID)
aumask = &audit_nae_mask;
else
aumask = &td->td_ucred->cr_audit.ai_mask;
class = au_event_class(event);[6]
if (au_preselect(event, class, aumask, AU_PRS_BOTH)) {[7]
record_needed = 1;
} else if (audit_pipe_preselect(auid, event, class, AU_PRS_BOTH, 0)) {[8]
record_needed = 1;
} else {
record_needed = 0;
}
if (record_needed) {
td->td_ar = audit_new(event, td);[9]
}
freebsd在每个进程结构体里都保存一个系统调用数组指针struct sysentvec,它包含一个成员struct sysent:
struct sysent { /* system call table */
int sy_narg; /* number of arguments */
sy_call_t *sy_call; /* implementing function */
au_event_t sy_auevent; /* audit event associated with syscall */
systrace_args_func_t sy_systrace_args_func;
/* optional argument conversion function. */
u_int32_t sy_entry; /* DTrace entry ID for systrace. */
u_int32_t sy_return; /* DTrace return ID for systrace. */
u_int32_t sy_flags; /* General flags for system calls. */
u_int32_t sy_thrcnt;
};
Sy_call保存的是具体的系统调用函数指针。
前面讲过freebsd audit是基于event事件来驱动的,sy_auevent保存的就是event事件号。每个系统调用只有一个或没有event事件。如果没有event事件,那么在audit审计的时候就会被忽略。这一点与linux不同, linux是所有的系统调用都要被审计。我们可以看下freebsd的init进程的struct sysent的初始化表:
kern/init_sysent.c:
struct sysent sysent[] = {
{ 0, (sy_call_t *)nosys, AUE_NULL, NULL, 0, 0, 0, SY_THR_STATIC },
{ AS(sys_exit_args), (sy_call_t *)sys_sys_exit, AUE_EXIT, NULL, 0, 0, SYF_CAPENABLED, SY_THR_STATIC },
{ 0, (sy_call_t *)sys_fork, AUE_FORK, NULL, 0, 0, SYF_CAPENABLED, SY_THR_STATIC },
{ AS(break_args), (sy_call_t *)sys_break, AUE_NULL, NULL, 0, 0, SYF_CAPENABLED, SY_THR_STATIC },
{ compat(AS(ogetkerninfo_args),getkerninfo), AUE_NULL, NULL, 0, 0, 0, SY_THR_STATIC }, /* 63 = old getkerninfo */
{ compat(0,getpagesize), AUE_NULL, NULL, 0, 0, SYF_CAPENABLED, SY_THR_STATIC }, /* 64 = old getpagesize */
}
这里还是有很多空event事件的,那么这些系统调用都不会被audit审计到。Freebsd内核开发者应该是认为某些系统调用没有危险性,所以暂时不需要被审计到。
在[5]处获取当前的会话session id,来判断是否使用内核的class mask还是进程的class mask。[6]处开始将event事件号,转化为对应的class组,前面提到freebsd将类似的event事件归并入一个class组。Event事件和class组是通过哈希表来管理的, audit子系统在初始化的时候把上述init进程的sysent数组中event号进行提取,然后归档到哈希表中。后续应用进程也可以通过auditon来进行动态添加。[7]处的au_preselect对class mark进行匹配,来判断是否需要进行本地审计。[9]处是否需要使用/dev/audit_pipe来与用户层进行实时交互。[9]处如果需要记录就通过audit_new动态分配一个struct kaudit_record数据结构。Linux的audit数据结构是在进程fork时就提前生成,笔者认为这样做的效率会高些。
当[2]处具体的系统调用执行完毕后, 在[3]处开始做记录日志操作。
void
audit_syscall_exit(int error, struct thread *td)
{
audit_commit(td->td_ar, error, retval);
}
void
audit_commit(struct kaudit_record *ar, int error, int retval)
{
while (audit_q_len >= audit_qctrl.aq_hiwater)
cv_wait(&audit_watermark_cv, &audit_mtx);
TAILQ_INSERT_TAIL(&audit_q, ar, k_q);
audit_q_len++;
audit_pre_q_len--;
cv_signal(&audit_worker_cv);
}
与linux不同, feebsd的系统调用日志记录操作逻辑很清晰简单,因为没有linux的规则匹配引擎。Linux在进入系统调用之前只有一些简单的初始化操作,真正的判断是在系统调用返回时通过规则引擎来识别的,这是它们的不同之处。
Freebsd是在进入系统调用之前就已经预判此次系统调用是否需要被审计,后续的audit_commit只管往日志队列里写数据,当队列长度超过高水位线时就进行休眠,否则将一个节点插入到队列里,并唤醒等待的audit worker进程。
Audit worker进程是在audit子系统初始化被建立的:
static void
audit_worker(void *arg)
{
struct kaudit_queue ar_worklist;
struct kaudit_record *ar;
int lowater_signal;
TAILQ_INIT(&ar_worklist);[1]
while (1) {
mtx_assert(&audit_mtx, MA_OWNED);
while (TAILQ_EMPTY(&audit_q))[2]
cv_wait(&audit_worker_cv, &audit_mtx);
lowater_signal = 0;
while ((ar = TAILQ_FIRST(&audit_q))) {[3]
TAILQ_REMOVE(&audit_q, ar, k_q);
audit_q_len--;
if (audit_q_len == audit_qctrl.aq_lowater)
lowater_signal++;
TAILQ_INSERT_TAIL(&ar_worklist, ar, k_q);
}
if (lowater_signal)[4]
cv_broadcast(&audit_watermark_cv);
mtx_unlock(&audit_mtx);
while ((ar = TAILQ_FIRST(&ar_worklist))) {[5]
TAILQ_REMOVE(&ar_worklist, ar, k_q);
audit_worker_process_record(ar);[6]
audit_free(ar);
}
mtx_lock(&audit_mtx);
}
}
[1]处初始化一个临时的日志队列,[2]处判断audit日志队列是否为空,为空时就进入休眠状态,当再次被唤醒后,如果audit日志队列不为空,就将节点一个个取下来插入到临时队列里,同时判断audit日志队列长度在低水位线时,就要在[4]处通知audit_commit进行日志的补充。Linux的这部分操作没有使用临时队列,而是在持有锁的情况下进行队列节点的处理,而freebsd则是将节点插入临时队列后,马上释放锁,这样做做效率会更高些。
[6]处的audit_worker_process_record首先将日志转化为BSM格式后,通过调用audit_record_write将日志写入到磁盘文件里,然后调用audit_send_trigger,将日志信息同步到一个队列里, 这个队列是由/dev/audit进行操作,这样用户态程序可以通过读取/dev/audit获取到本次系统调用的日志内容。