-
Notifications
You must be signed in to change notification settings - Fork 47
8. Endpoint Security DYLIB
Similar to the system library we've just discussed: libEndpointSecuritySystem.dylib
this library: libEndpointSecurity.dylib
handles exposing the power of the Endpoint Security KEXT safely via API to third party code. In other words, when developers build Endpoint Security apps they'll sign it with the required entitlement and link it against libEndpointSecurity.dylib
. Doing so exposes functionality like: creating new clients, subscribing to events, applying path muting, and authorizing system activity. These are all capabilities that have traditionally been out of reach for user space agents and were reserved exclusively for KEXTs in the form of the KAuth KPI, MACF, and OpenBSM APIs. We've explored these largely legacy technologies in previous sections. However, Endpoint Security fundamentally still very much relies on them.
The following code is a distilled version of our AtomicESClient project. This project attempts to demonstrate demonstraite how to create a new Endpoint Security app in Swift for the command line. In our build instructions we note that you will need to link against Endpoint Security. You can do this at the command line or with Xcode. For example,
Important
swiftc AtomicESClient.swift -L /Applications/Xcode.app/.../MacOSX.sdk/usr/lib/ -lEndpointSecurity -lbsm -o AtomicESClient
Here's the basic structure:
- Define event subscriptions:
ES_EVENT_TYPE_NOTIFY_EXEC
- Create a new ES client:
es_new_client(...)
- Define a completion callback function
eventToJSON(...)
- Define a completion callback function
- Make your event subscriptions:
es_subscribe(...)
- (Optional) Apply path muting:
es_mute_path
/es_mute_path_events
// @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)
}
// Mute all event notifications where `launchd` is the parent process.
let muteResult: es_new_client_result_t = es_mute_path(&client, "/sbin/launchd", ES_MUTE_PATH_TYPE_LITERAL);
Above we've seen how developers link against libEndpointSecurity.dylib
to enable them to call functions like es_new_client(...)
, es_subscribe(...)
, and es_mute_path
/ es_mute_path_events
. It begs the question... can we intercept an arbitrary client making its event subscriptions? After all, we know exactly which function we're after to hook: es_subscribe(...)
.
Tip
In addition to targeting an individual client: E.g. com.vmware.carbonblack.cloud.se-agent.extension
, com.redcanary.agent.securityextension
, or com.refractionpoint.rphcp.extension
, etc. you can generalize and hook the sendEvent(...)
function of endpointsecurityd
which handles sending analytics for ES clients system wide. Doing so will provide resolution whenever a client subscribes to events and on ESE install / uninstall.
If we know exactly which client / EDR sensor we're interested we can grab its PID, attach Frida, and attempt to force the client to make event subscriptions. If that last step isn't feasible you can still get the same information by instrumenting endpointsecurityd
directly as described above and walked through in the Endpoint Security Daemon section. Using Mac Monitor is perfect for demonstration here as we can dynamically subscribe to and from events to trigger the API.
The full script can be found at the Gist here. We know exactly what we're looking for so we can go ahead and target
libEndpointSecurity.dylib
directly here and specify thees_subscribe(...)
function to be hooked:
const moduleName = 'libEndpointSecurity.dylib';
const functionName = 'es_subscribe';
Next, we'll want to find the module and attach our interceptor to the ES event subscription function:
const address = Module.findExportByName(moduleName, functionName);
if (address != null) {
Interceptor.attach(address, {
// ...
});
}
Additionally, like we've shown in the Endpoint Security daemon section we'll need to decode the event types to a human readable form since they exist as a C enumeration. Lastly, the function has the signature of:
es_return_t es_subscribe(es_client_t *client, const es_event_type_t *events, uint32_t event_count);
So we're interested in the second / third arguments. The client here isn't as useful to us as can be seen in the ES documentation: "An opaque type that stores the Endpoint Security client state". So,
// Pull the number of events requested from the stack (arg 2)
const eventCount = args[2].toInt32();
// The events are stored in a list of es_event_type_t values
const eventsPtr = args[1];
// Ensure we stay within the event bounds
for (let i = 0; i < eventCount; i++) {
const eventType = eventsPtr.add(i * 4).readInt();
// Convert the event type to a human readable string
const eventName = eventTypeMapping[eventType] || `Unknown (${eventType})`;
console.log(eventName);
}
Now we can try it out! Let's target the Red Canary Security Extension. First ensure that Mac Monitor is installed. Next, run:
sudo frida -p $(TARGET_CLIENT_PID) -l event_subscription_interceptor.js
Then when you launch the app Mac Monitor will make event subscriptions. However, you can also influence this yourself directly in Settings > Subscriptions
. Here's sample output you'd expect to see for Mac Monitor v1.0.5 would be. Note that this is Mac Monitor's initial set of event subscriptions -- more events will be appended as you subscribe to them:
Number of events requested: 27
ES_EVENT_TYPE_NOTIFY_EXEC
ES_EVENT_TYPE_NOTIFY_FORK
ES_EVENT_TYPE_NOTIFY_EXIT
ES_EVENT_TYPE_NOTIFY_CREATE
ES_EVENT_TYPE_NOTIFY_DELETEEXTATTR
ES_EVENT_TYPE_NOTIFY_MMAP
ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_ADD
ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_REMOVE
ES_EVENT_TYPE_NOTIFY_OPENSSH_LOGIN
ES_EVENT_TYPE_NOTIFY_XP_MALWARE_DETECTED
ES_EVENT_TYPE_NOTIFY_MOUNT
ES_EVENT_TYPE_NOTIFY_LOGIN_LOGIN
ES_EVENT_TYPE_NOTIFY_LW_SESSION_UNLOCK
ES_EVENT_TYPE_NOTIFY_RENAME
ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE
ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED
ES_EVENT_TYPE_NOTIFY_TRACE
ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN
ES_EVENT_TYPE_NOTIFY_PROFILE_ADD
ES_EVENT_TYPE_NOTIFY_OD_CREATE_USER
ES_EVENT_TYPE_NOTIFY_OD_GROUP_ADD
ES_EVENT_TYPE_NOTIFY_OD_MODIFY_PASSWORD
ES_EVENT_TYPE_NOTIFY_OD_ATTRIBUTE_VALUE_ADD
ES_EVENT_TYPE_NOTIFY_XPC_CONNECT
ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_PETITION
ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_JUDGEMENT
ES_EVENT_TYPE_NOTIFY_OD_CREATE_GROUP
Note
Function definition references are directly from Apple's Endpoint Security developer documentation. Functions here are prefixed with ES for Endpoint Security.
Client management
-
es_new_client
: Creates a new client instance and connects it to the Endpoint Security system. The handler block receives messages serially, and in the order the system delivers them. Returning control from the handler causes Endpoint Security to dequeue the next available message. You can respond to a message out of order by returning control before calling one of the es_respond-prefixed functions. For out-of-order responding, your handler must copy the message with es_copy_message. To create a client, your app must have the com.apple.developer.endpoint-security.client entitlement. The user also needs to approve your app with Transparency, Consent, and Control (TCC) mechanisms. The user does this in the Security and Privacy pane of System Preferences, by adding the app to Full Disk Access. When you no longer need to receive Endpoint Security messages, destroy the client with es_delete_client to free resources. -
es_delete_client
: Destroys and disconnects a client instance from the Endpoint Security system.
Event subscriptions
-
es_subscribe
: Subscribes a client to a set of events. -
es_unsubscribe
: Unsubscribes the provided client from a set of events. -
es_unsubscribe_all
: Unsubscribes a client from all events. -
es_subscriptions
: Returns a list of the client’s subscriptions.
Authorizing system activity
-
es_respond_auth_result
: Responds to an event that requires an authorization response. -
es_respond_flags_result
: Responds to an event that requires authorization flags as a response.
Housekeeping
-
es_retain_message
: Retains the given message, extending its lifetime until released. -
es_release_message
: Releases a previously-retained message. -
es_clear_cache
: Clears all cached results for all clients. Endpoint Security shares caches across all clients, so you can provide any valid client as the parameter to this function.
Muting
-
es_mute_path
: Suppresses events from executables that match a given path. -
es_mute_path_events
: Suppresses a subset of events from executables that match a given path. -
es_mute_process
: Suppresses events from a given process. To mute a subset of events from a process, use es_mute_process_events. -
es_mute_process_events
: Suppresses a subset of events from a given process. -
es_muted_paths_events
: Retrieve a list of all muted paths. -
es_muted_processes_events
: Retrieve a list of all muted processes. -
es_release_muted_paths
: Frees resources associated with a set of previously-retrieved muted paths. -
es_release_muted_processes
: Frees resources associated with a set of previously-retrieved muted processes. -
es_unmute_all_paths
: Restores event delivery from previously-muted paths. es_unmute_all_target_paths
-
es_unmute_path
: Restores event delivery from a previously-muted path. -
es_unmute_path_events
: Restores event delivery of a subset of events from a previously-muted path. -
es_unmute_process
: Restores event delivery from a previously-muted process. -
es_unmute_process_events
: Restores event delivery of a subset of events from a previously-muted process.
Inversion
Process execution event helpers
-
es_exec_arg
: Gets the argument at the specified position from a process execution event. This function doesn’t allocate memory for the returned token; it points to a string token inside of event. Because you don’t own this memory, don’t try to free it. -
es_exec_arg_count
: Gets the number of arguments from a process execution event. -
es_exec_env
: Gets the environment variable at the specified position from a process execution event. This function doesn’t allocate memory for the returned token; it points to a string token inside of event. Because you don’t own this memory, don’t try to free it. The returned pointer must not outlive the event parameter passed to the function, because the pointer will likely be invalid after the function returns. -
es_exec_env_count
: Gets the number of environment variables from a process execution event. -
es_exec_fd
: Gets the file descriptor at the specified position from a process execution event. This function doesn’t allocate memory for the returned file descriptor description; it points to an es_fd_t inside of event. Because you don’t own this memory, don’t try to free it. The returned pointer must not outlive the event parameter passed to the function, because the pointer will likely be invalid after the function returns. -
es_exec_fd_count
: Gets the number of file descriptors from a process execution event.
Deprecated
- (Deprecated)
es_free_message
: Frees the memory allocated for the given message. Only free messages you explicitly copied with es_copy_message. Freeing a message from inside a handler block will cause your app to crash. - (Deprecated)
es_copy_message
- (Deprecated)
es_message_size
: Calculates the size of a message structure. - (Deprecated)
es_mute_path_literal
: Suppresses events from executables matching a path literal. - (Deprecated)
es_mute_path_prefix
: Suppresses events from executables matching a path prefix. - (Deprecated)
es_muted_processes
: Generates a list of muted processes.
Note
The descriptions of these functions were inferred through reverse engineering libEndpointSecurity.dylib
. The recommended approach here would be to use Hopper.app (or otherwise carve it out) as the dylib lives within the shared cache.
-
es_new_client_with_config
: Internally called byes_new_client
. -
es_sync_client
: Internally called byes_delete_client
-
es_register_early_boot_client
: Used by internal logic:sysdiagnoseInformationForEndpointSecurity
-
es_unregister_early_boot_client
: Does not appear to be internally called -- likely supporting interaction with the System Extension subsystem. -
es_unregister_early_boot_clients
: Does not appear to be internally called -- likely supporting interaction with the System Extension subsystem. -
es_invert_path_match
: Does not appear to be internally called. Fundamentally, this function calls out toIOConnectCallScalarMethod(...)
with the selector0x11
. -
es_disable_exclusive_mode
: Does not appear to be internally called. Fundamentally, this function calls out toIOConnectCallScalarMethod(...)
with the selector0x13
. -
es_enable_exclusive_mode
: Does not appear to be internally called. Fundamentally, this function calls out toIOConnectCallScalarMethod(...)
with the selector0x13
. -
sysdiagnoseInformationForEndpointSecurity
: Does not appear to be internally called. Fundamentally, this function collects diagnostic information. It calls out toes_copy_diagnostics
.