审计(audit)

1,550 阅读13分钟

简介

audit是macOS操作系统上一个安全审计功能,最初是从OpenBSM移植到OS X系统的。

由于审计是一个和安全密切的相关的操作,所以它是由内核在系统级别执行的,但是当出现安全性敏感的操作或情况时,用户态应用程序可以请求显示地记录日志,但在大多数情况下,它通过外部定义的审计策略记录用户和进程的操作。这些审计策略决定了哪些事件或情况值得系统关注。因此,系统管理员可以定义和实施审计策略,收集数据,进行安全分析。但是对于系统性能开销比较大,所以在iOS上是没有的。

注意:在macOS 14被弃用了,被EndpointSecurity替代,但是也可以通过一下命令重启audit

sudo launchctl enable system/com.apple.auditd && sudo reboot

从用户态的角度看审计

1.1 auditd守护进程

审计是OS X中自包含的一个子系统,在用户态主要的组件是auditd,它是由launchd根据需要启动的后台服务进程,这个后台服务进程不负责实际的审计日志记录,而是内核本身通过vnode直接写入日志的。但是这个后台服务进程可以控制内核组件,因此如果要控制审计,则需要控制auditd进程。管理员可以通过audit命令去控制auditd进程。

参数描述
-e删除过期的日志。(过期标准是啥??)
-i初始化审计
-n关闭当前审计文件,开启新的日志,并且删除过期的审计日志
-s指定审计系统应该从审计控制文件/etc/security/audit_control中同步其配置并创建一个新的日志文件。来自audit_control(5)配置flags参数是在登录时设置的,不与此标志同步。
-t终止审计

1.2 审计日志

1.2.1 日志文件格式

审计日志存放在/var/audit目录中,日志文件的命名格式是(起始时间戳.终止时间戳),时间精度是秒,由于日志是持续生成的,所以除非系统崩溃或重启,一个文件的stop_time就是下一个日志文件的start_time,最后一个日志的stop_time为not_terminated.可以通过一下命令查看日志:

sudo ls -l /var/audit

-r--r-----  1 root  wheel     2375  1 17 23:12 20240117150345.20240117151217
-r--r-----  1 root  wheel      196  1 17 23:22 20240117151217.not_terminated

1.2.2 查看日志

日志文件是以二进制格式保存,可以通过praudit命令进行解码。这个命令可以将日志输出位人类可读的格式,例如默认输出为CSV,还可以输出为XML格式。具体命令参数解释如下表

参数描述
-d ,指定分隔符。默认的分隔符是逗号。
-l在同一行打印整个记录。如果未指定此选项,则每个令牌将显示在不同的行上。
-n不要将用户和组id转换为其名称,而是保留其数字形式。
-p如果要从tail(1)实用程序通过管道输入praudit,请指定此选项。这将导致praudit同步到下一条记录的开始。
-r以原始形式打印记录。以数字形式(也称为原始形式)显示记录和事件类型。该选项与-s不兼容。
-s打印短格式的记录。以简短的文本形式显示记录和事件。该选项与-r不兼容。
-x将日志输出为XML格式

由于日志循环得太频繁了,所有还有一个特殊的字节设备/dev/auditpipe,用户态程序可以通过这个设备实时访问审计记录。praudit命令也可以直接操作这个设备查看实时日志。具体先执行以下命令

sudo praudit -s /dev/auditpipe

然后睡眠屏幕,然后在认证解锁系统,可以看到以下类似的输出

header,196,11,AUE_auth_user,0,Wed Jan 17 23:42:40 2024, + 305 msec
subject,wzf,wzf,staff,wzf,staff,506,100003,1238,0.0.0.0
text,Verify password for record type Users 'wzf' node '/Local/Default'
return,failure: Unknown error: 255,5000
identity,1,com.apple.opendirectoryd,complete,,complete,0xe7c3aea310e4e4c45cf6640ca9931e35805c68f3
trailer,196
...........

1.3 审计控制策略

由于审计操作都是在行为发生时执行的,所以对于性能的损耗比较严重,管理员可以通过控制审计策略来控制审计日志记录。这些策略都是放在/etc/security目录下的文件中。其文件作用描述如下表:

审计控制文件作用描述
audit_control设置审计策略以及日志相关的管理数据,其中flags值定义了审计的事件类别
audit_class定义了事件类别,比如文件、进程、网络等类别
audit_event定义了事件标识符映射为类别助记符和人类可读的名称,比如AUE_OPEN事件,表示仅仅访问了文件属性fa(文件属性访问)3:AUE_OPEN:open(2) - attr only:fa
audit_user提供了额外针对每个用户的审计策略,与audit_control中的审计策略组合使用
audit_warn对auditd后台服务程序产生的警告信息(例如:'audit space low (< 5% free) on audit log file-system')进行处理的shell脚本。这个脚本通常将消息传递给logger

几个文件详解如下:

1.3.1 audit_control

dir:/var/audit //审计日志存储目录
flags:lo,aa //指定为所有用户审计哪些事件类别,其中lo表示记录登录/注销事件,aa表示记录授权和拒绝访问的事件。
minfree:5//审计日志目录大小至少需要磁盘空间的百分之5%,当可用磁盘空间低于此值时将发出限制警告
naflags:lo,aa //定义这些事件类别不能仅仅用于某个特定的用户
policy:cnt,argv//指定各种行为的全局审计策略标志列表,例如失败停止、路径和参数审计等。
filesz:2M//设置单个审计日志文件的最大大小为2M。当达到此大小时,系统可能会轮转日志文件
expire-after:10M//设置审计日志文件的过期时间为10分钟。过期后的日志文件可能会被系统清理。如果不设置就不会过期

1.3.2 audit_class

audit_class文件包含系统上可审计事件类别的描述,每个可审计事件是一个类的成员,每行将一个审计事件掩码(位图)映射到一个类描述。内容的格式如下:

eventmask:eventclass:description

0x00000000:no:invalid class//无效的类别,通常不会使用
0x00000001:fr:file read//文件读取操作
0x00000002:fw:file write//文件写入操作
0x00000004:fa:file attribute access//访问文件属性
0x00000008:fm:file attribute modify//修改文件属性
0x00000010:fc:file create//创建文件
0x00000020:fd:file delete//删除文件
0x00000040:cl:file close//关闭文件
0x00000080:pc:process//进程操作
0x00000100:nt:network// 网络操作
0x00000200:ip:ipc//进程间通信
0x00000400:na:non attributable
0x00000800:ad:administrative
0x00001000:lo:login_logout//登录和注销操作
0x00002000:aa:authentication and authorization//认证和授权操作
0x00004000:ap:application//应用程序操作
0x10000000:res://保留供内部使用
0x20000000:io:ioctl//输入/输出控制操作
0x40000000:ex:exec 
0x80000000:ot:miscellaneous//其他杂项操作
0xffffffff:all:all flags set

1.3.3 audit_event

audit_event文件包含系统中可审计事件的描述。每行将审计事件号映射到名称、描述和类,格式如下:

eventnum:eventname:description:eventclass

1:AUE_EXIT:exit(2):pc//进程AUE_EXIT事件
2:AUE_FORK:fork(2):pc//进程AUE_FORK事件
3:AUE_OPEN:open(2) - attr only:fa//访问文件属性对应的AUE_OPEN事件
4:AUE_CREAT:creat(2):fc//文件创建事件
5:AUE_LINK:link(2):fc//文件link事件
6:AUE_UNLINK:unlink(2):fd//文件删除类别中的unlink事件
.....
 

1.3.4 audit_user

管理员可以针对不同的用户,设定不同的审计级别,内容格式

username:alwaysaudit:neveraudit

root:lo:no //root用户执行登录和注销操作审计,不执行no(无效的类别,通常不会使用)

1.3.5 audit_warn

管理员可以设定当审计警告出现时运行的脚本程序。

argument=""
willsleep=0
type=$1
shift

while [ $# -ge 1 ]; do
	case $1 in
	--will-sleep) willsleep=1 ;;
	*) argument=$1 ;;
	esac
	shift
done

# Don't log audit warning events when the system is about to sleep.
if [ $willsleep -eq 0 ]; then
	logger -p security.warning "audit warning: $type $argument"
fi

从内核态看审计

从内核的角度看,审计只不过是在系统调用的逻辑中穿插了一些宏的调用过程,下载darwin-xnu源码发现

如/bsd/dev/arm/systemcalls.c文件代码所示:

void unix_syscall(struct arm_saved_state * state,__unused thread_t thread_act,
	struct uthread * uthread,struct proc * proc)
{
	....
	AUDIT_SYSCALL_ENTER(code, proc, uthread);
	error = (*(callp->sy_call))(proc, &uthread->uu_arg[0], &(uthread->uu_rval[0]));
	AUDIT_SYSCALL_EXIT(code, proc, uthread, error);
  ...
}
 

在这段代码中调用了宏AUDIT_SYSCALL_ENTER和AUDIT_SYSCALL_EXIT,这些宏定义在/bsd/security/audit/audit.h文件中,当用的时候会调用AUDIT_ENABLED宏检查全局变量audit_enabled的值,以免在禁用审计的情况下产生任何开销。管理员可以通过 auditon系统调用(指定 A_SETCOND 命令)的方式修改这个变量的值。

#define AUDIT_SYSCALL_ENTER(args...)    do {                            \
	if (AUDIT_ENABLED()) {                                  \
	    audit_syscall_enter(args);                              \
	}                                                               \
} while (0)

/*
 * Wrap the audit_syscall_exit() function so that it is called only when
 * we have a audit record on the thread.  Audit records can persist after
 * auditing is disabled, so we don't just check audit_enabled here.
 */
#define AUDIT_SYSCALL_EXIT(code, proc, uthread, error)  do {            \
	  if (AUDIT_AUDITING(uthread->uu_ar))                     \
	        audit_syscall_exit(code, error, proc, uthread); \
} while (0)
  • AUDIT_SYSCALL_ENTER:调用 sysent 表中的一条 UNIX 系统调用之前调用这个宏。这个宏接受3个参数:系统调用代码(编号)、BSD 进程以及负责这个调用的线程对象。
  • AUDIT_ARG:在系统调用的实现内部调用。这个宏接受一个表示操作的参数,以及其他可变参数,其他参数具体取决于对应的系统调用。
  • AUDIT_SYSCALL_EXIT:在系统调用的实现之后立即被调用。参数和 ENTER 的参数一致,还接受一个系统调用的返回值

还有一些宏专门用于 Mach 陷阱的审计,但是仅限于 BSD 调用导致 Mach 调用的情况,而且只有部分Mach 陷阱才支持。

如果确实启用了审计,那么这些宏要么创建一个新的 kaudit_record(最终调用 audit_new),要么使用一条已有的审计记录(如果在 BSD 线程的 uu_ar 字段中能找到的话。在audit_syscall_exit函数中通过调用 audit_commit 将审计记录最终确定下来,然后 audit_commit 会将这条审计记录转移到一个 audit_q 队列中。一旦这条记录进入了队列,线程的 uu_ar 字段就会被重置。除了将记录放在 audit_q 中之外,audit_commit 还会向一个条件变量 audit_worker_cv 发信号。

void
audit_commit(struct kaudit_record *ar, int error, int retval)
{
  ...
  TAILQ_INSERT_TAIL(&audit_q, ar, k_q);
	...
	cv_signal(&audit_worker_cv);
}

这样可以唤醒一个专用的审计工作线程,这个线程会调用 audit_worker_process_record 对审计记录进行处理,而这个函数会调用 kaudit_to_bsm,将审计记录转换为 OpenBSM 兼容的格式。这种记录可以直接写入(通过内核)审计文件,提交到审计管道,而且从 Lion 开始,还可以写入审计会话设备(通过 audit_session.c 文件中定义的audit_sdev_submit 函数.然后这条记录被释放,如下方示例代码所示:

/*
* 给定一条内核审计记录,根据要求对其进行处理。根据是否还存在一条用户审计记录,
* 内核审计记录被转换为一条或两条BSM记录。内核记录必须被转换为BSD之后才能被写出
*到其他地方。两种类型都会被写入磁盘和审计管道
*/
static void audit _worker_process_record(struct kaudit_record *ar)
{
  //转为OpenBSM兼容的格式
  error = kaudit_to_bsm(ar, &bsm);
  switch (error){
    ///基本所有的错误都会跳到out
  }
 
  //直接写入文件。audit_vp 是审计文件的vnode
  if (ar->k_ar_commit & AR_PRESELECT_TRAIL) {
		AUDIT_WORKER_SX_ASSERT();   
		audit_record_write(audit_vp, &audit_ctx, bsm->data, bsm->len);
	}
  
  //发送到任何/dev/auditpipe 实例
  if (ar->k_ar_commit & AR_PRESELECT_PIPE) {
		audit_pipe_submit(auid, event, class, sorf,
		    ar->k_ar_commit & AR_PRESELECT_TRAIL, bsm->data,
		    bsm->len);
	}

  //发送到任何/dev/auditsessions 设备实例(Lion 新引入)
  if (ar->k_ar_commit & AR_PRESELECT_FILTER) {
		/*
		 *  XXXss - 需要一般化,以便可以方便地插入新的过滤器
		 */
		audit_sdev_submit(auid, ar->k_ar.ar_subj_asid, bsm->data,
		    bsm->len);
	} 
  kau_free(bsm) ;
out:
  if (trail_locked) {
		AUDIT_WORKER_SX_XUNLOCK();
	}
}

audit_vp是内核代码直接写入文件的有趣实例,不需要用户态的干预。这是一个必要的捷径,因为审计具有安全敏感的本质。

在用户态如何使用审计

在用户态我们可以通过ioctl函数控制/dev/auditpipe管道的行为,然后通过au_read_rec读取管道里面的审计纪录进行安全分析。

  • 获取/dev/auditpipe文件描述符
  • 通过ioctl控制管道行为
  • 读取分析审计日志

打印进程类别事件示例代码如下:

//
//  main.m
//  auditDemo
//
//  Created by xxx on 2024/1/18.
//

#import <Foundation/Foundation.h>
#import <bsm/libbsm.h>
#import <sys/ioctl.h>
#import <security/audit/audit_ioctl.h>

//配置audit pipe
int configAuditPipe(FILE *auditFile) {
    if (auditFile == NULL) {
        return -1;
    }
    int auditFileDescriptor = fileno(auditFile);
    //1.设置审计策略flags
    u_int auditFlags = 0x00000080;
    int ioctlResult;
    ioctlResult = ioctl(auditFileDescriptor,AUDITPIPE_SET_PRESELECT_FLAGS, &auditFlags);
    if (ioctlResult == -1) {
        return -1;
    }    
    //2.设置审计操作模式
    int auditModel = AUDITPIPE_PRESELECT_MODE_LOCAL;
    ioctlResult = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_MODE,&auditModel);
    
    //3.设置审计管道队列限制
    int max_queue_count;
    ioctlResult = ioctl(auditFileDescriptor, AUDITPIPE_GET_QLIMIT_MAX,&max_queue_count);
    if (ioctlResult == -1) {
        return -1;
    }
    NSLog(@"max_queue_count:%d",max_queue_count);
    ioctlResult = ioctl(auditFileDescriptor, AUDITPIPE_SET_QLIMIT,&max_queue_count);
    return 0;
}
int readPipe(FILE *auditFile) {
    if (auditFile == NULL) {
        return -1;
    }
    //1.读取审计纪录
    u_char *buffer = NULL;
    int recordLen = 0;
    int bytesread = 0;
    tokenstr_t token = {0};
    while ((recordLen = au_read_rec(auditFile, &buffer)) != -1) {
        bytesread = 0;
        while (bytesread < recordLen) {
            if (-1 == au_fetch_tok(&token, buffer + bytesread, recordLen - bytesread)){
                break;
            }
            bytesread += token.len;
             //2.打印审计日志
            au_print_tok_xml(stdout, &token, ",", 0, 0);
            printf("\n");
        }
        free(buffer);
        fflush(stdout);
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    
        //1.获取审计管道文件句柄
        char *auditPipe = "/dev/auditpipe";
        FILE *auditFile = fopen(auditPipe, "r");
        if (auditFile != NULL) {
            //2.配置审计管道
            int result = configAuditPipe(auditFile);
            if (result == -1) {
                return -1;
            }
           
            //3.读取管道审计日记
            readPipe(auditFile);
             
        }
    }
    CFRunLoopRun();
    return 0;
}

代码输出如下:

max_queue_count:1024
<record version="11" event="pid_for_task()" modifier="0" time="Thu Jan 18 22:28:30 2024" msec=" + 723 msec" >
<argument arg-num="1" value="0x203" desc="port" />
<argument arg-num="2" value="0xefd9" desc="pid" />
<subject audit-uid="wzf" uid="wzf" gid="staff" ruid="wzf" rgid="staff" pid="61401" sid="100003" tid="50331650 0.0.0.0" />
<return errval="success" retval="0" />
</record>
<record version="11" event="pid_for_task()" modifier="0" time="Thu Jan 18 22:28:30 2024" msec=" + 723 msec" >
<argument arg-num="1" value="0x203" desc="port" />
<argument arg-num="2" value="0xefd9" desc="pid" />
<subject audit-uid="wzf" uid="wzf" gid="staff" ruid="wzf" rgid="staff" pid="61401" sid="100003" tid="50331650 0.0.0.0" />
<return errval="success" retval="0" />
</record>
<record version="11" event="pid_for_task()" modifier="0" time="Thu Jan 18 22:28:30 2024" msec=" + 723 msec" >
<argument arg-num="1" value="0x203" desc="port" />
<argument arg-num="2" value="0xefd9" desc="pid" />
<subject audit-uid="wzf" uid="wzf" gid="staff" ruid="wzf" rgid="staff" pid="61401" sid="100003" tid="50331650 0.0.0.0" />
<return errval="success" retval="0" />
</record>