-
Notifications
You must be signed in to change notification settings - Fork 47
6. Endpoint Security Daemon
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:]
)
- (e.g.
- ESE install / uninstall
- (e.g.
-[ESD listener:willUninstallExtension:replyHandler:]
) - TCC (Transparency, Consent, and Control) management
- (e.g.
- Registering early boot clients (those with the
NSEndpointSecurityEarlyBoot
key in theirInfo.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
- (e.g.
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
}
-
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.
-
/Library/SystemExtensions/EndpointSecurity/.started_es_jobs.plist
: Contains theCandidateCDHash
(s) of ESE(s) in the[activated enabled]
state. -
/Library/SystemExtensions/EndpointSecurity/.early_boot.plist
: Contains theCandidateCDHash
(s) of ES clients to be started before any other third party software. This is defined by the developer in theirInfo.plist
with theNSEndpointSecurityEarlyBoot
key. If enabled macOS will hold up execution of non-platform binaries until all registered early boot clients make their first subscription(s).
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
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:
-
Client initialization (e.g.
/usr/bin/eslogger
). Mostly handled bylibEndpointSecurtity.dylib
for the reasons below- Create a new with
es_client_t
- Make event subscriptions with
es_subscribe
- (optionally) apply path muting
- Create a new with
-
ESE install / uninstall. Mostly handled by
SystemExtensions.framework
- Submit the activation request
OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier identifier: String, queue: dispatch_queue_t) -> Self
OSSystemExtensionManager.sharedextensionManager.submitRequest(request)
- This request will prompt the user for approval in the form of a Gatekeeper dialog
- Submit the activation request
-
ESE connection and event subscriptions (e.g. Mac Monitor)
- 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
, andESCoreAnalytics
)
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`);
}
});
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
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