linux内核从2.6.6版本开始支持audit机制,为了更好的理解audit本身的机制,需要对audit的内核代码进行分析。
内核中,audit相关的代码主要有三个文件:
从audit的功能来说,包含三个部分:
audit的初始化包括两个函数:
对于audit_init()函数,根据CONFIG_NET宏还定义了不同的实现,如果定义了CONFIG_NET,就会创建aduit的netlink的socket,否则,就不会创建socket。
当前的audit版本只支持系统调用的审计,因此,主要查看系统调用的审计流程。
系统调用的入口位于arch/x86_64/kernel/entry.S,中间的x86_64随实际的架构有所变化。
ENTRY(system_call)
CFI_STARTPROC
swapgs
movq %rsp,%gs:pda_oldrsp
movq %gs:pda_kernelstack,%rsp
sti
SAVE_ARGS 8,1
movq %rax,ORIG_RAX-ARGOFFSET(%rsp)
movq %rcx,RIP-ARGOFFSET(%rsp)
GET_THREAD_INFO(%rcx)
testl $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),threadinfo_flags(%rcx)
jnz tracesys
cmpq $__NR_syscall_max,%rax
ja badsys
movq %r10,%rcx
call *sys_call_table(,%rax,8) # XXX: rip relative
movq %rax,RAX-ARGOFFSET(%rsp)
上面是系统调用的入口,开始会处理一些参数,中间就会判断是否启用trace或者audit,如果启用,就会调用tracesys:
tracesys:
SAVE_REST
movq $-ENOSYS,RAX(%rsp)
FIXUP_TOP_OF_STACK %rdi
movq %rsp,%rdi
call syscall_trace_enter
LOAD_ARGS ARGOFFSET /* reload args from stack in case ptrace changed it */
RESTORE_REST
cmpq $__NR_syscall_max,%rax
ja 1f
movq %r10,%rcx /* fixup for C */
call *sys_call_table(,%rax,8)
movq %rax,RAX-ARGOFFSET(%rsp)
1: SAVE_REST
movq %rsp,%rdi
call syscall_trace_leave
RESTORE_TOP_OF_STACK %rbx
RESTORE_REST
jmp ret_from_sys_call
这里就进行系统调用的审计,里面有三个call,中间的call就是调用系统调用,第一个call是系统调用入口的审计,第三个call是系统调用出口的审计。
// arch/x86_64/kernel/ptrace.c
asmlinkage void syscall_trace_enter(struct pt_regs *regs)
{
if (unlikely(current->audit_context))
audit_syscall_entry(current, regs->orig_rax,
regs->rdi, regs->rsi,
regs->rdx, regs->r10);
if (test_thread_flag(TIF_SYSCALL_TRACE)
&& (current->ptrace & PT_PTRACED))
syscall_trace(regs);
}
这里的unlikely可以进行编译优化:Linux内核入门-- likely和unlikely。
后续调用流程如下:
-F
的过滤选项,过滤选项可以对产生日志的进程的字段进行过滤,该函数的返回值表明过滤选项是否匹配上,同时,会根据action设置state系统调用结束时的调用流程:
用进程的信息填充context
,例如,填充pid:context->pid = tsk->pid
audit: audit_backlog=XX > audit_backlog_limit=XX
的打印atomic_inc(&audit_lost)
,如果rate_limit为0,则会在内核日志中打印audit: audit_lost=XX audit_backlog=XX audit_rate_limit=XX audit_backlog_limit=XX
atomic_inc(&audit_backlog)
audit(second.milisecond:serial)
写入刚才分配的audit_buffer系统调用过程中访问的文件和设备信息
处于硬件中断上下文
时调用,将audit_buffer放到audit_txlist队列
不处于硬件中断上下文
时调用,直接将audit_buffer中的数据发送到用户空间
netlink_unicast
将数据发送出去,如果多次发送失败,且错误码是EAGAIN,就会调用audit_log_end_irq将audit_buffer放到audit_txlist队列,其他错误则会打印错误日志;如果发送数据的返回值是ECONNREFUSED,则在内核日中打印audit: *NO* daemon at audit_pid=%d
,并将audit_pid设置为0atomic_dec(&audit_backlog)
从整个实现的流程看,主要依赖audit_context进行数据传递,在真正要执行系统调用前,创建audit_context,将参数保存到audit_context,并生成audit日志的audit(second.milisecond:serial)
部分(保证单次系统调用生成的多条审计日志的这部分内容是一样的),将audit_context保存到进程的task_struct,在系统调用结束后,获取进程的audit_context,用进程的信息填充audit_context,再将数据写入到audit_buffer缓存中,最后要么直接通过netlink_unicast发送给用户态进程,要么将数据放到audit_txlist队列。
audit_log_exit()会将audit_context中的数据输出到审计日志:
struct audit_names {
const char *name;
unsigned long ino;
dev_t rdev;
};
struct audit_context {
int in_syscall; /* 标记是否正在执行系统调用 */
enum audit_state state;
unsigned int serial; /* 审计日志的序列号 */
struct timespec ctime; /* 系统调用入口的时间 */
uid_t loginuid; /* login uid (identity) */
int major; /* 系统调用号 */
unsigned long argv[4]; /* 系统调用参数 */
int return_valid; /* 返回值是否有效 */
int return_code;/* 系统调用返回值 */
int auditable; /* 标记是否需要写入审计日志 */
int name_count;
struct audit_names names[AUDIT_NAMES];
struct audit_context *previous; /* 系统调用嵌套 */
/* 写入审计日志的字段 */
pid_t pid;
uid_t uid, euid, suid, fsuid;
gid_t gid, egid, sgid, fsgid;
unsigned long personality;
#if AUDIT_DEBUG
int put_count;
int ino_count;
#endif
};
在打印日志时,会先输出一条日志,里面会输出pid到personality这些字段,以及上面的liginuid、major、argv、return_code等字段,然后就会打印names这个数组中的字段,这个里面是什么呢?看字面意思就是名字,是什么的名字呢?而且里面还有name、inode、rdev这些字段。其实,这里保存的就是系统调用执行过程中访问的文件或者目录的信息。
有两个地方会向names中加入元素:
names中的元素打印的格式类似于item=0 name=fname inode=11111 dev=xx:xx
,因此,这里打印的就是系统调用过程中查找或者访问过的目录或者文件。
上面是系统调用过程中,根据给定的规则然后输出审计日志,那么,审计规则是怎么来的呢?当用户在机器上执行auditctl -l
或者auditctl -S
进行审计规则的读取和设置时,依然是通过NETLINK_AUDIT的netlink socket进行通信的。audit自身的一些配置也是采用同样的流程。
当用户态程序需要增加规则时,通常会使用libaudit的audit_add_rule_data():
int audit_add_rule_data(int fd, struct audit_rule_data *rule,
int flags, int action) {
int rc;
if (flags == AUDIT_FILTER_ENTRY) {
audit_msg(LOG_WARNING, "Use of entry filter is deprecated");
return -2;
}
rule->flags = flags;
rule->action = action;
rc = audit_send(fd, AUDIT_ADD_RULE, rule,
sizeof (struct audit_rule_data) +rule->buflen);
if (rc < 0)
audit_msg(audit_priority(errno),
"Error sending add rule data request (%s)",
errno == EEXIST ?
"Rule exists" : strerror(-rc));
return rc;
}
这里的audit_send就是调用sendto系统调用向内核发送规则数据,调用sendto时,addr.nl_family设置为AF_NETLINK,下一步就会进入到内核的sys_sendto系统调用。
sys_sendto的调用流程如下:
sk_data_ready是在创建内核netlink socket时设置的,通过netlink_data_ready最终调用到audit_init中的audit_receive:在audit_init初始化函数中,在创建netlink socket时会指定一个回调函数用于处理收到的数据:audit_sock = netlink_kernel_create(NETLINK_AUDIT, audit_receive)
。
audit_receive的调用链:
在audit_receive_msg中会根据收到的消息的类型执行不同的业务逻辑:
AUDIT_GET:用户态程序查询audit的状态,对应auditctl -s
命令
AUDIT_SET:用户态程序设置audit的配置,对应auditctl的设置命令,例如auditctl -b
设置backlog_limit,auditctl -e
设置enabled,auditctl --reset-lost
重置lost
AUDIT_USER:接收用户态发送的数据,直接写入到审计日志,对应auditctl -m
命令
AUDIT_LOGIN:用户和注销时的审计日志
AUDIT_LIST:列出系统调用监控规则,对应auditctl -l
命令
AUDIT_ADD:增加系统调用监控规则,对应auditctl -a
命令
AUDIT_DEL:删除系统调用监控规则,对应auditctl -d
命令
audit_receive_msg:
所以,规则的操作也就是操作audit_tsklist、audit_entlist、audit_extlist三个链表,这三个链表的含义分别是:task、entry、exit。
audit_tsklist的调用链:
audit_entlist的调用链:
audit_extlist的调用链:
从整个流程来说需要了解以下内容: