前面几篇博客说过可以使用auditctl 添加和删除规则等。
现在谈谈auditd的相关实现。基于 Linux2.6.11.12
这些代码主要在下面的文件中
kernel/audit.c 提供了核心的审计机制。
kernel/auditsc.c 实现了系统调用审计、过滤审计事件的机制。
用户可以使用应用程序auditctl向内核添加规则,内核检测到这些变动之后,会在进程创建的时候创建对应的audit_context,在copy_process中调用audit_alloc创建struct audit_context实例。
/* 进程描述符*/
struct task_struct {
...
struct audit_context *audit_context; /* 指针类型, 如果没有开启审计规则,将不分配内存*/
...
};
/* 每个进程描述都一个这个对象. */
struct audit_context {
int in_syscall; /* 1 :当前进程是否在系统调用中 */
enum audit_state state; /* 当前audit的状态,通过这个标志进行过滤、audit的记录何时生成 */
unsigned int serial; /* audit记录的序列号,用于唯一标识一组记录 */
struct timespec ctime; /* 生成记录的时间 */
uid_t loginuid; /* 登录用户的uid */
int major; /* 系统调用号 */
unsigned long argv[4]; /* 系统调用参数 */
int return_valid; /* 系统调用返回值是否合法 */
int return_code;/* 系统调用的返回值 */
int auditable; /* 1 :是否需要审计 */
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
};
state:表示审计的活动级别。可能的状态由 audit_state定义,AUDIT_DISABLED(不记录系统调用)、AUDIT_SETUP_CONTEXT(在进程描述符创建的时候创建 audit_context,但是在系统调用时不需要填充信息)、AUDIT_BUILD_CONTEXT(创建audit_context,在系统调用进入时写入系统调用的相关信息)、
AUDIT_RECORD_CONTEXT(创建audit_context,在系统调用进入时写入系统调用相关信息,在系统调用退出时写审计记录)。
只有在系统调用审计曾经开启过,但是在后面停止过,AUDIT_DISABLED才有意义。如果没进行过审计,audit_context不会创建,当然也不需要这个状态。
struct audit_buffer {
struct list_head list;
struct sk_buff_head sklist; /* 已格式化的skbs, 准备发送 */
struct audit_context *ctx; /* NULL 或者相关联的审计上下文 */
...
};
list 是一个链表元素,用于将审计缓冲区存储在各种链表上。审计信息使用内核提供的netlink机制将数据发往用户态进程auditd。
由于审计缓冲区经常使用,内核会回收audit_buffer保存在audit_freelist链表中,并使用audit_freelist_count进行计数,供随时使用,如果还是不够调用kmalloc分配。
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];
};
在规则匹配时,可以进行两种操作(action表示)。AUDIT_NEVER表示什么都不做,AUDIT_ALWAYS生成审计记录。
如果启用了系统调用审计,mask通过位串制定了如何审计系统调用。
字段/值对(fields/values两个数组)用于指定审计规则使用的条件。字段中指定的量标识内核内部的某个对象。例如,
可能是一个进程ID,而值是指定可出发审计事件的字段值集合。比如,PID为xxx的进程打开一个文件,创建一个审计记录。
field_cout标识规则中包好的字段/值对的数目。fields数组项的可能的值在 audit.h中列出了。例如 #define AUDIT_PID 0
vaules数组只能指定数值。
struct audit_entry {
struct list_head list;
struct rcu_head rcu;
struct audit_rule rule;/* auditd 向内核发送的规则信息*/
};
在auditd守护进程向内核发送适当的请求时,将调用audit_add_rule添加新的规则。
审计子系统的初始化由audit_init执行。除了设置数据结构之后,该函数还创建了一个netlink套接字,
与用户层通信,如下:
int __init audit_init(void)
{
printk(KERN_INFO "audit: initializing netlink socket (%s)\n",
audit_default ? "enabled" : "disabled");
/*创建netlink套接字,用户态进程和内核交互,因为需要接收用户态数据,需要设置 audit_receive回调函数*/
audit_sock = netlink_kernel_create(NETLINK_AUDIT, audit_receive);
if (!audit_sock)
audit_panic("cannot initialize netlink socket");
audit_initialized = 1;
/* 判定auditd是否开启标志 */
audit_enabled = audit_default;
audit_log(NULL, "initialized");
return 0;
}
/* 处理auditd发送过来的数据*/
static void audit_receive(struct sock *sk, int length)
{
struct sk_buff *skb;
if (down_trylock(&audit_netlink_sem))
return;
/* FIXME: this must not cause starvation */
while ((skb = skb_dequeue(&sk->sk_receive_queue))) {
if (audit_receive_skb(skb) && skb->len)
skb_queue_head(&sk->sk_receive_queue, skb);
else
kfree_skb(skb);
}
up(&audit_netlink_sem);
}
static int audit_receive_skb(struct sk_buff *skb)
{
int err;
struct nlmsghdr *nlh;
u32 rlen;
while (skb->len >= NLMSG_SPACE(0)) {
...
/* 处理auditd 发过来的数据*/
if ((err = audit_receive_msg(skb, nlh))) {
netlink_ack(skb, nlh, err);
} else if (nlh->nlmsg_flags & NLM_F_ACK)/*如果发送的数据没有问题,回复ack */
netlink_ack(skb, nlh, 0);
...
}
return 0;
}
static int audit_receive_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
u32 uid, pid, seq;
void *data;
struct audit_status *status_get, status_set;
int err;
struct audit_buffer *ab;
u16 msg_type = nlh->nlmsg_type;
/* 首先验证发送者是否执行该请求。如果请求得到授权,继续处理 */
err = audit_netlink_ok(NETLINK_CB(skb).eff_cap, msg_type);
if (err)
return err;
pid = NETLINK_CREDS(skb)->pid;
uid = NETLINK_CREDS(skb)->uid;
seq = nlh->nlmsg_seq;
data = NLMSG_DATA(nlh);
switch (msg_type) {
...
case AUDIT_ADD:
case AUDIT_DEL:
if (nlh->nlmsg_len < sizeof(struct audit_rule))
return -EINVAL;
/* fallthrough */
case AUDIT_LIST:
#ifdef CONFIG_AUDITSYSCALL
/* 此处是处理审计规则的入口函数*/
err = audit_receive_filter(nlh->nlmsg_type, NETLINK_CB(skb).pid,
uid, seq, data);
#else
err = -EOPNOTSUPP;
#endif
break;
default:
err = -EINVAL;
break;
}
return err < 0 ? err : 0;
}
在所有准备工作都完成之后,现在开始分析审计是如何实现的,该过程分为三步,
首先,用audit_log_start开始记录过程。然后,用audit_log_format格式化一个记录消息,
最后用audit_log_end结束该审计记录,消息通过netlink发送到auditd。
audit_log将该过程封装了,提供给其他内核模块使用。
struct audit_buffer *audit_log_start(struct audit_context *ctx)
{
struct audit_buffer *ab = NULL;
unsigned long flags;
struct timespec t;
int serial = 0;
/* audit未初始化,直接返回 */
if (!audit_initialized)
return NULL;
/* 检查 backlog limit 和 rate limit 如果超出限制,写入一条报错日志到audit.log中 */
if (audit_backlog_limit
&& atomic_read(&audit_backlog) > audit_backlog_limit) {
if (audit_rate_check())
printk(KERN_WARNING
"audit: audit_backlog=%d > "
"audit_backlog_limit=%d\n",
atomic_read(&audit_backlog),
audit_backlog_limit);
audit_log_lost("backlog limit exceeded");
return NULL;
}
/* 检查audit_freelist链表,是否有未使用的audit_buffer,有则使用,更新统计计数,否则,创
建该实例,如果内存不够,往audit.log中写入内存不足的日志*/
spin_lock_irqsave(&audit_freelist_lock, flags);
if (!list_empty(&audit_freelist)) {
ab = list_entry(audit_freelist.next,
struct audit_buffer, list);
list_del(&ab->list);
--audit_freelist_count;
}
spin_unlock_irqrestore(&audit_freelist_lock, flags);
if (!ab)
ab = kmalloc(sizeof(*ab), GFP_ATOMIC);
if (!ab) {
audit_log_lost("out of memory in audit_log_start");
return NULL;
}
atomic_inc(&audit_backlog);
skb_queue_head_init(&ab->sklist);
/* 初始化 ab*/
ab->ctx = ctx;
ab->len = 0;
ab->nlh = NULL;
ab->total = 0;
ab->type = AUDIT_KERNEL;
ab->pid = 0;
ab->count = 0;
/* 获取当前时间戳和序列号 */
#ifdef CONFIG_AUDITSYSCALL
if (ab->ctx)
audit_get_stamp(ab->ctx, &t, &serial);
else
#endif
t = CURRENT_TIME;
/* 设置 audit log 记录头信息*/
audit_log_format(ab, "audit(%lu.%03lu:%u): ",
t.tv_sec, t.tv_nsec/1000000, serial);
return ab;
}
audit_log_format用于向一个给定的审计缓冲区(ab)写入一条记录消息。
在所有必要的记录消息都已写入到审计缓冲区之后,需要调用audit_log_end确保将审计记录发送到用户空间守护进程。
void audit_log_end(struct audit_buffer *ab)
{
...
audit_log_end_fast(ab);
}
void audit_log_end_fast(struct audit_buffer *ab)
{
unsigned long flags;
BUG_ON(in_irq());
if (!ab)
return;
if (!audit_rate_check()) {
audit_log_lost("rate limit exceeded");
} else {
audit_log_move(ab);
/* 通过netlink 发送到用户空间守护进程*/
if (audit_log_drain(ab))
return;
}
/* 发送成功之后对ab进行缓存, 减少内存分配,提升性能*/
atomic_dec(&audit_backlog);
spin_lock_irqsave(&audit_freelist_lock, flags);
if (++audit_freelist_count > AUDIT_MAXFREE)
kfree(ab);
else
list_add(&ab->list, &audit_freelist);
spin_unlock_irqrestore(&audit_freelist_lock, flags);
}
/* Iterate over the skbuff in the audit_buffer, sending their contents
* to user space. */
static inline int audit_log_drain(struct audit_buffer *ab)
{
struct sk_buff *skb;
while ((skb = skb_dequeue(&ab->sklist))) {
int retval = 0;
if (audit_pid) {
if (ab->nlh) {
ab->nlh->nlmsg_len = ab->total;
ab->nlh->nlmsg_type = ab->type;
ab->nlh->nlmsg_flags = 0;
ab->nlh->nlmsg_seq = 0;
ab->nlh->nlmsg_pid = ab->pid;
}
skb_get(skb); /* because netlink_* frees */
/* 发往 auditd*/
retval = netlink_unicast(audit_sock, skb, audit_pid,
MSG_DONTWAIT);
}
if (retval == -EAGAIN && ab->count < 5) {
++ab->count;
skb_queue_tail(&ab->sklist, skb);
audit_log_end_irq(ab);
return 1;
}
if (retval < 0) {
if (retval == -ECONNREFUSED) {
printk(KERN_ERR
"audit: *NO* daemon at audit_pid=%d\n",
audit_pid);
audit_pid = 0;
} else
audit_log_lost("netlink socket too busy");
}
if (!audit_pid) { /* No daemon */
int offset = ab->nlh ? NLMSG_SPACE(0) : 0;
int len = skb->len - offset;
printk(KERN_ERR "%*.*s\n",
len, len, skb->data + offset);
}
kfree_skb(skb);
ab->nlh = NULL;
}
return 0;
}
到现在为止,已经描述了系统调用审计所需的大部分数据和机制,接下来描述系统调用审计的实现。
系统调用审计不同于基本的审计机制,它依赖task_struct中扩展的审计上下文,前面已经描述过。
在系统调用进入和完成时会涉及审计子系统,进入时调用audit_syscall_entry,完成时调用audit_syscall_exit。
这需要底层、特定于体系结构中的中断处理代码的支持。该支持集成在do_syscall_trace中,每当中断发生或者完成时,底层的中断处理代码都会调用该函数。
/* notification of system call entry/exit
* - triggered by current->work.syscall_trace
*/
/**
* 被system_call调用,调试进程用于收集关于current的信息。
*/
__attribute__((regparm(3)))
void do_syscall_trace(struct pt_regs *regs, int entryexit)
{
if (unlikely(current->audit_context)) {
if (!entryexit)
audit_syscall_entry(current, regs->orig_eax,
regs->ebx, regs->ecx,
regs->edx, regs->esi);
else
audit_syscall_exit(current, regs->eax);
}
...
}
系统调用号、传递到系统调用的参数(由a1...a4表示),这些都保存在审计上下文中。如下
void audit_syscall_entry(struct task_struct *tsk, int major,
unsigned long a1, unsigned long a2,
unsigned long a3, unsigned long a4)
{
struct audit_context *context = tsk->audit_context;
enum audit_state state;
BUG_ON(!context);
/* This happens only on certain architectures that make system
* calls in kernel_thread via the entry.S interface, instead of
* with direct calls. (If you are porting to a new
* architecture, hitting this condition can indicate that you
* got the _exit/_leave calls backward in entry.S.)
*
* i386 no
* x86_64 no
* ppc64 yes (see arch/ppc64/kernel/misc.S)
*
* This also happens with vm86 emulation in a non-nested manner
* (entries without exits), so this case must be caught.
*/
if (context->in_syscall) {
struct audit_context *newctx;
#if defined(__NR_vm86) && defined(__NR_vm86old)
/* vm86 mode should only be entered once */
if (major == __NR_vm86 || major == __NR_vm86old)
return;
#endif
#if AUDIT_DEBUG
printk(KERN_ERR
"audit(:%d) pid=%d in syscall=%d;"
" entering syscall=%d\n",
context->serial, tsk->pid, context->major, major);
#endif
newctx = audit_alloc_context(context->state);
if (newctx) {
newctx->previous = context;
context = newctx;
tsk->audit_context = newctx;
} else {
/* If we can't alloc a new context, the best we
* can do is to leak memory (any pending putname
* will be lost). The only other alternative is
* to abandon auditing. */
audit_zero_context(context, context->state);
}
}
BUG_ON(context->in_syscall || context->name_count);
if (!audit_enabled)
return;
/* 系统调用号和参数保存在审计上下文中*/
context->major = major;
context->argv[0] = a1;
context->argv[1] = a2;
context->argv[2] = a3;
context->argv[3] = a4;
/* 根据进程的审计模式,需要使用audit_filter_syscall来进行过滤,
该函数将应用内中注册的所有适当的过滤器*/
state = context->state;
if (state == AUDIT_SETUP_CONTEXT || state == AUDIT_BUILD_CONTEXT)
state = audit_filter_syscall(tsk, context, &audit_entlist);
if (likely(state == AUDIT_DISABLED))
return;
/* 记录的序列号*/
context->serial = audit_serial();
context->ctime = CURRENT_TIME;
context->in_syscall = 1;
context->auditable = !!(state == AUDIT_RECORD_CONTEXT);
}
现在把注意力转向系统调用退出时如何处理审计。audit_syscall_exit对审计上下文创建了一条或者多条记录。
void audit_syscall_exit(struct task_struct *tsk, int return_code)
{
struct audit_context *context;
get_task_struct(tsk);
task_lock(tsk);
context = audit_get_context(tsk, 1, return_code);
task_unlock(tsk);
/* Not having a context here is ok, since the parent may have
* called __put_task_struct. */
if (likely(!context))
return;
/* 创建系统调用审计记录, 可能存在多条,但是共用同一个序列号和时间*/
if (context->in_syscall && context->auditable)
audit_log_exit(context);
/* 如果有嵌套系统调用,下面进行处理*/
context->in_syscall = 0;
context->auditable = 0;
if (context->previous) {
struct audit_context *new_context = context->previous;
context->previous = NULL;
audit_free_context(context);
tsk->audit_context = new_context;
} else {
audit_free_names(context);
audit_zero_context(context, context->state);
tsk->audit_context = context;
}
put_task_struct(tsk);
}
/* 创建审计记录*/
static void audit_log_exit(struct audit_context *context)
{
int i;
struct audit_buffer *ab;
ab = audit_log_start(context);
if (!ab)
return; /* audit_panic has been called */
/* 格式化系统调用号 、返回值是否符合法、系统调用参数*/
audit_log_format(ab, "syscall=%d", context->major);
if (context->personality != PER_LINUX)
audit_log_format(ab, " per=%lx", context->personality);
if (context->return_valid)
audit_log_format(ab, " exit=%d", context->return_code);
audit_log_format(ab,
" a0=%lx a1=%lx a2=%lx a3=%lx items=%d"
" pid=%d loginuid=%d uid=%d gid=%d"
" euid=%d suid=%d fsuid=%d"
" egid=%d sgid=%d fsgid=%d",
context->argv[0],
context->argv[1],
context->argv[2],
context->argv[3],
context->name_count,
context->pid,
context->loginuid,
context->uid,
context->gid,
context->euid, context->suid, context->fsuid,
context->egid, context->sgid, context->fsgid);
audit_log_end(ab);
/* 如果还有其他信息要审计,继续构造记录,比如 sys_open 就会有多条审计记录 */
for (i = 0; i < context->name_count; i++) {
ab = audit_log_start(context);
if (!ab)
continue; /* audit_panic has been called */
audit_log_format(ab, "item=%d", i);
if (context->names[i].name)
audit_log_format(ab, " name=%s",
context->names[i].name);
if (context->names[i].ino != (unsigned long)-1)
audit_log_format(ab, " inode=%lu",
context->names[i].ino);
/* FIXME: should use format_dev_t, but ab structure is
* opaque. */
if (context->names[i].rdev != -1)
audit_log_format(ab, " dev=%02x:%02x",
MAJOR(context->names[i].rdev),
MINOR(context->names[i].rdev));
audit_log_end(ab);
}
}
其他审计基本类似,这里就不描述了。