作者:wzt
原文链接:https://mp.weixin.qq.com/s/-U71_2jMvboCj-y7CHDBAA

1 背景

1.1 freebsd audit 简介

Freebsd audit子系统是由TrustBSD项目从AppleXNU内核移植过来的,在freebsd6.2系统中发布。XNU内核中的audit子系统最初是由McAfee公司给apple设计的,它遵循的是solaris发明的BSM框架。

1.2 freebsd 与 linux audit 对比

同样作为audit审计功能,freebsd的设计理念跟linux的有所不同。

  • 1) 对于审计的对象, linux是每个系统调用,而freebsd定义的则是event事件,多个类似的event事件归属同一个class组。对于linux,在用户空间定义规则时就要指定某个具体的系统调用,freebsd则是指定的class组。

  • 2) linux提供了更精确的rule规则列表,只针对在某种特定条件下才记录日志,它有一个规则匹配引擎,而freebsd没有提供这项功能,只是纯粹的记录日志。比如两个系统都能监控socket系统调用,freebsd会把所有的socket调用都记录,而linux可以做到只记录第一个参数domainAF_INET,第二个参数typeSOCK_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) FreebsdMAC强制访问控制系统是不做审计的, 而linuxMAC甚至是secomp都做了审计操作。

  • 8) Freebsd的日志格式采用的是工业界的标准BSM(basic security model)linux采用的是自定义的格式。

2 实现

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是所有的系统调用都要被审计。我们可以看下freebsdinit进程的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_preselectclass 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获取到本次系统调用的日志内容。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1612/