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_bufer保存在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);
	}
}

其他審計基本類似,這裏就不描述了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章