Threat Hunting Pids Within Apple’s Endpoint Security API

Published by jaron.bradley on

Threat Hunting Pids Within Apple's Endpoint Security API

The Apple Endpoint Security (ES) API provides a number of different process ID’s that can be used in our day to day threat hunting. For those of us that obsess over gettin the best visibility possible out of the activity occurring on the system, it’s best to take the time to fully understand these available pids and how they operate. This blog post lays the foundation for a tool recently released here on the MittenMac titled SpriteTree. In this two part blogpost we’ll focus first on the available pids that ES provides before getting into how we’ve used them in SpriteTree to create a useful tool that can help aid with dynamic malware analysis, threat hunting, and system internals investigation.

Every time a process is run (fork or exec) ES will provide us with details regarding said created process. Among the details it provides are a series of process ids each serving their own system internals purpose which means each one also serves its own threat hunting purpose. Let’s discuss these one by one.

The Process ID (pid)

Can be changed programmatically after assignment?: No

Every process that gets created is assigned a process id. On macOS this pid increments until the max of 99999 is hit. When this limit is hit, new processes will be assigned again starting at 1, incrementing until an available pid is found . If I’ve lost you here, it might be worth taking a step back and reading up on what a processes is.

The Parent Process ID (ppid)

Can be changed programmatically after assignment?: Kind of (see original_ppid)

The most basic of the process ancestry. If we want to know what process lead to the creation of the current process we’re looking at, we often use the ppid. In the macOS world, as documented in my previous write-ups, the ppid will almost always point to launchd on macOS. This is because no matter how you chose to open an application, launchd is ultimately responsible for opening that application. It’s also responsible for booting all the various services required by your operating system. All of that to say, more often than not, the parent of any given process on your macOS system is likely going to be launchd (pid = 1). The processes that do not have a launchd parent are generally programs/apps that are directly shelling out commands or directly calling fork and exec.

The Process Group ID

Can be changed programmatically after assignment?: Yes (to its own pid)

If you’re a long time Unix user than you’re at home with this pid. A process group is basically what it sounds like – a group of processes that are jointly working together. Every process group that gets created has a process group id leader. Take the following command as an example which grabs base64 content located at a url, decodes that base64 content, and then directly runs the resulting code via Python

curl <some_url> | base64 —-decode | python3

When you run this command from your Terminal you are creating processes that look like the following…

Any time a process runs, if the process group id of that process matches the pid of that process, it is the process group id leader. In the above example we see that the process group id leader is the curl process.

The Process Group ID is a very valuable hunting pid in that points us to the process group leader. Let’s say as a threat hunter, we encounter python3 running by itself with no args. When we encounter this, we know one of two things happened…

  1. Python could have received code from stdin
  2. Python could have been executed interactively.

By taking python3 and looking up the process group leader, we get our answer. We see that curl was the process group id leader ultimately responsible for feeding code to python3. Performing a search for all processes that match this process group id would reveal that the base64 command also played a role in this operation.

The one down side to the process group id is that when job control isn’t at play (think a fully functional shell) it can seem unpredictable which processes get set as process group id leaders. Although the operating system is still very specific about what it’s doing, I’d argue this pid is most helpful within fully functional shells where job control is at play.

Finally, as mentioned at the top of this section, a process may change it’s process group id. However, it may only do this by setting itself as the process group id leader. It is only able to change in this manner.

The Session ID (session_id)

Can be changed programmatically after assignment?: Yes (to its own pid)

So at this point, we have a process, a parent process, and we’ve learned if we zoom out a bit with the microscope,we have a process group of multiple processes capable of passing data around. If we zoom out even more with the microscope, we have a session. A session is a collection of process groups. So just like every process has a process group leader, every process also has a session leader. In the context of a Terminal, this pid generally belongs to the “login” process which is handling the job control of the shell. With a proper shell setup with job control, a session allows us to have both foreground and background processes. Personally, I find the process group id to be more helpful when it comes to writing detections, but the session id can be helpful during analysis. For example, if your EDR is tracking session_id, you can easily pull back all of the commands that were executed within a session in which you’ve discovered suspicious commands. It’s likely that if one command within a session was performed by an attacker, all commands within that session were performed by the attacker. My more favored way to track all commands executed under an app is the responsible process id discussed below.

Similar to the process group id a session may set itself as a session leader. This is the only way a processes session_id can change.

The Responsible Process ID (responsible_pid)

Can be changed programmatically after assignment?: Yes. (Only by the OS)

The responsible pid is where things get a little more interesting. This process id is actually heavily relied upon by the operating system for more than just tracking heritage. The permissions that an application has are also dependent on the processes responsible_pid. If your process has a ppid of launchd (likely the case) then the responsible process id can often provide you with better clarification of how this app opened. You can actually see this within Apple’s Activity Monitor tool if you choose to use the application with the hierarchy layout mode enabled. Rather than displaying a tree built on pids and ppids, Activity Monitor will by default display the tree based on responsible_pid lineage. This is far more helpful to us than Activity Monitor reporting that every program has a parent of launchd. You’ll notice that even XPC processes show their according app parents this way.

In the above image, MediaLibraryService, RegisterProExtension, and VTDecoderXPCService all technically have a parent process of launchd. However, Activity Monitor shows them as children of Motion.app. This is due to the fact that it’s building hierarchy based on responsible_pid.

Generally, a good way to think of it is this – every created program will have a responsible_pid of its highest level application. In other words, if you run a shell command under your application that kicks off ten more shell commands, all of those shell commands will have a responsible_pid of the application that executed them regardless of how many levels deep they are in ancestry. The easiest example of this is the Terminal.app application. Despite the fact that every command executed within a standard Terminal has a parent process of zsh, every created process will reflect a responsible pid of Terminal.app. If I open a Terminal and execute the “last” and whoami” commands, I’d see the following…

Notice that every responsible_pid here points back to the Terminal.

Why does Apple track the Responsible Pid?

The responsible_pid is used for more than just telling you what app was responsible for a process. In fact, it likely wasn’t even the original reason this pid was created. One major reason this pid exists on the operating system is to track what Transparency Consent & Control (TCC) permissions a given process should have. When a process performs a privileged action, such as taking a screenshot for example, the operating system performs a lookup on that process’ responsible_pid. If the responsible_pid belongs to a program that has been granted permissions to take a screenshot, it will be allowed. If not, the user will receive a prompt. We can return to the Terminal.app for yet another great example of this. On a newly built macOS system, take a look at what happens when I try to print text file on my own desktop.

We see here that I’m denied. Even as root, I’m still denied access to this file. This is because my Terminal application does not have access to the desktop. Lets grant it this TCC permission and try again.

Now we can use the cat command to print the contents of the file. But there’s an important distinction here. In this scenario, the “cat” process is what’s accessing the text file rather than the Terminal. Notice that I didn’t have to grant access to the cat program. The reason the operating system allows this action is because no matter how many levels deep the cat command might be, it’s responsible_pid still points to the Terminal application and therefore it’s allowed to access the file.

Another fun fact is that unlike other pid values, this one can be changed at any given moment by the operating system. Although this does not happen frequently, Apple can and does change a processes’ responsible_pid under certain circumstances, but perhaps a breakdown of this is best saved for another time.

Embrace the Responsible

A final note regarding the responsible_pid is that Apple historically hasn’t spoken much of it. Many of those investigating the low levels of the operating system did notice the private api function held within libquarantine.dylib titled responsibility_get_responsible_for_pid() and began playing with it. In recent years, Apple has started providing a little bit more visibility into this pid especially within the Endpoint Security api where you can often get the responsible audit token for most events.

Apple has also provided developers a new use case with this pid via App environment constraints. Using these constraints, the operating system will not allow a process to run if its responsible process is not as the developer has specified it should be. A feature that Apple themselves have implemented into some of their own system processes.

The Original Parent Process Id (original_ppid)

Can be changed programmatically after assignment?: Not that I’ve seen

Now here’s a pid unique to Apple’s Endpoint Security API as far as I’m aware. I haven’t seen any other sources provide this pid value. This is a useful pid and despite the fact it will often reflect the same as the ppid, it does have one trick up it’s sleeve. To best understand it, we must have a bit more UNIX background. For those that aren’t overly familiar with the platform, it is possible for a program to abandon its parent process. In fact, back in the day when we didn’t have launchd to manage our services on macOS, a true service almost always had to deamonize, otherwise it would terminate if its parent terminated. For example, consider the following C code.

This program will loop for five seconds before shelling out the whoami command. Thanks to the while loop, it will do this infinitely. However, if we run this in a Terminal and then close said Terminal, the program will be killed along with it. So this is not a proper daemon. In order for this code to continue running, it must first perform a number of actions. (See chapter 37.2 of The Linux Programming Interface by Michael Kerrisk. An absolutely phenomenal book)

  1. It must call fork() to generate a child process (many recommend forking twice to avoid acquiring a controlling terminal on some Unix platforms)
  2. The parent of that child process must then terminate. This leaves the child “orphaned”.
  3. The orphaned process must call setsid() which creates a new session and sets the process group id to itself free’ing it of any controlling terminal.

If the actions here are performed correctly, the new orphaned process will now be running as a daemon that will not terminate when its controlling terminal is closed.

If we perform these steps we get code that looks like the following…

Notice we still have a while loop down at the bottom of our daemon, but we perform the necessary actions to daemonize first. When we compile and run this program it will start as a child of zsh under the terminal, but as the code executes and carries out further actions, we will see child processes generated under launchd. For example, within the while loop shown below we will instruct our daemon to exec into “whoami”.

So when we run this program from within a Terminal while monitoring exec events that are occurring, we will see the following process tree activity.

Notice here that our whoami process spawned as a child of launchd instead of the daemon process. As an analyst without access to the daemon.c code we can still determine why this happened by looking at the original_ppid for the whoami command which points back to the daemon process. Therefore we know that this process was run by the daemon process and conclude furthermore that it was originally executed under the Terminal.

So that’s how the original_ppid works to the best of my knowledge. It’s worth noting that from a telemetry perspective this pid is useful, but under a rather specific circumstance. An audit token for this pid is also not provided by the Apple Endpoint Security API. Instead, it’s just an Int32 pid. Still it may allow for a valuable pivot point when encountering a broken process tree.

 

If you find yourself wondering what kind of malware would take this approach, crack open the (old) GreenLambert malware in a disassembler. You’ll see that it does in fact call fork twice before exec’ing. 

Conclusion

This blog posts visits the various pids provided by the Apple Endpoint Security API. These pids are provided within every exec event that occurs on the system each providing it’s own type of visibility. In the next blog post, we will discuss SpriteTree, a tool that can be used to investigate many of these values from an eslogger capture.

Categories: Uncategorized