31

Writing a File Monitor with Apple's Endpoint Security Framework

 4 years ago
source link: https://www.tuicool.com/articles/rQ7naem
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Writing a File Monitor with Apple's Endpoint Security Framework

September 17, 2019

Our research, tools, and writing, are supported by “Friends of Objective-See”

Today’s blog post is brought to you by:

nEfMZzV.png!web
eiMVnuM.png!webrq6VnuQ.png!web

Become a Friend!

# ./fileMonitor
Starting file monitor...[ok]

FILE CREATE ('ES_EVENT_TYPE_NOTIFY_CREATE')

source path: (null)
destination path: /private/tmp/test

process: pid: 849
path: /usr/bin/touch
uid: 501
signing info: {
    cdHash = 818C29925EE42814EFA951413B713788AD62;
    csFlags = 603996161;
    isPlatforBinary = 1;
    signatureIdentifier = "com.apple.touch";
}

On github:

File Monitoring Library

Background

Earlier this week, I posted a blog titled “ Writing a Process Monitor with Apple’s Endpoint Security Framework .” In this post we (rather thoroughly) discussed a new framework/subsystem introduced into macOS Catalina (10.15): “Endpoint Security”

Moreover, we detailed exactly how to build a comprehensive (user-mode) process monitor that leveraged this new framework (and posted the full-source online ).

This blog post assumes you’ve read the previouspost, or have a solid understanding of the new Endpoint Security framework.

As such, here, we won’t be covering any foundational details about the Endpoint Security framework/subsystem.

A common component of (many) security tools is a file monitor. As its name implies, a file monitor watches for the file I/O events (plus generally extracts information about the process responsible for said file event).

Many of my Objective-See tools contain such a file monitor component and track file events.

Examples include:

  • Ransomwhere? Tracks file creations to detect the rapid creation of encrypted files by untrusted processes (read: ransomware).

  • BlockBlock Tracks file creations and modifications in order to detect and alert on persistence events (such as malware installation).

Until now, the preferred way to programmatically create a file monitor in user-mode was to subscribe to events from the FSEvents character device ( /dev/fsevents ):

1open("/dev/fsevents", O_RDONLY);

Why not use Apple’s “ File System Events ” API? Because (AFAIK) this API provides no way to identify the process that generated the file I/O event.

(I posted a question about this on StackOverflow, “ FSEvents - get PID of the process that performed the operation? ” in 2014 …but nobody provided an answer :(

j6ZFNrJ.png!web

Though directly reading file events off /dev/fsevents , is sufficient (that is to say it provides notifications about file events, and includes the pid of the responsible process) it suffers from various drawbacks and limitations.

First, Apple actually discourages it use (as noted in the bsd/vfs/vfs_fsevents.c file):

 1if (!strncmp(watcher->proc_name, "fseventsd", sizeof(watcher->proc_name)) ||
 2    !strncmp(watcher->proc_name, "coreservicesd", sizeof(watcher->proc_name)) ||
 3    !strncmp(watcher->proc_name, "mds", sizeof(watcher->proc_name))) {
 4
 5  watcher->flags |= WATCHER_APPLE_SYSTEM_SERVICE;
 6
 7} else {
 8  
 9  printf("fsevents: watcher %s (pid: %d) -
10         Using /dev/fsevents directly is unsupported. Migrate to FSEventsFramework\n",
11         watcher->proc_name, watcher->pid);
12}

Second, it is rather painful to programmatically interface with, as it it requires one to parse and tokenize various (binary) file events:

 1//skip over args to get to next event struct
 2-(NSString*)advance2Next:(unsigned char*)ptrBuffer currentOffsetPtr:(int*)ptrCurrentOffset
 3{
 4    //path
 5    NSString* path = nil;
 6
 7    int arg_len = 0;
 8    unsigned short *argLen;
 9    unsigned short *argType;
10    struct kfs_event_a *fse;
11    struct kfs_event_arg *fse_arg;
12  
13    fse = (struct kfs_event_a *)(unsigned char*)
14          ((unsigned char*)ptrBuffer + *ptrCurrentOffset);
15  
16    //handle dropped events
17    if(fse->type == FSE_EVENTS_DROPPED)
18    {
19        //err msg
20        logMsg(LOG_ERR, @"file-system events dropped by kernel");
21        
22        //advance to next
23        *ptrCurrentOffset += sizeof(kfs_event_a) + sizeof(fse->type);
24        
25        //exit early
26        return nil;
27    }
28    
29    *ptrCurrentOffset += sizeof(struct kfs_event_a);
30    fse_arg = (struct kfs_event_arg *)&ptrBuffer[*ptrCurrentOffset];
31    
32    //save path
33    path = [NSString stringWithUTF8String:fse_arg->data];
34    
35    //skip over path
36    *ptrCurrentOffset += sizeof(kfs_event_arg) + fse_arg->pathlen ;
37    
38    argType = (unsigned short *)(unsigned char*)
39              ((unsigned char*)ptrBuffer + *ptrCurrentOffset);
40    argLen  = (unsigned short *) (ptrBuffer + *ptrCurrentOffset + 2);
41    
42    (*argType ==  FSE_ARG_DONE) ? arg_len = 0x2 : arg_len = (4 + *argLen);
43
44    *ptrCurrentOffset += arg_len;
45
46    ...

Finally, (and most problematic) though the file events delivered via /dev/fsevents contain information about the process responsible for generating the file event ( struct kfs_event_a ), this information is simply a process identifier ( pid ):

1typedef struct kfs_event_a {
2    uint16_t type;
3    uint16_t refcount;
4    pid_t    pid;
5} kfs_event_a;

When building a comprehensive file monitor (especially as part of a security tool), one generally requires more information about the responsible process, such as its path and code-signing information.

Generating code-signing process via pid, is (somewhat) non-trivial and may also be rather computationally (CPU) intensive.

Although there exist APIs (such as proc_pidpath ) to generate more comprehensive process information solely from a pid , such APIs unsurprisingly fail if the process as (already) terminated. As there is some inherent delay in file events delivered via /dev/fsevents , this is actually not uncommon (think malware installers that simply persist a binary then (quickly) exit).

Worse, if the pid is reused one may actually mis-identify the process that generated the file event. For a security product, this is rather unacceptable! (For other “issues” with pids see: “ Don’t Trust the PID! ”).

It is also possible to receive file I/O events via the OpenBSM subsystem. However, there are limitations to this approach as well, as we highlighted in previous blogpost.

As such, until now, the only way to realize a truly effective file monitor was via code running in ring-0 (the kernel).

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 (including code-signing), information for all events
  • the ability to proactively respond to file events (though here, our file 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 file 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 file monitor that leverages Apple’s new framework.

As noted in our previous blogpost 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 :sweat_smile:), give yourself that entitlement (i.e. in your app’s Info.plist 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 file monitor that leverages Apple’s new “Endpoint Security Framework”.

Besides “capturing” file I/O events, we’re also interested in:

  • the type of event (create, write, etc.)

  • the path(s) of the file ((possibly) source and destination)

  • the process responsible for the event, including its:

    • process id (pid)

    • process path

    • any process code-signing information

…luckily, unlike reading events off /dev/fsevents the new Endpoint Security framework makes this a breeze!

As noted in our previous blogpost, 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:

nY36baR.png!web

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
 8es_new_client(&endpointClient, ^(es_client_t *client, const es_message_t *message)
 9{
10    //process events
11
12});

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!)

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

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

e67BRzF.png!web

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):

yQfY7zA.png!web

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_CREATE,
 4    ES_EVENT_TYPE_NOTIFY_WRITE,
 5    ...
 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 file monitor we’re (only) interested in file-related events such as:

  • ES_EVENT_TYPE_NOTIFY_CREATE “A type that represents file creation notification events.”

  • ES_EVENT_TYPE_NOTIFY_OPEN “A type that represents file opening notification events.”

  • ES_EVENT_TYPE_NOTIFY_WRITE “A type that represents file writing notification events.”

  • ES_EVENT_TYPE_NOTIFY_CLOSE “A type that represents file closing notification events.”

  • ES_EVENT_TYPE_NOTIFY_RENAME “A type that represents file renaming notification events.”

  • ES_EVENT_TYPE_NOTIFY_LINK “A type that represents link creation notification events.”

  • ES_EVENT_TYPE_NOTIFY_UNLINK “A type that represents link unlinking notification events.”

See Apple’s documentation for other events types in the es_event_type_t enumeration.

Once the es_subscribe function successfully returns ( ES_RETURN_SUCCESS ), the Endpoint Security subsystem will start delivering messages (read: file events).

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* ).

Notable members of the es_message_t include:

  • es_process_t * process A pointer to a structure that describes the process responsible for the file 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_CREATE )

  • event_type event An event specific structure (i.e. es_event_create_t )

Though the event member of the message structure ( message->event ) is event specific, for all file events it is largely the same. For example, compare the es_event_write_t and es_event_create_t structures:

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h

typedef struct {
        es_file_t * _Nullable target;
        uint8_t reserved[64];
} es_event_write_t;

typedef struct {
        es_file_t * _Nullable target;
        uint8_t reserved[64];
} es_event_create_t;

Both contain a pointer to a es_file_t structure, which contains a path to the file (created, written to, etc.):

$ less MacOSX10.15.sdk/usr/include/EndpointSecurity/ESMessage.h

/**
 * es_file_t provides the inode/devno & 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;

Note however, some file events (such as the es_event_rename_t event) involve both a source and destination file:

typedef struct {
  es_file_t * _Nullable source;
  es_destination_type_t destination_type;
  union {
    es_file_t * _Nullable existing_file;
    struct {
            es_file_t * _Nullable dir;
            es_string_token_t filename;
    } new_path;
  } destination;
  uint8_t reserved[64];
} es_event_rename_t;

All file events messages also contain a pointer to a es_process_t structure ( message->process ) for the responsible process (i.e. that generated the file event). As detailed in our previouspost, this structure contains full process information (including pid, path, and code-signing information).

At this point we’re stoked as we’re receiving all file events along with full details about the responsible process. (No more worrying about pid lookups failing or returning the incorrect process!) :sweat_smile:

Let’s now look illustrate this in code.

First, in the es_new_client message callback, we instantiate a (custom) File object, passing in the received es_message_t message:

 1//create client
 2// callback invoked on file events
 3es_new_client(&endpointClient, ^(es_client_t *client, const es_message_t *message)
 4{
 5    //new file obj
 6    File* file = nil;
 7    
 8    //init file obj
 9    file = [[File alloc] init:(es_message_t* _Nonnull)message];
10    if(nil != file)
11    {
12        //invoke user callback
13        callback(file);
14    }
15});

This (custom) File object’s init: method simply parses out relevant information from the es_process_t structure (such as process id, path, and code-signing information as detailed in our previouspost), and then extracts the file path(s):

1//set process
2self.process = [[Process alloc] init:message];
3
4//extract path(s)
5// logic is specific to event
6[self extractPaths:message];

The extractPaths: method contains event specific logic (as recall some, but not all, file events contain both a source and destination path):

 1//extract source & destination path
 2// this requires event specific logic
 3-(void)extractPaths:(es_message_t*)message
 4{
 5  //event specific logic
 6  switch (message->event_type) {
 7      
 8    //create
 9    case ES_EVENT_TYPE_NOTIFY_CREATE:
10        self.destinationPath = convertStringToken(&message->event.create.target->path);
11        break;
12        
13    //write
14    case ES_EVENT_TYPE_NOTIFY_WRITE:
15        self.destinationPath = convertStringToken(&message->event.write.target->path);
16        break;
17        
18    ...
19        
20    //rename
21    case ES_EVENT_TYPE_NOTIFY_RENAME:
22            
23        //set (src) path
24        self.sourcePath = convertStringToken(&message->event.rename.source->path);
25        
26        //existing file ('ES_DESTINATION_TYPE_EXISTING_FILE')
27        if(ES_DESTINATION_TYPE_EXISTING_FILE == message->event.rename.destination_type)
28        {
29            //set (dest) file
30            self.destinationPath = convertStringToken(&message->event.rename.destination.existing_file->path);
31        }
32        //new path ('ES_DESTINATION_TYPE_NEW_PATH')
33        else
34        {
35            //set (dest) path
36            // combine dest dir + dest file
37            self.destinationPath = [convertStringToken(&message->event.rename.destination.new_path.dir->path) stringByAppendingPathComponent:convertStringToken(&message->event.rename.destination.new_path.filename)];
38        }
39        
40        break;
41          
42      ...
43  }
44  
45  return;
46}

Once the File object’s init: method returns, we have a comprehensive (and fully parsed) representation of the reported file event.

File Monitor Library

As noted, several of Objective-See’s tools track file events but currently do so via inefficient and (now) antiquated means.

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

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

On github:

File Monitoring Library

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

  1. Build the library, libFileMonitor.a
  2. Add the library and its header file ( FileMonitor.h ) to your project:

    #import "FileMonitor.h" Rfa2meY.png!web

    As shown above, you’ll also have link against the libbsm (for audit_token_to_pid ) and libEndpointSecurity libraries.

  3. Add the com.apple.developer.endpoint-security.client entitlement (to your project’s Info.plist file).

    3aiAjiF.png!web
  4. Write some code to interface with the library!

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

 1//init monitor
 2FileMonitor* fileMon = [[FileMonitor alloc] init];
 3
 4//define block
 5// automatically invoked upon file events
 6FileCallbackBlock block = ^(File* file)
 7{
 8  switch(file.event)
 9  {
10    //create
11    case ES_EVENT_TYPE_NOTIFY_CREATE:
12        NSLog(@"FILE CREATE ('ES_EVENT_TYPE_NOTIFY_CREATE')");
13        break;
14
15    //write
16    case ES_EVENT_TYPE_NOTIFY_WRITE:
17        NSLog(@"FILE WRITE ('ES_EVENT_TYPE_NOTIFY_WRITE')");
18        break;
19
20    //print info
21    NSLog(@"%@", file);
22};
23
24//start monitoring
25// pass in block for events
26[fileMon start:block];
27
28//run loop
29// as don't want to exit
30[[NSRunLoop currentRunLoop] run];

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

The File object is declared in the library’s header file; FileMonitor.h . This object contains information about the file event ((possibly) source and destination path) and the process responsible for the event (in a Process object). Take a peek at the FileMonitor.h file for more details.

Once compiled, we’re ready to start monitoring for file events!

For example, we run: $ echo "objective-see rules" > /tmp/test , which generates an open, write, and close file I/O events:

# ./fileMonitor
Starting file monitor...[ok]

FILE OPEN ('ES_EVENT_TYPE_NOTIFY_OPEN')
source path: (null)
destination path: /private/tmp/test

process: pid: 649
path: /bin/zsh
uid: 501
signing info: {
    cdHash = BD67298030CA90256B3999A118DCF2FFE5352A9E;
    csFlags = 603996161;
    isPlatforBinary = 1;
    signatureIdentifier = "com.apple.zsh";
}

FILE WRITE ('ES_EVENT_TYPE_NOTIFY_WRITE')
source path: (null)
destination path: /private/tmp/test

process: pid: 649
path: /bin/zsh
uid: 501
signing info: {
    cdHash = BD67298030CA90256B3999A118DCF2FFE5352A9E;
    csFlags = 603996161;
    isPlatforBinary = 1;
    signatureIdentifier = "com.apple.zsh";
}

FILE CLOSE ('ES_EVENT_TYPE_NOTIFY_CLOSE')
source path: (null)
destination path: /private/tmp/test

process: pid: 649
path: /bin/zsh
uid: 501
signing info: {
    cdHash = BD67298030CA90256B3999A118DCF2FFE5352A9E;
    csFlags = 603996161;
    isPlatforBinary = 1;
    signatureIdentifier = "com.apple.zsh";
}

Conclusion

Previously, writing a (user-mode) file monitor for macOS was not a trivial task. Thanks to Apple’s new Endpoint Security framework/subsystem (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 file monitor, we illustrated how to subscribe to the file-related events such as:

  • ES_EVENT_TYPE_NOTIFY_CREATE

  • ES_EVENT_TYPE_NOTIFY_OPEN

  • ES_EVENT_TYPE_NOTIFY_WRITE

We then showed how to extract the relevant file and (responsible) process structures and parse out all relevant meta-data.

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

:heart: Love these blog posts and/or want to support my research and tools?
You can support them via my Patreon page!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK