Monitoring Process Creation via the Kernel (Part II)
11/22/2015
Last week, in (part I) of this blog mini-series, I discussed why BlockBlock required processes creation notifications, and one way to achieve this via a kext. Specifically, the blog post showed a MAC policy could be registered that would receive notification whenever a process was started.
Once I posted the blog, the venerable @osxreverser and others (mahalo Simon!), were kind enough to reach out to me to mention that such process monitoring could equally be achieved via the Kernel Authorization (KAuth) subsystem. As the KAuth interface is a more stable API than the MAC framework (which is 'unsupported' by Apple), I decided to explore this option.
Kernel Authorization (KAuth) Subsystem
Before getting into the details of the KAuth subsystem, its API, and how to monitor process creation; a few brief statements to summarize my explorations about the KAuth:
- Unlike the MAC framework, it is publicly supported and documented by Apple
- It can easily be used to monitor process creations
- Appears less powerful than the MAC framework (e.g. cannot track forks())
...it should noted that to OS X kext programmers, none of these statements are likely new!
Alright, so KAuth. Detailed in Apple's Technical Note TN2127, the KAuth subsystem "exports a kernel programming interface (KPI) that allows third party kernel developers to authorize actions within the kernel [and] can also be used as a notification mechanism." For the purposes of BlockBlock and monitoring process creation, we're only interested in the notification abilities of the KAuth subsystem.
The Technical Note provides a somewhat comprehensive overview of 'Kernel Authorization', so we won't dive into to many details. However, it's worth recognizing a few key concepts and terms:
- Scopes:
Scopes are simply areas or categories of interest. Via a scope, a kext can register for a subset of notifications, as opposed to all. For example, the KAUTH_SCOPE_FILEOP scope (defined in sys/kauth.h), can provide notifications about file system operations...including file 'executions'
- Actions:
Actions are "operations within a scope." For example, the KAUTH_FILEOP_EXEC action, within the KAUTH_SCOPE_FILEOP scope, is generated (and delivered to listeners), after a process is created.
- Listeners:
Listeners are simply callbacks that will receive notifications (that they have registered for), from the KAuth subsystem.
With a decent understanding of the KAuth subsystem, it's fairly easy to write a kext that can receive most process creation notifications in two easy steps. If you'd like to follow along in code, download the source for the kext as an Xcode project.
Implementation Step 1: Register A Listener
The kauth_listen_scope function allows one to register a listener for a existing scope. Its prototype is as follows:
extern kauth_listener_t kauth_listen_scope(const char* identifier, kauth_scope_callback_t callback, void *idata);
The first parameter ('identifier'), is the name of the scope the kext is registering for (e.g. KAUTH_SCOPE_FILEOP).
The second parameter ('callback'), is the address of the kext's callback function that will be automatically invoked by the KAuth subsystem whenever a event (that matches the scope) is generated.
The third parameter ('idata'), is a cookie or 'refCon', for the callback. For purposes of monitoring process creations, utilization of this parameter is not needed.
The value returned by the call to kauth_listen_scope, is a kauth_listener_t. Save this, so that the listener can be unregistered when the kext is unloaded.
Let's look at the first two parameters a little closer.
As mentioned, the first parameter is scope of interest. Scopes are defined in sys/kauth.h as well as Technical Note TN2127.
For process creation notifications a kext can either use the KAUTH_SCOPE_VNODE or KAUTH_SCOPE_FILEOP scope. Both scopes have actions that are generated via process creation events. Specifically, the KAUTH_SCOPE_VNODE scope has the KAUTH_VNODE_EXECUTE action, while the KAUTH_SCOPE_FILEOP scope has the KAUTH_FILEOP_EXEC action. A posting on stackoverflow describes the differences between the two:
"You can register in KAUTH_SCOPE_VNODE and track for KAUTH_VNODE_EXECUTE to be notified before the execve performs (and possibly deny it to succeed by return value from your callback); or register in KAUTH_SCOPE_FILEOP and track for KAUTH_FILEOP_EXEC to be notified after the execve() is performed."
For BlockBlock, as we merely need a notification that a process was created, (as opposing or denying it), the KAUTH_SCOPE_FILEOP scope with the KAUTH_FILEOP_EXEC action suffices:
//register kauth listener
// ->scope 'KAUTH_SCOPE_FILEOP'
kauthListener = kauth_listen_scope(KAUTH_SCOPE_FILEOP, &processExec, NULL);
Implementation Step 2: Code Up the Callback
As noted, the second parameter to the kauth_listen_scope is a callback function, that is invoked by the KAuth subsystem when scoped (e.g. file system) events occur. The prototype for the callback is:
static int <callback function name>(kauth_cred_t credential, void* idata, kauth_action_t action, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3);
While the function takes a good number of parameters, for purposes of monitoring process creations, the only are few are relevant. In terms of the return value, Apple states that the function must return either KAUTH_RESULT_DEFER, KAUTH_RESULT_ALLOW, or KAUTH_RESULT_DENY. These values are described in the Technical Note TN2127, however for simple notification purposes (as opposed to authorizations), just return KAUTH_RESULT_DEFER.
The first parameter of interest is the 'action' parameter. This, as previously mentioned, is the operation within the (KAUTH_SCOPE_FILEOP), scope. As the kext is only interested in the KAUTH_FILEOP_EXEC action, the rest can be ignored:
//call back function for file ops
// ->only care about KAUTH_FILEOP_EXEC
static int processExec(kauth_cred_t credential, ..., kauth_action_t action, ...)
{
...
//ignore all non exec events
if(KAUTH_FILEOP_EXEC != action)
{
//bail
goto bail;
}
...
For KAuth callback functions, the values of arg0 - arg3 are action specific. For KAUTH_FILEOP_EXEC actions, 'arg0' is a pointer to the vnode for the executable, while 'arg1' is a pointer to the path of the executable:
In other words, within the callback (which is automatically invoked anytime a process is created), the code has access to process's path. Combined with the pid, ppid and uuid, (which are all equally accessible), the information that BlockBlock required, is can be provided.
The following code illustrates the full implementation of the (BlockBlock's KAuth KAUTH_SCOPE_FILEOP), callback:
//callback function for file ops
// ->only care about KAUTH_FILEOP_EXEC
// for this action, grab process path, id, etc
static int processExec(kauth_cred_t credential, void* idata, kauth_action_t action, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3)
{
//path to executable
char* path = NULL;
//uid
uid_t uid = -1;
//pid
pid_t pid = -1;
//ppid
pid_t ppid = -1;
//ignore all non exec events
if(KAUTH_FILEOP_EXEC != action)
{
//bail
goto bail;
}
//path is arg1
// ->see k_auth.h for details
path = (char*)arg1;
//get UID
uid = kauth_getuid();
//get pid
pid = proc_selfpid();
//get ppid
ppid = proc_selfppid();
//dbg msg
printf("BLOCKBLOCK KEXT: new process: %s %d/%d/%d\n", path, pid, ppid, uid);
//TODO alert user mode
//bail
bail:
return KAUTH_RESULT_DEFER;
}
Compiling and loading this kext (see the previous blog post for details on this process), illustrates that yes, process notifications are received and output by the kext. Hooray!
KAuth vs. MAC
The previous blog post illustrated how process notifications could be received via a MAC policy. And now, we've shown how to accomplish the via a KAuth listener. So which method should be selected? Well, as KAuth is a supported and documented (read: the legitimate) subsystem, is seems the obvious choice. However, MAC policies may be more powerful, and allow one to more comprehensively track process creations. How so? Well the following stackoverflow post explains:
"KAUTH_VNODE_EXECUTE [and KAUTH_FILEOP_EXEC] isn't quite sufficient for all processes; this won't catch processes which are fork()ed without exec(). Fairly rare on OSX, but not unheard of."
To illustrate this claim, try compile and execute the following code, and observe what the KAuth callback displays:
int main(int argc, const char * argv[])
{
//pid
// ->return value from fork
pid_t pid = -1;
//fork!
pid = fork();
//child logic
// ->note: don't exec()
if(0 == pid)
{
//dbg msg
printf("Aloha: i'm the child (pid: %d)\n", getpid());
}
//parent logic
else
{
//dbg msg
printf("Aloha: i'm (still) the parent (pid: %d)\n", getpid());
}
return 0;
}
The result of executing the code? While the KAuth listener does receive notification of the creation of the parent, it does not receive the notification for the child process execution (pid 411):
The same stackoverflow post mentions, "There is a MAC framework policy callback for fork." And indeed there is: mpo_proc_check_fork
So, if a kext makes use of the MAC framework and registers for this notification, it will received notification of fork() attempts. It should be noted that this notification will be delivered before the fork() as completed. Thus the child's pid will not (yet) be accessible. However, the path will be known, as it the same as the parent, until the child execs (which should generate a mpo_vnode_check_exec or KAUTH_FILEOP_EXEC event).
Conclusion(s)
To summarize; the simplicity and interface stability of the KAuth subsystem, make it a great way to track process creations in the kernel. However, if one needs to also track process that fork(), but don't exec(), using the MAC framework may be the way to go.
Next up (part III); how the process information, captured in by BlockBlock kext, is made available to user-mode, by means of a broadcast to a system socket. Check back shortly for that new (part III), blog post!