Build an Endpoint Security app
Privacy & Security 高级 26m

构建 Endpoint Security 应用

Build an Endpoint Security app

2020年6月24日

在 Apple 官方观看视频

一句话判断

Endpoint Security Framework 是 macOS 上做企业安全管控的官方武器——如果你还在用 KEXT 做安全监控,Apple 已经明确告诉你:这条路走到了尽头。

这场 Session 讲了什么

Endpoint Security (ES) Framework 是 Apple 在 macOS 10.15 中引入、在 macOS Big Sur 中大幅增强的系统安全框架。它替代了传统的 Kernel Extension (KEXT) 方式来做端点安全监控。Session 系统讲解了 ES 框架的架构、订阅机制和事件处理流程。

ES 框架的核心是事件订阅模型。你的安全应用通过 ESNewSubscriber 注册到系统中,声明自己关心的系统事件类型(如进程创建、文件写入、网络连接等)。当这些事件发生时,系统会将事件详情异步推送给你的应用,你可以选择允许(AUTH)或拒绝(DENY)该操作。整个架构运行在用户空间,不需要加载内核扩展,这意味着更好的系统稳定性和更低的崩溃风险。

Session 还介绍了 macOS Big Sur 中的新能力:支持更多的事件类型、更丰富的元数据(如代码签名信息、进程树关系)、以及性能优化。特别值得一提的是新的 es_event_type_t 枚举扩展到了 60+ 种事件类型,几乎覆盖了所有安全相关的系统行为。

值得深挖的点

Auth 模式与 Notify 模式的区别

ES 框架提供两种监听模式。Auth 模式允许你在事件发生前进行拦截决策——你的回调返回 ES_AUTH_RESULT_ALLOWES_AUTH_RESULT_DENY,系统会据此放行或阻止操作。Notify 模式则只是事后通知,不参与决策。Session 建议:只对真正需要拦截的事件使用 Auth 模式,其他都用 Notify 模式,因为 Auth 模式会增加系统延迟。

Mute 路径的性能优化

ES 事件的吞吐量可能非常惊人——一个繁忙的 macOS 系统每秒可能产生数万个文件操作事件。Session 介绍了 es_mute_pathes_mute_process API,可以过滤掉不需要监控的路径和进程。比如你可以 mute /System/Library/ 路径,只关注用户空间和第三方应用的行为。这个优化直接决定了你的安全应用会不会成为性能瓶颈。

代码片段

import EndpointSecurity
import Foundation

// 创建 Endpoint Security 客户端
class EndpointSecurityMonitor {
    var client: OpaquePointer?
    
    func startMonitoring() -> Bool {
        // 创建 ES 客户端,需要系统扩展权限
        var client: OpaquePointer?
        let result = es_new_client(&client) { client, message in
            // 事件回调处理
            Self.handleMessage(message, client: client)
        }
        
        guard result == ES_NEW_CLIENT_RESULT_SUCCESS else {
            print("创建 ES 客户端失败: \(result)")
            return false
        }
        
        self.client = client
        
        // 订阅关心的事件类型
        var events: [es_event_type_t] = [
            ES_EVENT_TYPE_AUTH_EXEC,      // 进程执行
            ES_EVENT_TYPE_AUTH_OPEN,      // 文件打开
            ES_EVENT_TYPE_AUTH_KILL,      // 进程终止
            ES_EVENT_TYPE_NOTIFY_CONNECT, // 网络连接
        ]
        
        let subscribeResult = es_subscribe(
            client!,
            &events,
            UInt32(events.count)
        )
        
        return subscribeResult == ES_RETURN_SUCCESS
    }
    
    static func handleMessage(_ message: es_message_t, 
                               client: OpaquePointer) {
        let eventType = message.event_type
        
        switch eventType {
        case ES_EVENT_TYPE_AUTH_EXEC:
            handleExecEvent(message)
        case ES_EVENT_TYPE_AUTH_OPEN:
            handleFileOpenEvent(message)
        default:
            break
        }
    }
}
// 处理进程执行事件并做出授权决策
extension EndpointSecurityMonitor {
    static func handleExecEvent(_ message: es_message_t) {
        guard let execEvent = message.event.exec else { return }
        
        // 获取被执行的文件路径
        let targetPath = String(cString: execEvent.target.executable.path.data)
        
        // 获取父进程信息
        let parentPid = message.process.audit_token.pid
        
        // 安全策略:阻止从 /tmp 目录直接执行
        if targetPath.hasPrefix("/tmp/") {
            print("阻止从临时目录执行: \(targetPath), 父进程: \(parentPid)")
            
            // 返回拒绝决策(Auth 模式)
            es_respond_auth_result(
                client!,
                &message,
                ES_AUTH_RESULT_DENY,
                false
            )
            return
        }
        
        // 检查代码签名信息
        let signingInfo = message.process.codesigning_flags
        // 验证签名的完整性...
        
        // 允许执行
        es_respond_auth_result(
            client!,
            &message,
            ES_AUTH_RESULT_ALLOW,
            false
        )
    }
}
// 使用 Mute 路径优化性能
extension EndpointSecurityMonitor {
    func setupMuteRules(client: OpaquePointer) {
        // 过滤掉系统目录,减少不必要的事件
        var systemPaths = [
            "/System/Library/",
            "/usr/lib/",
            "/Library/SystemExtensions/",
        ]
        
        for path in systemPaths {
            let esPath = es_string_token_t(
                length: UInt32(path.utf8.count),
                data: path
            )
            es_mute_path(client, esPath, ES_MUTE_PATH_TYPE_PREFIX)
        }
        
        // Mute 特定进程(如系统守护进程)
        let systemProcesses = [
            "com.apple.kernel",
            "com.apple.securityd",
        ]
        
        for proc in systemProcesses {
            // 按进程标识符 mute
            // 减少来自系统核心进程的事件噪音
        }
        
        print("Mute 规则配置完成")
    }
}

最佳实践

  • ES 客户端必须在 System Extension 中运行,不能直接在普通 app 进程中使用——需要通过 systemextensionsctl 安装
  • Auth 回调中绝对不能做阻塞操作(如网络请求、文件 I/O),否则会拖慢整个系统;需要做异步决策时使用缓存和本地规则
  • 在生产环境中同时部署 Notify 模式的日志收集和 Auth 模式的实时防护,前者用于事后分析,后者用于实时拦截
  • 为 ES 客户端配置合理的缓存大小(es_cache_size),过小会导致事件丢失,过大会占用过多内存
  • 做好降级策略——如果 ES 客户端崩溃,系统会在短暂延迟后移除你的订阅并自动放行所有被阻塞的操作

还有什么值得关注

  • Session 提到 ES 框架的 Audit Token 包含了丰富的进程属性(PID、UID、GID、signing ID 等),比传统 KEXT 拿到的信息更全面
  • macOS Big Sur 引入了 es_event_type_t 的大量新增事件类型,包括 mount/unmount、signal 发送、IPC 通信等
  • 对于企业级部署,ES 应用需要通过 MDM (Mobile Device Management) 推送安装,不能通过 App Store 分发
WWDC 2020