Mac system extensions for threat detection: Part 1

When it comes to having visibility and detecting threats on macOS, one of the best sources of information for file system events, process events, and network events is the kernel. MacOS kernel extensions provide the ability to receive data about these events in real time with great detail. This is good for providing quick visibility into detecting anomalies and identifying possible threats. MacOS kernel extensions also provide the ability to block malicious behaviors that a piece of malware is attempting to carry out.

The kernel extension framework provided by macOS, however, is changing. Starting with macOS 10.15, a new framework has been added to assist those who want to have visibility into file system, process, and network events in real time. These new frameworks are called the SystemExtension framework, the EndpointSecurity framework, and the NetworkExtension framework, respectively. These new frameworks will be the new base from which endpoint security products must build on top of starting in macOS 10.16.

Before going into the new frameworks, it is important to understand what is currently offered by the legacy kernel extension framework, including the information that can be retrieved for events and the capabilities provided for stopping malicious behavior.

In part 1 of this series, we’ll go over some of the frameworks accessible by kernel extensions that provide information about file system, process, and network events. These frameworks include the Mandatory Access Control Framework, the KAuth framework, and the IP/socket filter frameworks.

We won't do a deep dive into each one of these frameworks specifically, as there have been many other posts and guides [0][1][2][3][4] regarding how to use these frameworks. Instead, we’ll recap and review each of these frameworks, then in part 2 we’ll cover some valuable tips and tricks we can use inside the kernel extensions framework that will no longer be available in the new SystemExtensions framework starting in macOS 10.15. And finally, in part 3 of the series, we’ll cover the new SystemExtensions framework and the features it provides to third-party developers.

What are Kernel extensions?

A quick primer on kernel extensions for those unfamiliar with them: Kernel extensions are compiled binaries that allow third-party developers, as well as Apple, to extend the functionality of the XNU kernel. This means that macOS has the ability to extend the functionality of its kernel by dynamically loading new binaries that run inside the kernel space. This is similar to how Windows can load drivers to extend the functionality of its kernel.

MacOS kernel extensions can be loaded via a kextload and unloaded via kextunload. MacOS kernel extensions, by nature, run with kernel privileges since they run in the context of the kernel process. Because of this, Apple requires that all kernel extensions be signed with a special kernel extension certificate. Apple provides this certificate and allows kernel extensions to run on SIP-protected systems.

Still, the user must allow the kernel extension to load manually on macOS 10.14+ via the Security Preference Pane. This can be worked around using spctl to allow a kernel extension to load without user consent.

KAuth framework

The Kernel Authorization subsystem, introduced in Mac OS X 10.4, allows third-party kernel developers to "authorize" actions within the kernel (e.g., process events, file system events). According to Apple’s technical documentation [3] on this framework, there are several components that provide functionality to the developers who want to leverage this subsystem. The main concepts that will be reviewed are listeners, scopes, and actions.

Listeners are the callbacks defined by the user that are called into by the kernel in order to “authorize” some action for a particular scope.

Scopes are considered areas of interest, and we will focus on three:

  • process scope
  • file operation scope
  • vnode scope

Actions are the operations the kernel attempts to carry out on behalf of a process. Depending on the scope, these actions can be denied from occurring by a listener.

The process scope, vnode scope, and file operation scope provide the ability to introspect into process events (i.e., trace attempts and signal attempts) or events such as file modifications or even process executions. This is important for security products that wish to monitor process/file activity to detect and prevent malicious behavior.

A good example of how to implement listeners for each scope is Apple's sample code project "KauthORama" [5] along with Patrick Wardle's series, "Monitoring Process Creation via the Kernel" [0]. While KAuth is powerful in providing information about system events, there are caveats to some of these scopes. For example, the return value of listeners that handle actions from the file operation scope are ignored, which makes this scope unsuitable for stopping malicious activity before it happens.

The vnode scope listeners also have strict efficiency requirements to prevent noticeable delays to the system. In order to introspect deeper into system events and retrieve even more information than what is provided by KAuth, Apple has developed the mandatory access control framework.

Mandatory access control framework (AKA TrustedBSD, MACF)

The mandatory access control framework provides a variety of callbacks for monitoring various events that occur on the system. It is worth emphasizing how powerful this framework is for security vendors. The MAC framework provides insight into nearly every single system event — ranging from file modification, process execution, memory protection changes, codesign checks, and others. In fact, this framework is so powerful that Apple themselves leverage MAC framework to implement features for AppleMobileFileIntegrity, which in turn enforces codesign checks and mach port protections.

A subset of these callbacks are related to file system events and process events. These callbacks provide different information than the KAuth scopes and can be useful for denying certain file system and process operations based on the information passed to the callback. This particular framework, however, is not considered a supported kernel programming interface, and compiling code with it results in an ominous warning "error: "MAC policy is not KPI, see Technical Q&A QA1574, this header will be removed in the next version".

Unfortunately, Apple has followed through on their promise and have pulled the MAC headers as of the 10.13 SDK. That being said, they are still available through the 10.12 and older SDKs. But more importantly, the kernel linker and loader, KXLD, will still load and link any kernel extensions that leverage the MAC framework. This allows developers to use older SDKs and, for the most part, the kernel extensions will work on newer macOS versions. There are still caveats associated with this, though, as certain callbacks are removed/rearranged between SDKs.

All the different available callbacks can be viewed in mac_policy.h[6] in the XNU source. Inside struct mac_policy_ops, there will be a large number of function pointers that can be configured during setup that will provide the MAC subsystem a means to call out to any kernel extension that is plugging into the MAC framework. For a good example of how this framework can be used, take a look at @osxreverser's "can_I_suid"[2], which has a simple, straightforward implementation of a kernel extension using the MAC framework.

We will touch on this subject in more actionable detail in part 2, where we’ll highlight the interesting callbacks and what information we can get from them.

IP/Socket filters

Alongside the ability to monitor file system events and process events, XNU provides a way to receive network events via the socket filter framework. This framework provides a way for other kernel extensions to register their interests — in the form of callbacks — for any subset of network events the framework supports (such as TCP/UDP flows) over both ipv4 and ipv6. Inside kpi_socketfilter.h there is a struct sflt_filter declaration which provides the available callbacks.

Some interesting callbacks for network activities are sf_attach_func, sf_detach_func for attaching/detaching a filter from a socket, or sf_data_in/sf_data_out for filtering on data that is being sent to and from a socket. Apple has some sample code in the tcplognke[7] that shows how to plug in and use this framework.

The other framework used for filtering on network events is the IP filter framework. This framework provides the ability to filter on IP packets in a method similar to how the socket filter works. There is a structure defined in kpi_ipfilter.h, struct ipf_filter that is filled out with the desired callbacks, much like the socket filter. More detailed information about how these filters work can be found by reading Apple's Network Kernel Extension guide[8]. These filters will later be compared and contrasted with Apple's Network Extension framework — new with macOS 10.15 — in order to understand what has changed, what's been improved, and what has been taken away.

Wrapping up

While KAuth, MACF, and IP/Socket filters all provide lots of information for introspecting into filesystem, network, and process events, there are times where not enough information is provided to developers regarding these events. In the next post of this series, we'll learn how to further enrich the information about certain events by diving deeper into the inner workings of XNU.