linux 审计(auditd)原理分析

前面几篇博客说过可以使用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);
	}
}

其他审计基本类似,这里就不描述了。

你可能感兴趣的:(linux,audit,linux内核,安全,linux,运维,服务器)