Highlight
Endpoint Security 让 macOS 安全产品用用户空间的 C API 订阅约 100 种系统事件,并通过 NOTIFY 与 AUTH 事件替代 Kauth KPI、Mac kernel framework 和 OpenBSM audit trail。
核心内容
过去做 macOS 端点安全,很多产品要进入内核。开发者需要写 kernel extension(内核扩展,KEXT),还要面对内核接口变化、调试困难、维护成本高的问题。一个小 bug 也可能触发 kernel panic,让整台机器不稳定。
Endpoint Security(端点安全,ES)把这件事移到用户空间。它在 macOS Catalina 首次引入,目标是替代 Kauth KPI、未支持的 Mac kernel framework,以及 OpenBSM audit trail。安全产品通过普通应用或 System Extension 建立 ES client,订阅系统事件,再在回调里分析进程、文件、代码签名、参数、打开标志等上下文。
这场 session 的重点放在安全产品真正会遇到的完整链路:怎样建立事件流,怎样处理 NOTIFY 事件,怎样在 AUTH 事件里允许或拒绝操作,怎样避开消息队列堆积、缓存误用、调试超时和早启动漏事件。
把 ES 放进 System Extension 还有额外收益。扩展安装后受 System Integrity Protection(系统完整性保护,SIP)保护,launchd job 不能被 root 用户随意卸载;某些能力只对 System Extension 开放,例如 early boot。启用 early boot 后,系统启动时会等待 ES 扩展完成首次订阅,再放行第三方应用执行。
详细内容
本场没有官方 code snippets。下面的代码块来自 transcript 中逐步展示的 API 组合,用于说明最小骨架和关键判断点;真实产品还需要补齐 entitlement、错误处理、日志和策略存储。
建立 ES client 并订阅事件
(03:15)ES 是 C library。Apple 选择 C,是为了控制内存和性能特征,也方便 Swift、Objective-C、Rust 等语言调用。一个最小客户端先调用 es_new_client 建立事件流,再用 es_subscribe 声明事件类型,最后用 dispatch_main 让进程持续处理回调。
es_client_t *client = NULL;
es_new_client(&client, ^(es_client_t *client, const es_message_t *message) {
printf("event type: %u\n", message->event_type);
});
es_event_type_t events[] = {
ES_EVENT_TYPE_NOTIFY_EXEC,
};
if (es_subscribe(client, events, 1) != ES_RETURN_SUCCESS) {
es_delete_client(client);
return;
}
dispatch_main();
关键点:
es_new_client返回es_client_thandle,并注册事件处理 block。es_subscribe决定这个 client 会收到哪些事件;示例里只订阅NOTIFY_EXEC。- 订阅失败时用
es_delete_client清理前面创建的 client。 dispatch_main让进程继续运行,事件会通过 handler block 送进来。
理解 es_message_t 的三类信息
(05:54)所有事件都会包装进 es_message_t。它包含消息元数据、触发事件的进程信息,以及事件自己的数据。演示里添加 NOTIFY_EXIT 后,通过 message->process 取进程路径和 audit token,通过 message->event.exit.stat 取退出状态。
switch (message->event_type) {
case ES_EVENT_TYPE_NOTIFY_EXIT: {
es_string_token_t path = message->process->executable->path;
pid_t pid = audit_token_to_pid(message->process->audit_token);
int status = message->event.exit.stat;
os_log(log, "process %{public}.*s pid %d exited with status %d",
(int)path.length, path.data, pid, status);
break;
}
default:
break;
}
关键点:
message->event_type决定应该读取eventunion 中的哪个字段。message->process描述触发事件的进程,包含 executable、代码签名、PID、UID 等信息。audit_token_to_pid可以从 audit token 中取出 PID;audit token 还包含用户 ID 等身份信息。es_string_token_t携带长度和数据指针,日志输出时不要假设它是以空字符结尾的 C 字符串。
处理 AUTH 事件时必须回应
(13:59)NOTIFY 事件是异步通知,操作会继续执行。AUTH 事件是同步授权,内核会暂停原操作,等待 ES client 回应或等待 deadline 过期。每条 AUTH message 都有自己的 deadline,client 必须逐条检查并及时回应。
static void handle_exec(es_client_t *client,
const es_message_t *message,
es_string_token_t signing_id_to_block) {
es_string_token_t signing_id = message->event.exec.target->signing_id;
bool should_block = matches_signing_id(signing_id, signing_id_to_block);
es_respond_auth_result(client,
message,
should_block ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW,
true);
}
关键点:
AUTH_EXEC可以用es_respond_auth_result返回ALLOW或DENY。- 示例策略读取
message->event.exec.target->signing_id,再和要阻止的 signing ID 比较。 matches_signing_id是产品自己的字符串比较逻辑,不是 ES API。- 最后一个参数控制是否允许 ES 缓存这个结果;缓存只能用于性能,不能当成策略来源。
用 flags 限制文件打开权限
(24:12)有些 AUTH 事件不能只返回允许或拒绝。AUTH_OPEN 用 es_respond_flags_result 返回允许的打开标志。演示里有三个策略:EICAR 测试文件全部拒绝,/usr/local/bin 只允许只读打开,其他文件放行。
static void deny_write_in_directory(es_client_t *client,
const es_message_t *message) {
uint32_t allowed_flags = UINT32_MAX;
allowed_flags &= ~FWRITE;
es_respond_flags_result(client, message, allowed_flags, true);
}
static void deny_all_open(es_client_t *client,
const es_message_t *message) {
es_respond_flags_result(client, message, 0, true);
}
关键点:
AUTH_OPEN使用的是 kernel 版本的 open flags,不是open(2)里常见的oflags。allowed_flags清掉FWRITE后,可以保留只读访问,同时拒绝写入。- 返回
0会清掉所有允许位,用于阻止对某个文件的所有 open 操作。 - 对 flags 结果启用缓存时,回应应包含这个 client 将来愿意允许的全部 flags,不能只回当前请求里的 flags。
异步处理前复制 message
(24:23)handler block 应尽快返回,避免消息队列变大并导致丢消息。如果要做文件扫描、I/O 或 CPU 密集任务,先用 es_copy_message 延长 message 生命周期,再把工作丢到异步队列,完成后用 es_free_message 释放。
static void handle_open(es_client_t *client,
const es_message_t *message,
dispatch_queue_t worker_queue) {
const es_message_t *copied_message = es_copy_message(message);
dispatch_async(worker_queue, ^{
handle_open_worker(client, copied_message);
es_free_message(copied_message);
});
}
关键点:
- handler block 返回后,原始
message不再保证有效。 es_copy_message让 AUTH 事件可以稍后回应,也允许不同 AUTH message 不按接收顺序回应。- 异步队列的 quality of service 要按事件类型和事件量选择,订阅 AUTH 事件时尤其要谨慎。
- 即使异步处理,也必须赶在该 message 的 deadline 前回应。
early boot 要一次性订阅关键事件
(30:24)early boot 只对 ES System Extension 开放。扩展在 Info.plist 中设置 NSEndpointSecurityEarlyBoot 后,系统启动时会等待它完成首次 es_subscribe,再允许第三方应用执行。
<key>NSEndpointSecurityEarlyBoot</key>
<true/>
es_event_type_t startup_events[] = {
ES_EVENT_TYPE_AUTH_EXEC,
ES_EVENT_TYPE_NOTIFY_EXEC,
ES_EVENT_TYPE_NOTIFY_FORK,
ES_EVENT_TYPE_NOTIFY_EXIT,
};
es_subscribe(client,
startup_events,
sizeof(startup_events) / sizeof(startup_events[0]));
关键点:
- readiness 信号来自首次
es_subscribe调用。 - 关键启动事件应尽量放在同一次订阅里;拆成多次订阅会让第三方进程在后续订阅前开始执行。
- ES 有启动 deadline;过期后第三方执行会自动放行。
- 长初始化工作应放在首次订阅之后,避免启动阶段漏事件。
Big Sur 的新增能力和迁移信号
(38:00)Apple 在这场 session 里宣布 audit subsystem 正在废弃,依赖 audit trail 文件或 Auditpipe 的产品应迁移到 Endpoint Security。macOS Big Sur 还为 EXEC 事件增加文件描述符信息,改进性能,并新增 trace 与 CS_INVALIDATED 等事件。
if (message->version >= 2) {
const es_event_create_t *create = &message->event.create;
inspect_acl_if_present(create->acl);
}
关键点:
message->version用于跨系统版本兼容;访问新字段前要先检查版本。- EXEC 事件可以提供新进程启动时携带的 file descriptors,并暴露 pipe 的唯一标识,方便追踪进程间通信。
CS_INVALIDATED会在进程代码签名有效位被清除时立即通知 ES client。- trace 事件可以通知 client 某个进程正在被调试。
核心启发
1. 做一个进程生命周期审计器
- 做什么:订阅
NOTIFY_FORK、NOTIFY_EXEC、NOTIFY_EXIT,记录进程树、可执行路径、PID、退出状态和代码签名信息。 - 为什么值得做:session 的第一个 demo 就用
ps展示了 fork、exec、exit 三个事件如何串起一次命令执行。 - 怎么开始:先用
es_new_client建立 client,再在同一次es_subscribe中订阅这三个 NOTIFY 事件,把message->process和事件特定字段写入统一日志。
2. 做一个企业应用启动拦截策略
- 做什么:用
AUTH_EXEC拦截特定 signing ID 或 CD hash 的可执行文件,阻止未批准工具启动。 - 为什么值得做:AUTH 事件会同步暂停原操作,安全产品可以在 deadline 前给出允许或拒绝结果。
- 怎么开始:从
message->event.exec.target->signing_id读取目标进程签名标识,匹配本地策略后调用es_respond_auth_result。
3. 做一个敏感目录只读保护
- 做什么:对
/usr/local/bin或企业自定义目录订阅AUTH_OPEN,允许读取,拒绝写入。 - 为什么值得做:演示里的 flags response 展示了同一个文件可以被读,但写入会被拒绝,适合保护工具链目录或安全基线目录。
- 怎么开始:检查
message->event.open.file的路径前缀,命中后用es_respond_flags_result返回清掉FWRITE的 flags。
4. 做一个早启动安全守护扩展
- 做什么:把 ES client 做成 System Extension,并启用 early boot,确保第三方应用执行前安全 agent 已经订阅关键事件。
- 为什么值得做:session 明确提到 early boot 是只有 ES System Extension 才能使用的能力,适合 EDR 和防病毒产品。
- 怎么开始:在扩展 Info.plist 加
NSEndpointSecurityEarlyBoot,启动后尽快一次性订阅AUTH_EXEC、NOTIFY_EXEC等启动阶段需要的事件。
关联 Session
- Modernize PCI and SCSI drivers with DriverKit — 同样围绕把内核扩展迁移到用户空间,补充 DriverKit 和 System Extension 的部署背景。
- Secure your app: threat modeling and anti-patterns — 从威胁建模和安全反模式角度解释安全产品要防什么,适合和 Endpoint Security 的事件拦截能力一起看。
- Explore logging in Swift — 介绍隐私友好的统一日志 API,可用于 Endpoint Security 产品记录检测和调试信息。
- What’s new in managing Apple devices — 覆盖企业设备管理和 MDM 能力,和本场提到的 System Extension 自动批准与 Full Disk Access 配置相关。
评论
GitHub Issues · utterances