Malwarebytes | Airo AV |
# ./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"; }
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
2recordBalance = recordLength;
3
4//init processed length to start (zer0)
5processedLength = 0;
6
7//parse record
8// read all tokens/process
9while(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)
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:
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:
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
4es_client_t* endpointClient = nil;
5
6//create client
7// callback invokes (user) callback for new processes
8result = es_new_client(&endpointClient, ^(es_client_t *client, const es_message_t *message)
9{
10 //process events
11
12});
13
14//error?
15if(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.
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
2es_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
9if(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;
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.
We (just) discussed how to subscribe to events from the Endpoint Security Subsystem by invoking:
es_new_client
functiones_subscribe
functionOf 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
2es_process_t* process = NULL;
3
4// set type
5// extract (relevant) process object, etc
6switch (message->event_type) {
7
8//exec
9case ES_EVENT_TYPE_NOTIFY_EXEC:
10 process = message->event.exec.target;
11 break;
12
13//fork
14case ES_EVENT_TYPE_NOTIFY_FORK:
15 process = message->event.fork.child;
16 break;
17
18//exit
19case 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:
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
2pid_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
2NSString* 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(¤tArg);
29 if(nil != argument)
30 {
31 //append
32 [self.arguments addObject:argument];
33 }
34 }
35
36bail:
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;
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 🥳
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"
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
2ProcessMonitor* procMon = [[ProcessMonitor alloc] init];
3
4//define block
5// automatically invoked upon process events
6ProcessCallbackBlock 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:
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
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. 🥳 \