Skip to content

5. Endpoint Security Overview

Brandon Dalton edited this page Dec 13, 2023 · 1 revision

Summary

The Endpoint Security (ES) API enables three primary classes of features: user space clients, path muting / inversion, and notification / authorization of system events. To date ES eventing covers a wide array of system activity: Process, File metadata, Memory mapping, Login, Background Task Management (BTM), XProtect, etc the list goes on. For a process to connect to ES it must be properly code signed with the entitlement to do so -- see ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED. Common examples of Endpoint Security solutions include: modern Endpoint Detection and Response (EDR) sensors, endpoint monitoring and troubleshooting tools, binary authorization, etc.

Terminology

  • Message: A notification sent by endpointsecurity.kext to your security agent. Messages are structs of type es_message_t (ESMessage.h) and have a few key top level properties:
    • action_type: Specifying an AUTH or NOTIFY event (see below)
    • event: The security event to deliver to subscribed clients (e.g. create file)
    • process: The process which has initiated the activity resulting in the event
    • thread: The thread that performed the action resulting in an event
  • Events: A notification your client receives of system activity (e.g. a user being added to an Open Directory node). There are two classes of events: notification and authorization (allowing your security agent to allow / deny an action). Additionally, there's a split between events emitted by XNU and by user space libraries.
  • Client: A C struct of type es_client_t (ESClient.h) which enables your security agent to perform actions. NOTE: Full Disk access is a requirement of the hosting process! For more info see ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED. One of the first things that security agents will do is create the ES client: es_new_client(). The client exposes functionality enabling it to act as a user space security agent.
  • Event subscriptions: Clients ask to be notified for a set of ES events (e.g. ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN, ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_AUTH_MMAP). When a client subscribes to events they will be notified by ES when any action resulting in that event occurs.
  • Target path: The logical path specified in the event's structure (not the message's).
  • Muting / unmuting: Instruct ES that you do not want to be notified of events matching a set of critieria. Globally or per event.
    • By process initiating path / target path (literal or prefix)
    • By process audit token
  • Inversion: Essentially selection instead of muting. Here developers have the option to invert muting based on: path, process audit token, or target path (the path specified in the event not the initiating process in the message).
  • Authorizing system activity: Clients can respond with ES_AUTH_RESULT_DENY to prevent the action or ES_AUTH_RESULT_ALLOW to allow it.
  • Subscriptions: Defines a set of events for your security agent to monitor.

Implementation options

Developers of security agents have two primary ways to connect to ES: System Extension (act as a user space KEXT / driver) or Launch Deamon (act as a regular System scope daemon). Launch Agents are not available here because to connect to ES the process must also be running as root -- see ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED. By taking the System Extension option developers will benefit from System Integrity Protection's (SIP) "rootless" defense against tampering. Running your agent as a Launch Daemon opens your implementation up to potential tampering. However, by running in this state developers do not need to deal with the somewhat tedious System Extension installation process. Additionally, for research use cases it's not necessary for either -- as long as the above requirements for entitlement and privilege are satisfied: see our AtomicESClient example and Apple's own /usr/bin/eslogger utility.

Uninstalling System extensions

Even if the root user wanted to remove a running Endpoint Security Extension they cannot in most cases. However, there are a few different options:

  • Trigger the developers API call to remove the system extension by following uninstall guidance.
  • By default the API call to remove the system extension will be triggered if the user deletes the hosting app using the Finder.
    • Non-EDR products can likely be removed in this way
  • Boot into recoveryOS (detailed above) and use systemextensionsctl to uninstall it.
    • Only necessary if SIP is enabled.

What makes ES so special?

ES is designed to facilitate "user space security agents" which for all the reasons listed above is a good idea: OS integrity, security, and user experience being some standouts. Beyond simply alerting us of new events ES allows us to take things one step further. There are two classes of event types in ES: notification and authorization. Notification event types (the majority) as their name entails simply "notify" the user of system activity. Authorization events on the other hand enable your security agent to authorize the activity -- very similar to KAuth (discussed above). Additionally, for your user space client to successfully register with ES you must be properly entitled with the com.apple.developer.endpoint-security.client entitlement (available on application from Apple). This restriction is very similar to Microsoft's Protected Process Light (PPL) level for ELAM (Early Launch Antimalware) drivers. Additionally, ELAM developers need to be a member of the Microsoft Virus Initiative (MVI) and must have their driver be signed by the Windows Hardware Quality Lab (WHQL).

Architecture

ES is architected as: user land libraries: libEndpointSecuritySystem.dylib (for entitled system eventing) / libEndpointSecurity.dylib (for developers), a user land daemon /usr/libexec/endpointsecurityd and the KEXT / “driver”: EndpointSecurity.kext. Apple, by architecting ES in this way enables defense-in-depth by proxying requests from the user space clients through Apple services to the kernel drivers. Now, you might ask yourself: "so where then do events come from?". The answer is two fold:

  • Emitted by the kernel: events such as ES_EVENT_TYPE_NOTIFY_SETGID and ES_EVENT_TYPE_NOTIFY_EXEC. These events are emitted by hooking the fundamental system calls responsible. Such as: setgid() for setting a file's GID and execve(2)/posix_spawn(2) for process execution.
  • Or... by the responsible user space library/binary. These are decently obvious to identify. For example ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_ADD has no corresponding syscall (as it's a much higher level abstraction). Therefore, this event is emitted directly by backgroundtaskmanagementd and sent to endpointsecurity.kext to notify subscribed clients.

Un-entitled processes are strictly disallowed from emitting ES events into endpointsecurity.kext. Binaries which are allowed will be signed with entitlements of the form: com.apple.private.endpoint-security.submit.*.

Sample code?

We have you covered! Please take a look at AtomicESClient, this project's goal is to get users up and going with an es_client_t of their own with minimal effort!

As a quick overview:

// @discussion: This ES event will give you basic *high level* process execution information.
public var esEventSubs: [es_event_type_t] = [
    ES_EVENT_TYPE_NOTIFY_EXEC
]

var client: OpaquePointer?

// MARK: - New ES client
// Reference: https://developer.apple.com/documentation/endpointsecurity/client
let result: es_new_client_result_t = es_new_client(&client){ _, event in
    // Here is where the ES client will "send" events to be handled by our app -- this is the "callback".
    completion(EndpointSecurityClientManager.eventToJSON(value: ExampleESEvent(fromRawEvent: event)))
}

// MARK: - Event subscriptions
// Reference: https://developer.apple.com/documentation/endpointsecurity/3228854-es_subscribe
if es_subscribe(client!, esEventSubs, UInt32(esEventSubs.count)) != ES_RETURN_SUCCESS {
    print("[ES CLIENT ERROR] Failed to subscribe to core events! \(result.rawValue)")
    es_delete_client(client)
    exit(EXIT_FAILURE)
}