-
-
Save tommy-dong/9bce6907c4bf6b12bc4cb07bcfb95478 to your computer and use it in GitHub Desktop.
A demo of using Apple's EndpointSecurity framework - tested on macOS Monterey 12.2.1 (21D62)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.apple.developer.endpoint-security.client</key> | |
<true/> | |
</dict> | |
</plist> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// main.m | |
// EndpointSecurityDemo | |
// | |
// Created by Omar Ikram on 17/06/2019. | |
// | |
#import <Foundation/Foundation.h> | |
#import <EndpointSecurity/EndpointSecurity.h> | |
#import <bsm/libbsm.h> | |
#import <signal.h> | |
/* | |
A demo of using Apple's new EndpointSecurity framework - tested on macOS Catalina 10.15 Beta 1 | |
(19A471t) | |
At the time of writing, Apple's documentation is a bit lacking: | |
https://developer.apple.com/documentation/endpointsecurity/ | |
Fortunately the headers provide enough information to build a working implementation. | |
Disclaimer: | |
This code is provided as is and is only intended to be used for illustration purposes. This code is | |
not production-ready and is not meant to be used in a production environment. Use it at your own risk! | |
Setup: | |
1. Build on Xcode 11, with macOS deployment target set to 10.15. | |
2. Codesign with entitlement 'com.apple.developer.endpoint-security.client'. | |
Runtime: | |
1. Test environment should be a macOS 10.15 machine which has SIP disabled (best to use a VM). | |
2. Run the demo binary in a terminal as root (e.g. with sudo). | |
3. Terminal will display messages related to subscribed events. | |
4. The demo will block the top binary and Calculator app bundle from running. | |
5. CTL-C to exit. | |
Known Issues: | |
Apple has documented known issues with EndpointSecurity in the macOS Catalina 10.15 Beta Release | |
Notes: | |
https://developer.apple.com/documentation/macos_release_notes/macos_catalina_10_15_beta_release_notes | |
Other Issues: | |
1. Calling es_delete_client will cause a kernel panic (from user mode!). | |
2. AUTH decsions appear to be cached between program invocations. This can be resolved by calling | |
es_clear_cache to explicitly clear the cache. | |
3. Type discrepancy in extracting command line arguments. es_exec_arg_count returns uint64_t, but | |
es_exec_arg only takes uint32_t. | |
*/ | |
#pragma mark - Logging | |
#define BOOL_VALUE(x) x ? "Yes" : "No" | |
#define LOG_INFO(fmt, ...) NSLog(@#fmt, ##__VA_ARGS__) | |
#define LOG_ERROR(fmt, ...) NSLog(@"ERROR: " @#fmt, ##__VA_ARGS__) | |
NSString* event_type_str(const es_event_type_t event_type) { | |
// This can be expanded to include the other ES_EVENT_TYPE_* constants | |
switch(event_type) { | |
case ES_EVENT_TYPE_AUTH_EXEC: return @"ES_EVENT_TYPE_AUTH_EXEC"; | |
case ES_EVENT_TYPE_NOTIFY_FORK: return @"ES_EVENT_TYPE_NOTIFY_FORK"; | |
default: return [NSString stringWithFormat:@"Unknown/Unsupported event type: %d", event_type]; | |
} | |
} | |
void log_proc(const NSString* header, const es_process_t *proc) { | |
LOG_INFO("%@:", header); | |
LOG_INFO(" proc.pid: %d", audit_token_to_pid(proc->audit_token)); | |
LOG_INFO(" proc.ppid: %d", proc->ppid); | |
LOG_INFO(" proc.original_ppid: %d", proc->original_ppid); | |
LOG_INFO(" proc.is_platform_binary: %s", BOOL_VALUE(proc->is_platform_binary)); | |
LOG_INFO(" proc.is_es_client: %s", BOOL_VALUE(proc->is_es_client)); | |
LOG_INFO(" proc.signing_id_truncated: %s", BOOL_VALUE(proc->signing_id_truncated)); | |
LOG_INFO(" proc.team_id_truncated: %s", BOOL_VALUE(proc->team_id_truncated)); | |
LOG_INFO(" proc.signing_id: %s", proc->signing_id); | |
LOG_INFO(" proc.team_id: %s", proc->team_id); | |
// proc.cdhash | |
NSMutableString *hash = [NSMutableString string]; | |
for(uint32_t i = 0; i < CS_CDHASH_LEN; i++) { | |
[hash appendFormat:@"%x", proc->cdhash[i]]; | |
} | |
LOG_INFO(" proc.cdhash: %@", hash); | |
LOG_INFO(" proc.file.path: %s", proc->file.path); | |
} | |
void log_event_message(const es_message_t *msg) { | |
LOG_INFO("--- EVENT MESSAGE ----"); | |
LOG_INFO("event_type: %@ (%d)", event_type_str(msg->event_type), msg->event_type); | |
LOG_INFO("time: %lld.%.9ld", (long long) msg->time.tv_sec, msg->time.tv_nsec); | |
LOG_INFO("mach_time: %lld", (long long) msg->mach_time); | |
log_proc(@"proc", &msg->proc); | |
// Type specific logging | |
switch(msg->event_type) { | |
case ES_EVENT_TYPE_AUTH_EXEC: { | |
log_proc(@"event.exec.proc", &msg->event.exec.proc); | |
// Log program arguments | |
uint64_t argCount = es_exec_arg_count(&msg->event.exec); | |
LOG_INFO("event.exec.arg_count: %llu", argCount); | |
// Extract each argument and log it out | |
for(uint32_t i = 0; i < argCount; i++) { | |
// es_exec_arg_count returns uint64_t, but es_exec_arg only takes uint32_t? | |
es_token_t arg = es_exec_arg(&msg->event.exec, i); | |
NSData *argData = [NSData dataWithBytes:arg.data length:arg.size]; | |
LOG_INFO("arg %d: %@", i, [NSString stringWithUTF8String:[argData bytes]]); | |
} | |
} | |
break; | |
case ES_EVENT_TYPE_NOTIFY_FORK: { | |
log_proc(@"event.fork.child", &msg->event.fork.child); | |
} | |
break; | |
case ES_EVENT_TYPE_LAST: | |
default: { | |
// Not interested | |
} | |
} | |
LOG_INFO(""); | |
} | |
#pragma mark - Endpoint Secuirty Demo | |
es_client_t *g_client = nil; | |
NSSet *g_blockedPaths = nil; | |
// Clean-up before exiting | |
void sig_handler(int sig) { | |
LOG_INFO("Tidying Up"); | |
if(g_client) { | |
es_unsubscribe_all(g_client); | |
//es_delete_client(g_client); // CRASHES THE BOX! (´・_・`) | |
} | |
LOG_INFO("Exiting"); | |
exit(EXIT_SUCCESS); | |
} | |
// Simple handler to make AUTH (allow or block) decisions. | |
// Returns either an ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY. | |
es_auth_result_t auth_event_handler(const es_message_t *msg) { | |
if(ES_EVENT_TYPE_AUTH_EXEC == msg->event_type) { | |
NSString *path = [NSString stringWithUTF8String:msg->event.exec.proc.file.path]; | |
// Block if path is in our blocked paths list | |
if([g_blockedPaths containsObject:path]) { | |
LOG_INFO("BLOCKING: %@", path); | |
return ES_AUTH_RESULT_DENY; | |
} | |
} | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
int main(int argc, const char * argv[]) { | |
signal(SIGINT, &sig_handler); | |
@autoreleasepool { | |
// List of paths to be blocked. | |
// For this demo we will block the top binary and Calculator app bundle. | |
g_blockedPaths = [NSSet setWithObjects: | |
@"/usr/bin/top", | |
@"/System/Applications/Calculator.app/Contents/MacOS/Calculator", | |
nil]; | |
// Create a new client with an associated event message handler. | |
// Requires 'com.apple.developer.endpoint-security.client' entitlement. | |
es_new_client_result_t res = | |
es_new_client(&g_client, ^(es_client_t *clt, const es_message_t *msg) { | |
log_event_message(msg); | |
// Handle subscribed AUTH events: | |
// For 'ES_EVENT_TYPE_AUTH_EXEC' events, the associated app will block until | |
// an es_auth_result_t response is sent or this app exits. | |
if(ES_ACTION_TYPE_AUTH == msg->action_type) { | |
es_respond_result_t res = | |
es_respond_auth_result(clt, | |
msg, | |
auth_event_handler(msg), | |
false // Cache flag doesn't work in 10.15 Beta 1 | |
); | |
if(ES_RESPOND_RESULT_SUCCESS != res) { | |
LOG_ERROR("es_respond_auth_result: %d", res); | |
} | |
} | |
}); | |
if(ES_NEW_CLIENT_RESULT_SUCCESS != res) { | |
if(ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED == res) { | |
LOG_ERROR("Application requires 'com.apple.developer.endpoint-security.client' entitlement"); | |
} else if(ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED == res) { | |
LOG_ERROR("Application needs to run as root (and SIP disabled)."); | |
} else { | |
LOG_ERROR("es_new_client: %d", res); | |
} | |
return 1; | |
} | |
// Subscribe to the events we're interested in | |
bool subscribed = es_subscribe(g_client, | |
2, // The number of event types being subscribed to | |
ES_EVENT_TYPE_AUTH_EXEC, | |
ES_EVENT_TYPE_NOTIFY_FORK | |
); | |
if(!subscribed) { | |
LOG_ERROR("es_subscribe: FALSE"); | |
return 1; | |
} | |
// Cache needs to be explicitly cleared between program invocations | |
es_clear_cache_result_t resCache = es_clear_cache(g_client); | |
if(ES_CLEAR_CACHE_RESULT_SUCCESS != resCache) { | |
LOG_ERROR("es_clear_cache: %d", resCache); | |
return 1; | |
} | |
dispatch_main(); | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment