简介
audit是macOS操作系统上一个安全审计功能,最初是从OpenBSM移植到OS X系统的。
由于审计是一个和安全密切的相关的操作,所以它是由内核在系统级别执行的,但是当出现安全性敏感的操作或情况时,用户态应用程序可以请求显示地记录日志,但在大多数情况下,它通过外部定义的审计策略记录用户和进程的操作。这些审计策略决定了哪些事件或情况值得系统关注。因此,系统管理员可以定义和实施审计策略,收集数据,进行安全分析。但是对于系统性能开销比较大,所以在iOS上是没有的。
注意:在macOS 14被弃用了,被EndpointSecurity替代,但是也可以通过一下命令重启audit
bash
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.可以通过一下命令查看日志:
bash
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命令也可以直接操作这个设备查看实时日志。具体先执行以下命令
bash
sudo praudit -s /dev/auditpipe
然后睡眠屏幕,然后在认证解锁系统,可以看到以下类似的输出
go
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
javascript
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
ruby
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
ruby
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
perl
root:lo:no //root用户执行登录和注销操作审计,不执行no(无效的类别,通常不会使用)
1.3.5 audit_warn
管理员可以设定当审计警告出现时运行的脚本程序。
ini
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文件代码所示:
scss
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 命令)的方式修改这个变量的值。
scss
#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 发信号。
scss
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 函数.然后这条记录被释放,如下方示例代码所示:
scss
/*
* 给定一条内核审计记录,根据要求对其进行处理。根据是否还存在一条用户审计记录,
* 内核审计记录被转换为一条或两条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控制管道行为
- 读取分析审计日志
打印进程类别事件示例代码如下:
ini
//
// 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;
}
代码输出如下:
ini
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>