Aloha it’s Patrick, Director of R&D at Synack. In my free time, I also run a small OS X security website objective-see.com, where I share my personal OS X security tools and blog about OS X security and coding topics. Below is one such post originally published on my site, which discusses one of my tools and more generally once way to monitor processing creation on OS X. Read & enjoy!
Update: Several people have reached out to me (mahalo!) to mention that the KAuth API can also be used to monitor process creation from a kext. While I wait for a kext signing certificate from Apple I’ll going to check this out, as the KAuth interface appears more stable than the prototype of the MAC policy function. Findings will be included in part II of this blog posting 🙂
Having recently returned from presenting at VirusBulletin and EkoParty, I finally have some free time to catchup on my todo list. First up? – updating BlockBlock for El Capitan compatibility. Although most of BlockBlock’s code and logic works great on El Capitan, one component is completely broken… thanks to Apple’s changes to their latest OS.
BlockBlock monitors file I/O events in order to detect “persistence attempts.” When it detects such an event, it alerts the user. In order to provide an informative alert, the alert popup contains the pid, path, and ancestry of the process responsible for at attempted persistence:
Although an application could use the FSEvents API to be alerted of specific file and directory changes, this API does not provide information about the process that generated the event. That is to say, sure you get notifications from the API such as, “hey, a new launch daemon (plist) was created” – but there is no direct or trivial way to then get the pid and/or path of the process that created the new daemon.
As such BlockBlock utilizes the /dev/fsevents device directly, as suggested by Amit Singh in his seminal “OS X Internals” book. While this mechanism captures all file I/O (as opposed to only events of interest), it does provide the process id (pid) of the process that generated the file I/O event. Now this is a good start, but as previously mentioned, BlockBlock seeks to provide the user more information about the responsible process such that the user may make an educated decision. For example, being able to display the process’s path and process ancestry is definitely useful (if not essential), information that should be contained in a BlockBlock alert.
Generally, given a pid, one can simply call API functions such as proc_pidpath (see libproc.c) to get a process’s path. However, if the process is short-lived and has already exited, this (and other) APIs will fail. As such, BlockBlock separately keeps a list of all process creations that includes somewhat detailed information about each process such as its pid and full path. Then, when a “persistence attempt” is detected via the file I/O monitoring component, even if the process has exited, BlockBlock can use the pid that’s tied to the file I/O event to query its process list to get required information, such as the process’s path.
Prior to El Capitan, BlockBlock used programmatic dtrace probes in order to record process creation events. Specifically it set probes from user-mode, on:
These probes allowed not only pids, but also process paths, uids, and ppids to be recorded by BlockBlock. All was well 🙂
Then along came El Capitan – and basically said, “no more (meaningful) dtrace” Don’t believe me? Try running Apple’s very own exesnoop that ships with the OS. As it uses dtrace, it’s totally broken, even when run as root 🙁
When I reached out to Apple about this I was told: “[you] can’t trace syscalls using dtrace when SIP is enabled. We [Apple] can’t make the distinction between a path or something more private.”
It should be noted that yes, one can still do basic dtracing of some processes. However, there appears to be no (legitimate) way of tracking process creation events via dtrace, in a way that also provided semi-detailed information about the process, such as its full path. As BlockBlock requires (or I should say, prefers) such detailed information I had to explore other approaches.
First, thanks to suggestion from @hey_pom, I dove into the audit (open BSM) framework. While this isn’t that well documented, I was able to coerce some user-mode code to cough up some semi-detailed information for each process creation. Unfortunately, this information varied based on how the process was created (execv’d, forked, or spawned). For example; when a process is forked, the audit subsystem only provides the pid (no path), while for a spawned process, only the process’s path is provided:
This is less than ideal, as BlockBlock requires a comprehensive pid -> process path mapping (even for short-lived/terminated processes). With all user-mode options (to the best of my knowledge) exhausted, I decided to head into ring-0.
Again, my goal was simple; record all process creation events so that at a later time, both the process id and full path of the newly created process would (still be) accessible. After a bunch of googling, it seemed that the simplest way to monitor process creations in ring-0 was via a mandatory access control (MAC) policy. Although this is (still?) an unsupported KPI, we’ll see it provides the perfect solution to our issue. That is to say; it provides a comprehensive way to monitor process creations in a detailed manner.
The mandatory access control implementation for OS X is TrustedBSD. For a quick primer and solid overview, checkout “Working with TrustedBSD in Mac OS X.” In terms of using a MAC policy to monitor process creations, I recalled one of @osxreverser’s blog post titled “Can I SUID: a TrustedBSD policy module to control suid binaries execution”. In this posting he discussed how one might control the execution of suid binary execution via a MAC policy. The code he shared is easy to follow and shows exactly how to register, via a MAC policy, a function that will be automatically called by the OS anytime process is created. Though he uses this policy function to determine if the process’s binary has any SUID bit set, we can tweak the code so that the function can also access process id, path, parent id, and uid. Perfect – that’s all we need 🙂
Let’s now walk thru some ring-0 code that implements such a MAC policy/hook (note, we’ll cover how to make this information available to BlockBlock’s user-mode components in part II of this blog post). If you’d like to follow along in code, its downloadable as an Xcode project. Again, mahalo to @osxreverser – this code is fully inspired by his previous work!
Kernel extensions (or ‘kexts’) are the legitimately way introduce code into the kernel. There are two kinds of kexts; ‘Generic Kernel Extensions’ and ‘IOKit Drivers’:
Since I wanted BlockBlock to be installable without requiring a reboot, I went with the former. (Apple answered when asked if it is possible to install an I/O Kit kext without requiring a restart: “There is no easy solution to this problem. Currently, even Apple’s own installers require restarting after a kext installation.”)
Conceptually, in order to record detailed information of all process creations, our generic kext only has to do two things. First, register a MAC policy (indicating our desire to monitor process creations). Then, implement the MAC policy function that is automatically invoked by the OS whenever a process is started. Within this function, we can add code to record details about the process (pid, path, ppid, etc).
Step 1: Registering the MAC Policy
In the kext’s start function, (specified in kext’s build settings, under ‘Module Start Routine’), invoke the mac_policy_register function. As shown below, this function takes three parameters; a pointer to the MAC policy configuration, an (out) pointer to the MAC policy handle, and the second argument that was passed to the kext’s entry point function.
Let’s take a closer look at the first two parameters of the mac_policy_register function. As just mentioned, the first parameter is a pointer to a MAC policy configuration. Specifically this is pointer to a structure of type mac_policy_conf. Defined in security/mac_policy.h this structure is defined as follows:
Although somewhat complex, luckily most the members in this structure can be initialized to blank or NULL values. However, four should (must?) be set.
1) const char *mpc_name
This is the policy name. You can pick any string (“BB Process Monitor”)
2) const char *mpc_fullname
This is the policies full name. Again, pick any string (“BlockBLock Kernel-Mode Process Monitor”)
3) struct mac_policy_ops *mpc_ops;
This is the most important member of the mac_policy_con structure. Described by Apple, as an ‘operations vector’ it should contain a pointer to a mac_policy_ops structure. The mac_policy_ops structure specifies what operation (e.g. ‘process creation’) the kext is interested in, and the name of the callback function to invoke when said operation or event occurs. This details of the mac_policy_ops structure are discussed in more detail below.
4) int mpc_loadtime_flags
These flags indicate such things as whether the policy (and thus kext) can be unloaded (‘MPC_LOADTIME_FLAG_UNLOADOK’) or when the policy should be loaded.
The following shows the fully populated mac_policy_con structure, used in BlockBlock’s kext:
As mentioned (step #3), the mac_policy_ops structure tells the OS what policy or event one is interested in. In order to register for process creation notifications, specify the mpo_vnode_check_exec_t MAC policy operation and provide a callback policy function:
Step 2: Implementing the MAC Policy Function
Once registered, the policy function (here, named ‘processExec’) will be automatically invoked by the OS anytime a process is being created.
The prototype of the policy function is rather long, and unfortunately, as @osxreverser notes, sometimes changes between OSs. However, since Yosemite it has remained the same. (As such this kext will only run on versions of OS X Yosemite and newer):
Luckily, in order to access detailed information about the process being created, only the first vnode parameter (vp) is needed. All other parameters for the purpose of this project, can be ignored. Recall, BlockBlock requires the pid, ppid, uid, and process path, for all created process. Turns out, all this information is readily available within mpo_vnode_check_exec‘s policy function (here, named processExec):
The process’s id, can be accessed via a call to the proc_selfpid() function
The process’s parent id, can be accessed via a call to the proc_selfppid() function
The process’s user identifier can be accessed via a call to the kauth_getuid() function
The passed in vnode argument pointer (‘vp’) can be used to get the process’s full path. Simply invoke the vn_getpath() function, passing in the vnode parameter and an out buffer, and pointer to the out buffer’s size:
Performing steps 1 thru 4 in the MAC policy function (which we named processExec), allows the BlockBlock kext to retrieve all required information about process creations:
Time to test this out! Since this is a beta kext, I’d suggest testing it in a VM. Once the kext is compiled (simply open in Xcode and hit Project->Build) and copied to the VM, change its owner to root/wheel:
Since OS X no longer (legitimately) allows the loading of unsigned kernel extensions by default, the VM must be explicitly configured to disable this ‘security’ feature. For Yosemite, tell the OS to allow the loading of unsigned kernel extensions via:
On El Capitan accomplish the same via:
Now, load the kext:
If all goes well the kext should be loaded (I’ve tested it on Yosemite, and El Capitan). Open Console.app to monitor the kext’s output. Try launch some apps, or execute some processes:
Obviously for production code, one would want to sign the kernel extension so users can install BlockBlock without having to turn off kext-signing checks. According to online documentation a normal Apple Developer ID is not longer enough to sign a kernel extension. Instead, one must request a ‘Developer ID for Signing Kexts’ via this form. Seems easy enough assuming one is developing legitimate OS X software? Or so I though:
Me: “Aloha, Objective-See LLC would like a Developer ID for Signing Kext, so that it may sign its kernel extensions for it customers. This is for a legitimate software product that does not attempt to bypass OS X security features in any way”
(note: I also provided the technical reasoning why BlockBlock (now) required a kext, as well as a link to BlockBlock’sproduct page)
On that bitter(sweet) note, let’s end. Check back soon for part II (and maybe Apple will have reconsidered their decision to deny me the ability to sign kexts). I’ll detail about how the process information captured in the BlockBlock kext, is made available to the user-mode component of BlockBlock via broadcasting to a system socket 🙂