Skip to content

8. Endpoint Security DYLIB

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

Overview

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.

Developer usage

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:

// @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);

An arbitrary client's event subscriptions

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 script

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 the es_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

Exported functions

Note

Function definition references are directly from Apple's Endpoint Security developer documentation. Functions here are prefixed with ES for Endpoint Security.

Documented

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

Authorizing system activity

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

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.

Undocumented

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 by es_new_client.
  • es_sync_client: Internally called by es_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 to IOConnectCallScalarMethod(...) with the selector 0x11.
  • es_disable_exclusive_mode: Does not appear to be internally called. Fundamentally, this function calls out to IOConnectCallScalarMethod(...) with the selector 0x13.
  • es_enable_exclusive_mode: Does not appear to be internally called. Fundamentally, this function calls out to IOConnectCallScalarMethod(...) with the selector 0x13.
  • sysdiagnoseInformationForEndpointSecurity: Does not appear to be internally called. Fundamentally, this function collects diagnostic information. It calls out to es_copy_diagnostics.