前面幾篇博客說過可以使用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);
}
}
其他審計基本類似,這裏就不描述了。