Tech Topics

macOS vs. Windows – What kernels tell you about security events: Part 2

This post continues this two-part blog series on further understanding the differences between macOS and Windows on the system level for effective endpoint security analysis. In Part 1, we covered process events. Here in Part 2, we’ll discuss file and network events. As with Part 1, my hope is to help cybersecurity professionals expand and enrich their experiences on a less familiar platform, ultimately helping them to be better prepared to face differences from past experiences.

File events


Windows file system minifilter driver is probably THE best thing Microsoft has offered so far for third-party kernel developers. It’s the best example of many things, including:

  1. Design
  2. Performance
  3. Ease of use
  4. Granular control for developers to do whatever they want on top of the file systems

Its programming interface is so friendly and well documented that it’s possible for beginner developers to spend 10% of their time and make it work for 90% of their use cases. But the remaining 10% of cases are the real challenge, and these are often unpredictable due to variance in customer environments. Also, the API is so powerful it can give developers the illusion that they’re able to achieve lots of things, which makes it easy to go down a wrong path with a dead end. Therefore, large companies spend millions of dollars and tons of time on sustaining customers before they become better at the game.

Let’s see what a minifilter driver can offer for developers. The main file operations a minifilter can intercept and control are:

What’s more, for each of the file operations, minifilter allows developers to insert actions into the pre-operation and post-operation stages. This grants developers maximum control on all things about a file in all different phases of its lifetime. Because of that, developers can create their minifilters for many different applications other than file eventing. To learn how to write minifilters, take a look at the Microsoft minifilter samples.


In the new Endpoint Security (ES) framework, file events, just like process events, are a subset of ES events. All ES events can be found in one enum definition es_event_type_t (defined in XCode’s SDK header file /Applications/

I highlight some of the file events here:


These event types contain AUTH, which means they can be used for inline prevention. 



These event types contain NOTIFY, which means they’re just notifications and cannot be used for inline prevention. 


The ES framework typically provides both an AUTH version and a NOTIFY version for the same event. However, exceptions exist, such as:


The above events are NOTIFY only. What caught my attention is the file write event notification: ES_EVENT_TYPE_NOTIFY_WRITE. In the new ES framework, file write event is notification only. Third-party developers can't authorize individual write operations. However, in ES framework's predecessor Kauth, file write was a preventable operation (passed as KAUTH_VNODE_WRITE_DATA action to vnode scope listener callback). So how does the new ES framework prevent file write? File write authorization is moved to the file open authorization stage. Third-party developers can strip off all write access to a file by using es_respond_flags_result in ES_EVENT_TYPE_AUTH_OPEN event handler, rather than authorizing individual file write operations like Kauth. So in a way, the ES framework makes the job of third-party developers easier. But developers have also lost some granular control over the files.

Compared to Windows minifilter, the control offered by the ES framework is quite minimal, and ES APIs are highly wrapped up just for ES applications. Developers cannot use the ES framework for any other purposes. To be able to use the ES framework for their applications, developers must obtain ES entitlement from Apple. Without proper entitlements, developers won’t be able to build and run their ES applications successfully.

Compared to Kauth, the ES framework is indeed an improvement in many ways. Aside from being easier to use and easier to debug, we also like the new information that Kauth doesn’t provide, such as actor process information. The macOS ES framework passes the event information to third-party ES extensions through the data structure es_message_t:

Figure 1: Information carried by es_message_t

For a full list of file event information provided by the ES framework, please refer to structure es_message_t defined in the header file: /Applications/

Here are some example file events collected by macOS FileMonitor using the new ES framework:

   "timestamp":"2020-03-26 22:03:55 +0000", 
         "signing info (reported)": 
         "signing info (computed)": 
            "signatureAuthorities":["Software Signing","Apple Code Signing             Certification Authority","Apple Root CA"] 
   "timestamp":"2020-03-23 05:04:56 +0000", 
         "signing info (reported)":         { 
         "signing info (computed)": 
            "signatureAuthorities":["Software Signing","Apple Code Signing Certification Authority","Apple Root CA"] 

For more hands-on experience, take a look at my colleague Will Yu’s blog post Mac system extensions for threat detection: Part 3 and download his sample code: SimpleEndpoint. Alternatively, you can download a macOS FileMonitor to play around with, and explore its source code.

Network events


The Windows kernel provides two main things for filtering network traffic:

  1. NDIS driver (Network Driver Interface Specification)
  2. WFP (Windows Filtering Platform)

The NDIS driver is a low-level driver that abstracts network hardware from network drivers. NDIS driver developers have to deal with many low-level details. Therefore, the NDIS driver is not ideal for high-level network eventing. WFP is a set of APIs supporting network filtering applications. WFP developers generally don’t need to worry about physical layer details. Also, WFP provides maximum flexibility to filter network traffic on different layers, which makes network eventing easy. WFP is capable of filtering network traffic on the following layers:

  • Datagram or stream layer
  • ALE layer (a unique WFP logical layer that provides network flow filtering based on the application identity)
  • TCP/UDP transport layer
  • IP (network) layer
  • Data link layer

WFP’s actual internal layering is way more granular than that. Detailed filtering layer information can be found in Microsoft's documentation. WFP can be used to examine, block, or modify network traffic on various layers. It’s a powerful tool to create innovative network applications. To learn how to write WFP drivers or applications, take a look at the Microsoft network samples.


Network kernel extension

Before macOS Catalina, network kernel extension (NKE) was the only supported way to filter network traffic. I briefly describe it here so that later in the Network Extension section you can see Apple’s evolution on this subject. NKE provides three filter layers:

  1. Socket filter - socket layer
  2. IP filter - IP layer
  3. Interface filter - interface layer

These filters are not limited to monitoring network events — they also allow developers to modify network events, such as delay and re-inject packets, or redirect packets. Below is a list of the main events they can filter:

 Socket layer events:
  • connect_in/out
  • bind
  • listen
  • data_in/out
  • Notification events:
    • sock_evt_connected
    • sock_evt_bound
    • sock_evt_disconnecting
    • sock_evt_disconnected

Socket filter provides: socket layer packets (TCP/UDP packets, without protocol headers)

Metadata associated with socket layer packets: 

  • Socket information in socket_t, which includes domain, type, and protocol 
  • Source or destination information in struct sockaddr that includes IP address/port
IP layer events:
  • input
  • output

IP filter provides: incoming and outgoing IP layer packets

Metadata associated with IP layer packets: interface information from mbuf_t header in the incoming direction

Interface layer events:
  • input
  • output

Interface filter provides: incoming and outgoing data link layer packets

Metadata associated with data link layer packets: network interface in an opaque type ifnet_t, protocol information in protocol_family_t

Network Extension

Network Extension is a type of system extension that’s meant to replace NKE. Compared to Microsoft WFP, or its Apple predecessor NKE, the new Network Extension framework is incredibly simple:  

  1. APIs and data structures are highly wrapped up. They hide as many details as possible. They are made for these specific applications:

Developers need to obtain proper entitlements from Apple to be able to use these APIs and develop such applications.

  1. Network Extension supports fewer filtering layers. IP layer filters are gone, which can mean more low-level details for third-party developers to handle, if they have to work on the packet layer.
  2. Developers’ ability to modify traffic is restricted to predefined actions.

Of all the network applications Network Extension supports, content filters typically provide the basic flow-level network events that security vendors collect. We’ll take a closer look at the content filter Network Extension.

A content filter Network Extension offers network traffic filtering at following layers:

  1. TCP/UDP flow layer (NEFilterDataProvider)
  2. Packet layer (NEFilterPacketProvider)

The following figure shows their packet inspection points relative to the TCP/IP protocol stack.

Figure 2: Content filter's packet inspection points relative to the TCP/IP protocol stack

TCP/UDP flow layer

This is an application-oriented layer. 

TCP/UDP flow layer events:

  • TCP/UDP flow established
  • TCP/UDP flow layer inbound traffic
  • TCP/UDP flow layer outbound traffic

Metadata associated with flow-layer traffic (provided via NEFilterSocketFlow):

  • remoteEndpoint
    • hostname
    • port
  • localEndpoint
    • hostname
    • port
  • Traffic direction (in/out)
  • sourceAppAuditToken, which can be used to query following acting process information:
    • pid - acting process ID
    • auid - acting process’s audit user ID
    • ruid - acting process’s real user ID
    • rgid - acting process real group ID
    • euid - acting process’s effective user ID
    • egid - acting process’s effective group ID
    • asid - acting process’s audit session ID

Developers are given read-only access to flows. You can’t modify any aspect of a flow, including any of the flow’s data. The actions you can take on a flow are defined by a verdict —  NEFilterNewFlowVerdict or NEFilterDataVerdict — which you return to NEFilterDataProvider through its overridden methods in your subclass. The main supported actions are:

Packet layer

NEFilterPacketProvider provides data link layer packets to applications. 

The metadata it provides is really simple:

  • Network interface information in nw_interface_t
  • Traffic direction (in/out)

Supported actions:

  • Allow
  • Drop
  • Delay

These actions are defined by enum type: NEFilterPacketProviderVerdict

NEFilterPacketProvider isn’t ideal for high-level network eventing, but it's a good place to collect pcaps. To enrich the limited online system extension sample code resource, I have created a simple packet capture program called SimplePcap using Objective-C based on Network Extension.


In this two-part series, we have compared the two operating systems, macOS and Windows, on how they support the basic operating system events: process events, file events, and network events. We provided detailed information for each of those areas and key differences were summarized. A sample based on the new macOS Network Extension is provided as a supplement to the limited online sample resources for system extensions.

After witnessing the two operating systems’ evolution through the years, I came away with a couple key takeaways. For macOS, I want to echo what we mentioned in the beginning of Part 1: Apple takes lots of things as their own responsibility instead of sharing them with third-party developers. Their new highly wrapped-up APIs reflect only the message they’ve always been sending: focus on what you do best. In other words, mind your own business.

Windows, on the other hand, is going to continue sharing responsibilities with third-party developers by providing strong programming support for them. However, unlike the pre-Windows 10 era, controls have been put in place. Microsoft has put checks on programs and more are coming. I believe one day Windows will be able to disable rogue programs with a mere flip of a switch.

If you want to protect your organization from malicious activity occurring in either environment, Elastic Security integrates leading endpoint protection with Elasticsearch-powered SIEM technology for rapid, scalable defense.


es_event_type_t Defined in ESTypes.h, as of Xcode 11.5 (11E608c)

 * The valid event types recognized by EndpointSecurity
 * @discussion When a program subscribes to and receives an AUTH-related event, it must respond
 * with an appropriate result indicating whether or not the operation should be allowed to continue.
 * The valid API options are:
 * - es_respond_auth_result
 * - es_respond_flags_result
 * Currently, only ES_EVENT_TYPE_AUTH_OPEN must use es_respond_flags_result. All other AUTH events
 * must use es_respond_auth_result.
typedef enum {
    // The following events are available beginning in macOS 10.15
    // The following events are available beginning in macOS 10.15.1
// The following events are available beginning in macOS 10.15.4
    // ES_EVENT_TYPE_LAST is not a valid event type but a convenience
    // value for operating on the range of defined event types.
    // This value may change between releases and was available
    // beginning in macOS 10.15
} es_event_type_t;

es_message_t Defined in ESMessage.h, as of Xcode 11.5 (11E608c)

 * @brief es_message_t is the top level datatype that encodes information sent
 * from the ES subsystem to its 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.
 * @field version Indicates the message version; some fields are not available
 *        and must not be accessed unless the message version is equal to or
 *        higher than the message version at which the field was introduced.
 * @field time The time at which the event was generated.
 * @field mach_time The Mach time at which the event was generated.
 * @field deadline The Mach time before which an auth event must be responded to.
 *        If a client fails to respond to auth events prior to the `deadline`, the client will be killed.
 * @field process Describes the process that took the action.
 * @field seq_num Per-client, per-event-type sequence number that can be
 *        inspected to detect whether the kernel had to drop events for this
 *        client.  When no events are dropped for this client, seq_num
 *        increments by 1 for every message of that event type.  When events
 *        have been dropped, the difference between the last seen sequence
 *        number of that event type plus 1 and seq_num of the received message
 *        indicates the number of events that had to be dropped.
 *        Dropped events generally indicate that more events were generated in
 *        the kernel than the client was able to handle.
 *        Field available only if message version >= 2.
 * @field action_type Indicates if the action field is an auth or notify action.
 * @field action For auth events, contains the opaque auth ID that must be
 *        supplied when responding to the event.  For notify events, describes
 *        the result of the action.
 * @field event_type Indicates which event struct is defined in the event union.
 * @field event Contains data specific to the event type.
 * @field opaque Opaque data that must not be accessed directly.
 * @note For events that can be authorized there are unique NOTIFY and AUTH
 * event types for the same event data, eg: event.exec is the correct union
 * types.
 * @note For fields marked only available in specific message versions, all
 * access must be guarded at runtime by checking the value of the message
 * version field, e.g.
 * ```
 * if (msg->version >= 2) {
 *     acl = msg->event.create.acl;
 * }
 * ```
typedef struct {
    uint32_t version;
    struct timespec time;
    uint64_t mach_time;
    uint64_t deadline;
    es_process_t * _Nonnull process;
    uint64_t seq_num; /* field available iff message version >= 2 */
    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;
  • We're hiring

    Work for a global, distributed team where finding someone like you is just a Zoom meeting away. Flexible work with impact? Development opportunities from the start?