Writing a Process Monitor with Apple's Endpoint Security Framework



Today’s blog post is brought to you by:

CleanMy Mac X Malwarebytes Airo AV

Become a Friend! Our research, tools, and writing, are supported by “Friends of Objective-See”Today’s blog post is brought to you by:

# ./processMonitor Starting process monitor...[ok] PROCESS EXEC ('ES_EVENT_TYPE_NOTIFY_EXEC') pid: 7655 path: /bin/ls uid: 501 args: ( ls, "-lart", "." ) signing info: { cdHash = 5180A360C9484D61AF2CE737EAE9EBAE5B7E2850; csFlags = 603996161; isPlatformBinary = 1 (true); signatureIdentifier = "com.apple.ls"; }

Background

A common component of (many) security tools is a process monitor. As its name implies, a process monitor watches for the creation of new processes (plus extracts information such as process id, path, arguments, and code-signing information).

Many of my Objective-See tools track process creations.



Examples include:

Ransomwhere?

Tracks process creations to classify processes (as belonging to the OS/Apple, from 3rd-party developers. etc.) such that if a process beings rapidly encrypting files, Ransomwhere? can quickly determine if this encryption is legitimate or possibly ransomware.

TaskExplorer

Tracks process creations (and terminations) in order to display a real-time list of active processes to the user.

BlockBlock

Tracks process creations to map process identifiers ( pids ) reported in persistent file events to full process paths in order to provide more informative alerts to users, when persistence events occur.

After a while I got tired of including duplicative process monitoring code in each project, so decided to write a process monitoring library. Now, any tool that is interested in tracking process events can simply link against this library.

The source code for this (original) process monitoring library, can be found on the Objective-See’s github page: Proc Info

Until now, the preferred way to programmatically create a process monitor was to subscribe to events from Apple’s OpenBSM subsystem.

For a deep-dive into the OpenBSM subsystem, check out my ShmooCon talk: “Get Cozy with OpenBSM Auditing“

Though sufficient, the OpenBSM subsystem is rather painful to programmatically interface with. For starters, it requires one to parse and tokenize various (binary) audit records and audit tokens (that amongst other things contain process-related events):

1 //init (remaining) balance to record's total length 2 recordBalance = recordLength; 3 4 //init processed length to start (zer0) 5 processedLength = 0 ; 6 7 //parse record 8 // read all tokens/process 9 while ( 0 != recordBalance) 10 { 11 //extract token 12 // and sanity check 13 if ( - 1 == au_fetch_tok( & tokenStruct, recordBuffer + processedLength, recordBalance)) 14 { 15 //error 16 // skip record 17 break ; 18 } 19 20 //now parse tokens 21 // looking for those that are related to process start/terminated events 22 23 //add length of current token 24 processedLength += tokenStruct.len; 25 26 //subtract length of current token 27 recordBalance -= tokenStruct.len; 28 29 }

Moreover, the audit events delivered by the OpenBSM subsystem do not contain information about the processes code-signing identifies. Thus once you receive an audit event related to process creation, if you want to know for example, if said process is signed by Apple proper, you have to write extra code to programmaticly extract this information. This is relatively non-trivial and may be computationally (CPU) intensive.

Finally, the OpenBSM audit subsystem (by design) is reactive, meaning that by the time you’ve received the events (i.e. process creation) it’s already occurred. This runs the gamut from being mildly annoying (for example, a short-lived process may have already exited, being you cannot query it to retrieve it’s code-signing identity) to well rather problematic. For example, if you’re a writing a security tool, clearly there exist many scenarios where being proactive about process events would be ideal (i.e. blocking a piece of malware before its allowed to execute). Until now, the only way to realize proactive security protections was to live in the kernel (something that Apple is rather drastically deprecating)

Apple’s Endpoint Security Framework

With Apple’s push to kick 3rd-party developers (including security products) out of the kernel, coupled with the realization (finally!) that the existing subsystems were rather archaic and dated, Apple recently announced the new, user-mode “Endpoint Security Framework” (that provides a user-mode interface to a new “Endpoint Security Subsystem”).

As we’ll see, this framework addresses many of the aforementioned issues & shortcomings.



Specifically it provides a:

well-defined and (relatively) simple API

comprehensive process code-signing information for events

the ability to proactively respond to process events (though here, our process monitor will be passive).

I’m often somewhat critical of Apple’s security posture (or lack thereof). However, the “Endpoint Security Framework” is potentially a game-changer for those of us seeking to write robust user-mode security tools for macOS. Mahalo Apple! Personally I’m stoked 🥳

This blog is practical walk-thru of creating a process monitor which leverages Apple’s new framework. For more information on the Endpoint Security Framework, see Apple’s developer documentation: Endpoint Security Framework

In this blog, we’ll illustrate exactly how to create a comprehensive user-mode process monitor that leverages Apple’s new framework.

There are a few prerequisites to leverage the Endpoint Security Framework that include: The com.apple.developer.endpoint-security.client entitlement

This can be requested from Apple via this link. Until then (I’m still waiting 😅), give yourself that entitlement (i.e. in your app’s $(ProductName).entitlements file, and disable SIP such that it remains pseudo-unenforced). <dict>

<key> com.apple.developer.endpoint-security.client </key>

<true/>

</dict>

Xcode 11/macOS 10.15 SDK

As these are both (still) in beta, for now, it’s recommended to perform development in a virtual machine (running macOS 10.15, beta).

macOS 10.15 (Catalina)

It appears the Endpoint Security Framework will not be made available to older versions of macOS. As such, any tools the leverage this framework will only run on 10.15 or newer.

Ok enough chit-chat, let’s dive in!



Our goal is simple: create a comprehensive user-mode process monitor that leverages Apple’s new “Endpoint Security Framework”.



Besides “capturing” process events, we’re also interested in:

the process id (pid)

the process path

any process arguments

any process code-signing information

…luckily, unlike the OpenBSM subsystem, the new Endpoint Security Framework makes this a breeze!

Besides Apple’s documentation, the “Endpoint Security Demo” on github, by a developer named Omar Ikram was hugely helpful! Thanks Omar! 🙏

In order to subscribe to events from the “Endpoint Security Subsystem”, we must first create a new “Endpoint Security” client. The es_new_client function provides the interface to perform this action:

Various (well commented!) header files in the usr/include/EndpointSecurity/ directory (such as ESClient.h) are also great resources.

$ ls /Library/Developer/CommandLineTools/SDKs /MacOSX10.15.sdk/usr/include/EndpointSecurity/ ESClient.h ESMessage.h ESOpaqueTypes.h ESTypes.h EndpointSecurity.h $ less EndpointSecurity/ESClient.h struct es_client_s; /** * es_client_t is an opaque type that stores the endpoint security client state */ typedef struct es_client_s es_client_t; /** * Initialise a new es_client_t and connect to the ES subsystem * @param client Out param. On success this will be set to point to the newly allocated es_client_t. * @param handler The handler block that will be run on all messages sent to this client * @return es_new_client_result_t indicating success or a specific error. */

In code, we first include the EndpointSecurity.h file, declare a global variable (type: es_client_t* ), then invoke the es_new_client function:

1 #import <EndpointSecurity/EndpointSecurity.h> 2 3 //(global) endpoint client 4 es_client_t * endpointClient = nil; 5 6 //create client 7 // callback invokes (user) callback for new processes 8 result = es_new_client( & endpointClient, ^ (es_client_t * client, const es_message_t * message) 9 { 10 //process events 11 12 }); 13 14 //error? 15 if (ES_NEW_CLIENT_RESULT_SUCCESS != result) 16 { 17 //err msg 18 NSLog( @"ERROR: es_new_client() failed with %d" , result); 19 20 //bail 21 goto bail; 22 }

Note that the es_new_client function takes an (out) pointer to the variable of type es_client_t . Once the function returns, this variable will hold the initialized endpoint security client (required by all other endpoint security APIs). The second parameter of the es_new_client function is a block that will be automatically invoked on endpoint security events (more on this shortly!)

The es_new_client function returns a variable of type es_new_client_result_t . Peeking at the ESTypes.h reveals the possible values for this variable:

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESTypes.h /** @brief Error conditions for creating a new client */ typedef enum { ES_NEW_CLIENT_RESULT_SUCCESS, ///One or more invalid arguments were provided ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, ///Communication with the ES subsystem failed ES_NEW_CLIENT_RESULT_ERR_INTERNAL, ///The caller is not properly entitled to connect ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, ///The caller is not permitted to connect. They lack Transparency, Consent, and Control (TCC) approval form the user. ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED } es_new_client_result_t;

Hopefully these are rather self explanatory (i.e. ES_NEW_CLIENT_RESULT_SUCCESS means ok! while ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED means you don’t hold the com.apple.developer.endpoint-security.client entitlement).

If all is well, the es_new_client function will return ES_NEW_CLIENT_RESULT_SUCCESS indicating that it has created newly initialized Endpoint Security client ( es_client_t ) for us to use.

To compile the above code, link against the Endpoint Security Framework (libEndpointSecurity)

Once we’ve created an instance of a es_new_client , we now must tell the Endpoint Security Subsystem what events we are interested in (or want to “subscribe to”, in Apple parlance). This is accomplished via the es_subscribe function (documented here and in the ESClient.h header file):

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESClient.h /** * Subscribe to some set of events * @param client The client that will be subscribing * @param events Array of es_event_type_t to subscribe to * @param event_count Count of es_event_type_t in `events` * @return es_return_t indicating success or error * @note Subscribing to new event types does not remove previous subscriptions */ OS_EXPORT API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_subscribe(es_client_t * _Nonnull client, es_event_type_t * _Nonnull events, uint32_t event_count);

This function takes the initialized endpoint client (returned by the es_new_client function), an array of events of interest, and the size of said array:

1 //(process) events of interest 2 es_event_type_t events[] = { 3 ES_EVENT_TYPE_NOTIFY_EXEC, 4 ES_EVENT_TYPE_NOTIFY_FORK, 5 ES_EVENT_TYPE_NOTIFY_EXIT 6 }; 7 8 //subscribe to events 9 if (ES_RETURN_SUCCESS != es_subscribe(endpointClient, events, 10 sizeof (events) / sizeof (events[ 0 ]))) 11 { 12 //err msg 13 NSLog( @"ERROR: es_subscribe() failed" ); 14 15 //bail 16 goto bail; 17 }

The events of interest depends on well, what events are of interest to you! As we’re writing a process monitor we’re (only) interested in the following three process-related events:

ES_EVENT_TYPE_NOTIFY_EXEC

“A type that represents process execution notification events.”

ES_EVENT_TYPE_NOTIFY_FORK

“A type that represents process forking notification events.”

ES_EVENT_TYPE_NOTIFY_EXIT

“A type that represents process exit notification events.”

For a full list of events that one may subscribe to, take a look at the es_event_type_t enum in the ESTypes.h header file:

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESTypes.h /** * @brief The valid event types recognized by EndpointSecurity */ typedef enum { ES_EVENT_TYPE_AUTH_EXEC , ES_EVENT_TYPE_AUTH_OPEN , ES_EVENT_TYPE_AUTH_KEXTLOAD , ES_EVENT_TYPE_AUTH_MMAP , ES_EVENT_TYPE_AUTH_MPROTECT , ES_EVENT_TYPE_AUTH_MOUNT , ES_EVENT_TYPE_AUTH_RENAME , ES_EVENT_TYPE_AUTH_SIGNAL , ES_EVENT_TYPE_AUTH_UNLINK , ES_EVENT_TYPE_NOTIFY_EXEC , ES_EVENT_TYPE_NOTIFY_OPEN , ES_EVENT_TYPE_NOTIFY_FORK , ES_EVENT_TYPE_NOTIFY_CLOSE , ES_EVENT_TYPE_NOTIFY_CREATE , ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA , ES_EVENT_TYPE_NOTIFY_EXIT , ES_EVENT_TYPE_NOTIFY_GET_TASK , ES_EVENT_TYPE_NOTIFY_KEXTLOAD , ES_EVENT_TYPE_NOTIFY_KEXTUNLOAD , ES_EVENT_TYPE_NOTIFY_LINK , ES_EVENT_TYPE_NOTIFY_MMAP , ES_EVENT_TYPE_NOTIFY_MPROTECT , ES_EVENT_TYPE_NOTIFY_MOUNT , ES_EVENT_TYPE_NOTIFY_UNMOUNT , ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN , ES_EVENT_TYPE_NOTIFY_RENAME , ES_EVENT_TYPE_NOTIFY_SETATTRLIST , ES_EVENT_TYPE_NOTIFY_SETEXTATTR , ES_EVENT_TYPE_NOTIFY_SETFLAGS , ES_EVENT_TYPE_NOTIFY_SETMODE , ES_EVENT_TYPE_NOTIFY_SETOWNER , ES_EVENT_TYPE_NOTIFY_SIGNAL , ES_EVENT_TYPE_NOTIFY_UNLINK , ES_EVENT_TYPE_NOTIFY_WRITE , ES_EVENT_TYPE_AUTH_FILE_PROVIDER_MATERIALIZE , ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_MATERIALIZE , ES_EVENT_TYPE_AUTH_FILE_PROVIDER_UPDATE , ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_UPDATE , ES_EVENT_TYPE_AUTH_READLINK , ES_EVENT_TYPE_NOTIFY_READLINK , ES_EVENT_TYPE_AUTH_TRUNCATE , ES_EVENT_TYPE_NOTIFY_TRUNCATE , ES_EVENT_TYPE_AUTH_LINK , ES_EVENT_TYPE_NOTIFY_LOOKUP , ES_EVENT_TYPE_LAST } es_event_type_t;

Note there are two main event types: ES_EVENT_TYPE_AUTH_* and ES_EVENT_TYPE_NOTIFY_* ES_EVENT_TYPE_AUTH_*

Events that require a response before being allowed to proceed. For example, the ES_EVENT_TYPE_AUTH_EXEC will block a process execution, until the subscriber (i.e. your security tool) provides a response.

ES_EVENT_TYPE_NOTIFY_*

Events that simply notify the subscriber (e.g. they do not require a response before being allowed to proceed).



For example, the ES_EVENT_TYPE_NOTIFY_EXEC event simply notifies one that a process is (about to)execute.



In our process monitor, we only utilize ES_EVENT_TYPE_NOTIFY_* events.

These events are also succinctly described in Apple’s documentation for the es_event_type_t enumeration.

Once the es_subscribe function successfully returns ( ES_RETURN_SUCCESS ), the Endpoint Security Subsystem will start delivering events.

Event/Message Delivery

We (just) discussed how to subscribe to events from the Endpoint Security Subsystem by invoking:

es_new_client function

function es_subscribe function

Of course, we’ll want add some logic/code process received messages. Recall that the final argument of the es_new_client function is a callback block (or handler). Apple states: “The handler block…will be run on all messages sent to this client.”

The block is invoked with the endpoint client, and most importantly the message from the Endpoint Security Subsystem. This message variable is a pointer of type es_message_t (i.e. es_message_t* ).

Apple adequately “documents” the es_message_t structure in the (aptly named) ESMessage.h file, and also online.

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h /** * es_message_t is the top level datatype that encodes information sent from the ES subsystem to it's clients * Each security event being processed by the ES subsystem will be encoded in an es_message_t * A message can be an authorization request or a notification of an event that has already taken place * The action_type indicates if the action field is an auth or notify action * The event_type indicates which event struct is defined in the event union. */ typedef struct { uint32_t version; struct timespec time; uint64_t mach_time; uint64_t deadline; es_process_t * _Nullable process; uint8_t reserved[8]; es_action_type_t action_type; union { es_event_id_t auth; es_result_t notify; } action; es_event_type_t event_type; es_events_t event; uint64_t opaque[]; /* Opaque data that must not be accessed directly */ } es_message_t;

Notable members of interest include:

es_process_t * process

A pointer to a structure that describes the process responsible for the event.

es_event_type_t event_type

The type of event (that will match one of the events we subscribed to, e.g. ES_EVENT_TYPE_NOTIFY_EXEC )

event_type event

An event specific structure (i.e. es_event_exec_t exec )

Since we only subscribed to three events ( ES_EVENT_TYPE_NOTIFY_EXEC , ES_EVENT_TYPE_NOTIFY_FORK , and ES_EVENT_TYPE_NOTIFY_EXIT ) processes the received messages is fairly straight forward.

For each of these three events, we are interested in extracting a pointer to a es_process_t which will hold the information about the process (starting, forking, or terminating). Recall the es_message_t structure received in the es_new_client callback contains a member: es_process_t * process ( message->process ). However, as noted this is the process responsible for the action, which might not always be the es_process_t * we’re actual interested in. Huh?

In the case of a process exec ( ES_EVENT_TYPE_NOTIFY_EXEC ) event, the message->process will describe the process that is responsible for spawning the process. In other words, the parent. We are interested actually in the child, that is, the process that is about to be (or just was) spawned.

For example, if we hop into a terminal and run the ls command the message->process points to the shell process ( /bin/zsh ). This of course is the parent - the process responsible for executing /bin/ls :

(lldb) p message->process.executable.path (es_string_token_t) $17 = (length = 8, data = "/bin/zsh")

So how do we ‘find’ the es_process_t * that points to the child process ( /bin/ls )?

Recall the message structure contains a member named event_type In the case of a process exec this will be set to ES_EVENT_TYPE_NOTIFY_EXEC and the message->event will point to a es_event_exec_t structure (defined in ESMessage.h ):

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h typedef struct { es_process_t * _Nullable target; es_token_t args; uint8_t reserved[64]; } es_event_exec_t;

The target member of this structure contains a pointer to the es_process_t we’re interested in (i.e. the one that described /bin/ls ):

(lldb) p message->event.exec.target->executable.path (es_string_token_t) $16 = (length = 7, data = "/bin/ls")

What about the other two events we’ve subscribed to?



For ES_EVENT_TYPE_NOTIFY_FORK events, the message contains an events of type es_event_fork_t , which contains information about the child process in es_process_t * child . For ES_EVENT_TYPE_NOTIFY_EXIT events, we can simply use message->process (as the process that’s generating the exit event, is the process we’re interested in …that is to say the process that’s about to exit).

If you’re comfortable reading code, the following should now make sense:

1 //process of interest 2 es_process_t * process = NULL; 3 4 // set type 5 // extract (relevant) process object, etc 6 switch (message -> event_type) { 7 8 //exec 9 case ES_EVENT_TYPE_NOTIFY_EXEC: 10 process = message -> event.exec.target; 11 break ; 12 13 //fork 14 case ES_EVENT_TYPE_NOTIFY_FORK: 15 process = message -> event.fork.child; 16 break ; 17 18 //exit 19 case ES_EVENT_TYPE_NOTIFY_EXIT: 20 process = message -> process; 21 break ; 22 }

Now we (finally) have a pointer to the (relevant) es_process_t process structure. The definition for this structure can be found in the ESMessage.h header file:

$ less /MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h ... /** * @brief describes a process that took the action being described in an es_message_t * For exec events also describes the newly executing process * */ typedef struct { audit_token_t audit_token; pid_t ppid; pid_t original_ppid; pid_t group_id; pid_t session_id; uint32_t codesigning_flags; bool is_platform_binary; bool is_es_client; uint8_t cdhash[CS_CDHASH_LEN]; es_string_token_t signing_id; es_string_token_t team_id; es_file_t * _Nullable executable; } es_process_t;

The es_process_t structure is also documented by Apple as part of it Endpoint Security Subsystem developer documentation:

Let’s discuss various fields in the structure, as they’ll be relevant for the process monitor we’re building.

First, we’re interested in extracting the process id ( pid ) from this structure. Though the es_process_t doesn’t directly contain a process pid, it does contain an audit token (type: audit_token_t ). In the ESMessage.h header file, Apple states that: “values such as PID , UID , GID , etc. can be extraced from the audit token via API in libbsm.h .”

Specifically, we can invoke the audit_token_to_pid (passing in the audit_token member of the es_process_t structure):

1 //extract pid pid 2 pid_t pid = audit_token_to_pid(process -> audit_token);

Of course, we’re also interested in the path to the process’s executable. This is found within the executable member of the es_process_t structure. The executable is pointer to a es_file_t structure:

$ less /MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h ... /** * es_file_t provides the inode/devno and path to a file that relates to a security event * the path may be truncated, which is indicated by the path_truncated flag. */ typedef struct { es_string_token_t path; bool path_truncated; union { dev_t devno; fsid_t fsid; }; ino64_t inode; } es_file_t;

The path to the process’s executable is found in the path member of the es_file_t structure ( &process->executable->path ). Its type is es_string_token_t (defined in ESTypes.h ):

$ less /MacOSX10.15.sdk/usr/include/EndpointSecurity/ESTypes.h /** * @brief Structure for handling packed blobs of serialized data */ typedef struct { size_t length; const char * data; } es_string_token_t;

We can convert this to a more “friendly” data type such as a NSString via the following code snippet:

1 //convert to data, then to string 2 NSString * string = [NSString stringWithUTF8String:[[NSData dataWithBytes:stringToken -> data length:stringToken -> length] bytes]];

If the process event is a ES_EVENT_TYPE_NOTIFY_EXEC , the process->event member points to a es_exec_env structure, which a contains the process’s arguments ( es_event_exec_t->args ):

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h ... /** * Arguments and environment variables are packed, use the following functions to operate on this field: * `es_exec_env`, `es_exec_arg`, `es_exec_env_count`, and `es_exec_arg_count` */ typedef struct { es_process_t * _Nullable target; es_token_t args; uint8_t reserved[64]; } es_event_exec_t;

As noted in comments with the ESMessage.h header file, the arguments are packed. The following helper method (which utilizes the es_exec_arg_count and es_exec_arg ) unpacks all arguments into an array:

1 //extract/format args 2 -( void ) extractArgs: (es_events_t * )event 3 { 4 //number of args 5 uint32_t count = 0 ; 6 7 //argument 8 NSString * argument = nil; 9 10 //get # of args 11 count = es_exec_arg_count( & event -> exec); 12 if ( 0 == count) 13 { 14 //bail 15 goto bail; 16 } 17 18 //extract all args 19 for (uint32_t i = 0 ; i < count; i ++ ) 20 { 21 //current arg 22 es_string_token_t currentArg = { 0 }; 23 24 //extract current arg 25 currentArg = es_exec_arg( & event -> exec, i); 26 27 //convert argument (es_string_token_t) to string 28 argument = convertStringToken( & currentArg); 29 if (nil != argument) 30 { 31 //append 32 [self.arguments addObject:argument]; 33 } 34 } 35 36 bail: 37 38 return ; 39 }

Once we’ve extracted the process’s identifier ( pid ), path, and arguments, all that’s left is the code signing information. This is pretty trivial, as such code signing information is directly embedded in the es_process_t structure:

code signing flags:

( uint32_t ) process->codesigning_flags These are “standard” mcaOS code-signing flags, found in the cs_blobs.h file

code signing id:

( es_string_token_t ) process->signing_id This is “the identifier used to sign the process.”

team id:

( es_string_token_t ) process->team_id This is “the team identifier used to sign the process.”

cdHash:

( uint8_t array[CS_CDHASH_LEN] ) process->cdhash This is “The code directory hash value”

Below is some (well-commented) code that extracts and formats code-signing information from the es_process_t structure, into a (ns)dictionary:

1 //extract/format signing info 2 -( void ) extractSigningInfo: (es_process_t * )process 3 { 4 //cd hash 5 NSMutableString * cdHash = nil; 6 7 //signing id 8 NSString * signingID = nil; 9 10 //team id 11 NSString * teamID = nil; 12 13 //alloc string for hash 14 cdHash = [NSMutableString string]; 15 16 //add flags 17 self.signingInfo[KEY_SIGNATURE_FLAGS] = 18 [NSNumber numberWithUnsignedInt:process -> codesigning_flags]; 19 20 //convert/add signing id 21 signingID = convertStringToken( & process -> signing_id); 22 if (nil != signingID) 23 { 24 //add 25 self.signingInfo[KEY_SIGNATURE_IDENTIFIER] = signingID; 26 } 27 28 //convert/add team id 29 teamID = convertStringToken( & process -> team_id); 30 if (nil != teamID) 31 { 32 //add 33 self.signingInfo[KEY_SIGNATURE_TEAM_IDENTIFIER] = teamID; 34 } 35 36 //add platform binary 37 self.signingInfo[KEY_SIGNATURE_PLATFORM_BINARY] = 38 [NSNumber numberWithBool:process -> is_platform_binary]; 39 40 //format cdhash 41 for (uint32_t i = 0 ; i < CS_CDHASH_LEN; i ++ ) 42 { 43 //append 44 [cdHash appendFormat: @"%X" , process -> cdhash[i]]; 45 } 46 47 //add cdhash 48 self.signingInfo[KEY_SIGNATURE_CDHASH] = cdHash; 49 50 return ; 51 }

Although we’re generally more interested in process creation events, we might want also want to track process termination events ( ES_EVENT_TYPE_NOTIFY_EXIT ). When a ES_EVENT_TYPE_NOTIFY_EXIT is delivered, message->event will point to a structure of type: es_event_exit_t :

typedef struct { int stat; uint8_t reserved[64]; } es_event_exit_t;

From this structure, we can extract the process’s exit code (via the stat member):

//grab process's exit code int exitCode = message -> event.exit.stat;

Process Monitor Library

As noted, many of Objective-See’s tools track process creations, and thus currently utilize my original process monitoring library; Proc Info. This library leverages Apple’s OpenBSM subsystem, in order to provide process events. As we previously discussed, there are several complexities and limitations of the OpenBSM subsystem (most notably process events from the subsystem do not include code-signing information).

Lucky us, as shown in this blog, we can now leverage Apple’s Endpoint Security Subsystem to effectively and comprehensively monitor process events (from user-mode!).

As such, today, I’m releasing an open-source process monitoring library, that implements everything we’ve discussed here today 🥳

On github: Process Monitoring Library

It’s fairly simple to leverage this library in your own (non-commercial) tools:

Build the library, libProcessMonitor.a



Add the library and its header file ( ProcessMonitor.h ) to your project:



#import "ProcessMonitor.h"





As shown above, you’ll also have link against the libbsm (for audit_token_to_pid ) and libEndpointSecurity libraries. Add the com.apple.developer.endpoint-security.client entitlement (to your project’s $(ProductName).entitlements file).



Write some code to interface with the library!



This final steps involves instantiating a ProcessMonitor object and invoking the start method (passing in a callback block that’s invoked on process events). Below is some sample code that implements this logic:

1 //init monitor 2 ProcessMonitor * procMon = [[ProcessMonitor alloc] init]; 3 4 //define block 5 // automatically invoked upon process events 6 ProcessCallbackBlock block = ^ (Process * process) 7 { 8 switch (process.event) 9 { 10 //exec 11 case ES_EVENT_TYPE_NOTIFY_EXEC: 12 NSLog( @"PROCESS EXEC ('ES_EVENT_TYPE_NOTIFY_EXEC')" ); 13 break ; 14 15 //fork 16 case ES_EVENT_TYPE_NOTIFY_FORK: 17 NSLog( @"PROCESS FORK ('ES_EVENT_TYPE_NOTIFY_FORK')" ); 18 break ; 19 20 //exec 21 case ES_EVENT_TYPE_NOTIFY_EXIT: 22 NSLog( @"PROCESS EXIT ('ES_EVENT_TYPE_NOTIFY_EXIT')" ); 23 break ; 24 25 default : 26 break ; 27 } 28 29 //print process info 30 NSLog( @"%@" , process); 31 32 }; 33 34 //start monitoring 35 // pass in block for events 36 [procMon start:block]; 37 38 //run loop 39 // as don't want to exit 40 [[NSRunLoop currentRunLoop] run];

Once the [procMon start:block]; method has been invoked, the Process Monitoring library will automatically invoke the callback ( block ), on process events, returning a Process object.

The Process object is declared in the library’s header file; ProcessMonitor.h . This object contains information about the process (responsible for the event), including:

pid

path

ancestors

signing info

…and more!

Take a peek at the ProcessMonitor.h file for more details.

Once compiled, we’re ready to start monitoring for process events! Here for example, we run ls -lart .

# ./processMonitor ... PROCESS EXEC ('ES_EVENT_TYPE_NOTIFY_EXEC') pid: 7655 path: /bin/ls uid: 501 args: ( ls, "-lart", "." ) ancestors: ( 6818, 6817, 338, 1 ) signing info: { cdHash = 5180A360C9484D61AF2CE737EAE9EBAE5B7E2850; csFlags = 603996161; isPlatformBinary = 1 (true); signatureIdentifier = "com.apple.ls"; } PROCESS EXIT ('ES_EVENT_TYPE_NOTIFY_EXIT') pid: 7655 path: /bin/ls uid: 501 signing info: { cdHash = 5180A360C9484D61AF2CE737EAE9EBAE5B7E2850; csFlags = 603996161; isPlatformBinary = 1; signatureIdentifier = "com.apple.ls"; } exit code: 0

Once I receive the com.apple.developer.endpoint-security.client entitlement from Apple, I’ll release this pre-built binary (that links agains the “Process Monitor” framework)!

Conclusion

Previously, writing a (user-mode) process monitor for macOS was not a trivial task. Thanks to Apple’s new Endpoint Security Subsystem/Framework (on macOS 10.15+), it’s now a breeze!

In short, one simply invokes the es_new_client & es_subscribe functions, to subscribe to events of interest (recalling that the com.apple.developer.endpoint-security.client entitlement is required).

For a process monitor, we illustrated how to subscribe to the three process-related events:

ES_EVENT_TYPE_NOTIFY_EXEC

ES_EVENT_TYPE_NOTIFY_FORK

ES_EVENT_TYPE_NOTIFY_EXIT

We then showed how to extract the relevant es_process_t process structure and then parse out all relevant process meta-data such as process identifier, path, arguments, and code-signing information.

Finally we discussed an open-source process monitoring library that implements everything we’ve discussed here today. 🥳