Skip to content

6. Endpoint Security Daemon

Brandon Dalton edited this page Dec 14, 2023 · 9 revisions

Summary

While most of the logic here is written to support System Extensions which implement the Endpoint Security API, known as Endpoint Security Extensions (ESE), the daemon also handles recording analytics on all connected clients. Furthermore, we can break down responsibilities into a few key classes:

  • ESE validation
    • (e.g. -[ESD listener:validateExtension:atTemporaryBundleURL:replyHandler:])
  • ESE install / uninstall
    • (e.g. -[ESD listener:willUninstallExtension:replyHandler:])
    • TCC (Transparency, Consent, and Control) management
  • Registering early boot clients (those with the NSEndpointSecurityEarlyBoot key in their Info.plist)
  • Analytics: CoreAnalytics.framework
    • (e.g. -[ESCoreAnalytics sendEvent:event:])
    • See the Instrumenting ESCoreAnalytics section below for how we produced this result! Using this method you should be able to monitor any arbitrary ES client making their subscriptions to the Endpoint Security subsystem.
    [+] sendEvent:event: entered
        Endpoint Security Event subscription details:
        ClientDisposition: sysext
        EventType: ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_PETITION (129)
        BundleID: com.redcanary.agent.securityextension
        TeamID: UA6JCQGF3F
        CDHash: 1c2dc75b5d568ddf7981179a8482d7b1c383d5e
    

ESD class

This functionality is largely internally implemented by the ESD Objective-C class. We can note that just by enumerating the functions implemented by those classes (see below). Additionally, the daemon instantiates an OSSystemExtensionPointListener delegate (enabling a privileged XPC channel between endpointsecurityd/nesessionmanager and sysextd). Here we can see the primary use case for each of these daemons: validating and managing the life cycle of Endpoint Security / Network Extensions.

Properties and methods

@class ESD : NSObject<OSSystemExtensionPointListenerDelegate> {
  @property listener
  @property hash
  @property superclass
  @property description
  @property debugDescription
  ivar _listener
  -init
  -run
  -sanityCheckExtensionInfo:
  -isValidBundle:
  -submitLaunchdJob:
  -removeLaunchdJob:
  -createLabelName:
  -smJobDictionary:
  -safeSubmitJob:
  -safeRemoveJob:
  -listener:validateExtension:atTemporaryBundleURL:replyHandler:
  -listener:willStartExtension:replyHandler:
  -listener:startExtension:replyHandler:
  -listener:willReplaceExtension:withExtension:replyHandler:
  -listener:willTerminateExtension:replyHandler:
  -listener:terminateExtension:replyHandler:
  -listener:willUninstallExtension:replyHandler:
  -listener
  -setListener:
  -.cxx_destruct
}

Data sheet

  • Signing ID: com.apple.endpointsecurityd
  • Image path: /usr/libexec/endpointsecurityd
  • Plist path: /System/Library/LaunchDaemons/com.apple.endpointsecurity.endpointsecurityd.plist
  • launchd domain: system/com.apple.endpointsecurity.endpointsecurityd
  • CS flags: 0x2300(hard,kill,library-validation)
  • Launchd Endpoints:
    • com.apple.endpointsecurity.endpointsecurityd.mig: Communication with the Endpoint Security KEXT.
    • com.apple.endpointsecurity.system-extensions: Communication with the System Extensions subsystem.
    • com.apple.endpointsecurity.endpointsecurityd.xpc: Label for the"IPC server" facilitating communication between Endpoint Security user clients and the daemon itself.
  • Entitlements:
    {
      "com.apple.private.endpoint-security.manager": true,
      "com.apple.private.security.storage.SystemExtensionManagement": true,
      "com.apple.private.system-extensions.extension-point": true,
      "com.apple.private.tcc.manager.access.delete": [
        "kTCCServiceSystemPolicyAllFiles",
        "kTCCServiceEndpointSecurityClient"
      ],
      "com.apple.private.tcc.manager.access.modify": [
        "kTCCServiceSystemPolicyAllFiles",
        "kTCCServiceEndpointSecurityClient"
      ],
      "com.apple.private.tcc.manager.access.read": [
        "kTCCServiceAll",
        "kTCCServiceEndpointSecurityClient"
      ],
      "com.apple.private.xpc.protected-services": true
    }

Note

The com.apple.private.endpoint-security.manager entitlement enables connection with the Endpoint Security KEXT and this is the only occurrence.

Static artifacts

  • /Library/SystemExtensions/EndpointSecurity/.started_es_jobs.plist: Contains the CandidateCDHash(s) of ESE(s) in the [activated enabled] state.
  • /Library/SystemExtensions/EndpointSecurity/.early_boot.plist: Contains the CandidateCDHash(s) of ES clients to be started before any other third party software. This is defined by the developer in their Info.plist with the NSEndpointSecurityEarlyBoot key. If enabled macOS will hold up execution of non-platform binaries until all registered early boot clients make their first subscription(s).

Daemon bootup

To start with, at the beginning of execution, the daemon calls: the BSD syscall: sysctlbyname to check if SafeBoot is enabled: sysctlbyname("kern.safeboot", &s, &var_b8, 0, 0). If it's not then it will Initialize a new sandbox profile with: sandbox_init by the name of: com.apple.endpointsecurity.endpointsecurityd: sandbox_init("com.apple.endpointsecurity.endpointsecurityd", 0x2, &var_F0);. Now that initial resources have been properly set it needs a way to communicate with clients. To do this the daemon initializes an "IPC server" / XPC service by the mach service name of: com.apple.endpointsecurity.endpointsecurityd.xpc. Lastly, the daemon handles the launching of the remaining ESE(s).

Tip

Interestingly enough there is one command line switch you can use to interactively execute the daemon.

./endpointsecurityd --init
endpointsecurityd: Starting early boot task
endpointsecurityd: Loaded 0 early boot clients from .early_boot.plist
endpointsecurityd: No need to downcall, there are no early boot

Dynamic analysis with Frida

Above we mentioned that in the daemon's implementation that the ESD class does the lion's share of the work. Naturally, we'd like to understand what's going on here at runtime. Leveraging the dynamic instrumentation toolkit Frida enables us to to very quickly understand what's going on in any given function.

For the Endpoint Security Daemon specifically we devised the following set of tests:

  1. Client initialization (e.g. /usr/bin/eslogger). Mostly handled by libEndpointSecurtity.dylib for the reasons below

    1. Create a new with es_client_t
    2. Make event subscriptions with es_subscribe
    3. (optionally) apply path muting
  2. ESE install / uninstall. Mostly handled by SystemExtensions.framework

    1. Submit the activation request
      1. OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier identifier: String, queue: dispatch_queue_t) -> Self
      2. OSSystemExtensionManager.sharedextensionManager.submitRequest(request)
    2. This request will prompt the user for approval in the form of a Gatekeeper dialog
  3. ESE connection and event subscriptions (e.g. Mac Monitor)

    1. The key difference here between the above is that this test is based on an Endpoint Security Extension

Now that we have our targets in mind and a few tests developed we can leverage frida-trace to quickly test our high level hypotheses: "is a given function called when this test occurs?". From there we can dig a bit deeper and develop a full Frida script using a method of your choice. I'll be using JavaScript here. The code is mostly self explanatory, but the key points to note here are:

  • Get a reference to each of the target Objective-C classes (ESD, ESSystemExtensionClient, and ESCoreAnalytics)
if (ObjC.available) {
    try {
        // Get a reference to the target classes and their methods
        const ESD = ObjC.classes.ESD;
        const ESDMethods = ESD.$ownMethods;

        const ESSystemExtensionClient = ObjC.classes.ESSystemExtensionClient;
        const ESSystemExtensionClientMethods = ESSystemExtensionClient.$ownMethods;

        const ESCoreAnalytics = ObjC.classes.ESCoreAnalytics;
        const ESCoreAnalyticsMethods = ESCoreAnalytics.$ownMethods;
    }
}
  • Hook the methods implemented by each of the target classes
  • Instrument each by injecting a simple logging function call. This will enable us to differentiate between which functions are called and when.
  • ... and that's it! It should get us started.
// Hook all methods for the ESD class
ESDMethods.forEach(methodName => {
    const method = ESD[methodName];
    if (method && method.implementation) {
        Interceptor.attach(method.implementation, {
            onEnter: function (args) {
                console.log(`ESD [+] ${methodName}: entered`);
            }
        });
    } else {
        console.error(`Method ${methodName} not found in ESD`);
    }
});

// ESSystemExtensionClient left off...

// Hook all methods for the ESCoreAnalytics class
ESCoreAnalyticsMethods.forEach(methodName => {
    const method = ESCoreAnalytics[methodName];
    if (method && method.implementation) {
        Interceptor.attach(method.implementation, {
            onEnter: function (args) {
                console.log(`ESCoreAnalytics [+] ${methodName}: entered`);
            }
        });
    } else {
        console.error(`Method ${methodName} not found in ESCoreAnalytics`);
    }
});

Test results

System Extension installation

// Initiate install 
ESD [+] - listener:validateExtension:atTemporaryBundleURL:replyHandler:: entered
ESD [+] - sanityCheckExtensionInfo:: entered
ESD [+] - isValidBundle:: entered
// Up to "System Extension Blocked" Gatekeeper prompt

ESD [+] - listener:willStartExtension:replyHandler:: entered
ESD [+] - sanityCheckExtensionInfo:: entered
ESD [+] - listener:startExtension:replyHandler:: entered
ESD [+] - sanityCheckExtensionInfo:: entered
ESD [+] - safeSubmitJob:: entered
ESD [+] - smJobDictionary:: entered
ESD [+] - createLabelName:: entered
ESD [+] - createLabelName:: entered
ESD [+] - removeLaunchdJob:: entered
ESD [+] - submitLaunchdJob:: entered
// Up to System Extension allow in System Settings

System Extension uninstall

ESD [+] - listener:willTerminateExtension:replyHandler:: entered
ESD [+] - sanityCheckExtensionInfo:: entered
ESD [+] - listener:terminateExtension:replyHandler:: entered
ESD [+] - sanityCheckExtensionInfo:: entered
ESD [+] - safeRemoveJob:: entered
ESD [+] - createLabelName:: entered
ESD [+] - removeLaunchdJob:: entered

Client connection

ESCoreAnalytics [+] + sharedManager: entered
ESCoreAnalytics [+] - sendEvent:event:: entered

Instrumenting ESCoreAnalytics

This class fundamentally calls into CoreAnalytics.framework. However, what were interested in here is what's beng sent. It takes two integer arguments: the event ID and the event type. You can see the pseudo code generated by Hopper below.

/* @class ESCoreAnalytics */
-(int)sendEvent:(int)arg2 event:(int)arg3 {
    r0 = AnalyticsSendEvent();
    return r0;
}

Using this pseudo we're able to get going with our Frida script. First, like the above we'll want to define the classes and methods we'd like to hook. However, we now want to go a bit deeper. Since we know that the events are being defined as parameters we should be able to decode those. To do this first let's grab the method arguments:

if (ObjC.available) {
    try {
        const ESCoreAnalytics = ObjC.classes.ESCoreAnalytics;
        const sendEvent_event_ = ESCoreAnalytics['- sendEvent:event:'];

        Interceptor.attach(sendEvent_event_.implementation, {
            onEnter: function (args) {
                console.log('[+] sendEvent:event: entered');
                // Interpreting 'event' as an NSDictionary
                const eventDict = new ObjC.Object(args[3]);
            }
        });
    }
}

To make sense of the event data, we'll next iterate over the keys in eventDict. Using the allKeys() function is a convenient way to access these dictionary keys. For each key, we retrieve the corresponding value and convert it to a string. However, simply printing out the key-value pairs would give us raw data, which might be difficult to interpret. Here's where our "domain" knowledge will comes into play. For instance, if the key is 'EventType', we can infer that the value corresponds to a specific type of event being passed. By maintaining a mapping of these event types, we can translate these cryptic integer or string values into human-readable event descriptions. This is especially useful for debugging or uncovering event subscriptions made by any ES client.

const keys = eventDict.allKeys();

for (let i = 0; i < keys.count(); i++) {
    const key = keys.objectAtIndex_(i);
    const value = eventDict.objectForKey_(key);

    if (value !== null && typeof value.toString === 'function') {
        let valueString = value.toString();

        // Resolve EventType to a readable format...  an ES event!
        if (key.toString() === 'EventType' && eventTypeMapping[valueString]) {
            // `eventTypeMapping` is discussed below
            valueString = eventTypeMapping[valueString] + ` (${valueString})`;
        }

        console.log(`    ${key.toString()}: ${valueString}`);
    } else {
        console.log(`    ${key.toString()}: <null or undefined>`);
    }
}

Great! Now, we'll need to define the event ID to event type mapping. The following is abridged, but completing the code will give you full resolution. I've provided the full implementation as a Gist here: https://gist.github.com/Brandon7CC/14cd97458629ca045774cb767d476e59.

const eventTypeMapping = {
    // Complete mapping based on es_event_type_t enumeration
    0: 'ES_EVENT_TYPE_AUTH_EXEC',
    1: 'ES_EVENT_TYPE_AUTH_OPEN',
    2: 'ES_EVENT_TYPE_AUTH_KEXTLOAD',
    3: 'ES_EVENT_TYPE_AUTH_MMAP',
    4: 'ES_EVENT_TYPE_AUTH_MPROTECT',
    // ...
}

What does the end result look like? Notice that simply by pivoting off of the method's arguments with ObjC.Object(args[3]) we were able to read the dictionary being sent across to CoreAnalytics. In this case, we can see some incredible detail:

  • Type of client: sysext (System Extension)
  • The event subscription requested: ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_PETITION in this case which has ID: 129
  • The bundle ID of the ESE: com.redcanary.agent.securityextension
  • The team ID of the ESE: UA6JCQGF3F
  • and the CDHash of the Mach-O: 1c2dc75b5d568ddf7981179a8482d7b1c383d5e
[+] sendEvent:event: entered
    Endpoint Security Event subscription details:
    ClientDisposition: sysext
    EventType: ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_PETITION (129)
    BundleID: com.redcanary.agent.securityextension
    TeamID: UA6JCQGF3F
    CDHash: 1c2dc75b5d568ddf7981179a8482d7b1c383d5e