Shield - An app to protect against process injection on macOS
In this post I would like to tell the story of the Shield.app development and also introduce its features. It’s been a ride over the past year, and I wasn’t sure always that it will happen.
Motivation for limiting process injections
In the past 2 years I started to dig into macOS security research, and along the way it became pretty clear that beyond memory corruption issues the alpha and omega of macOS exploits is to run code in the context of other applications. The reason for this lies within the security model of macOS (and in fact *OS as well). Each application has a list of entitlements that grants the application various rights. If we take only 3rd party applications it’s mostly around what it can do if it’s sandboxed (e.g.: access the network) or if not sandboxed, which privacy (TCC) protected areas can it access, like camera, microphone, messages, etc… In case of TCC, if we don’t hold these entitlements, we can’t access those resources or location even if we run as root.
For Apple binaries there are hundreds of different Apple private entitlements, which no third party app can have, for example these can control access to SIP protected areas, or the ability to load kernel extensions.
To add to this list, one of the fundamental protections of XPC cross process communication, especially where one of the process is privileged, is the ability to control who can talk to a specific XPC service. In the case of Apple this is typically done by entitlements, and in case of third parties, it’s done by code signature verification. In both cases if we can run code on behalf of the XPC client, we will have the ability to talk to a privileged XPC service.
The list can go on and on, Keychain sometimes also controls access based on code signatures.
This ultimately means that if we can inject code to an application, we can gain its rights. This is why process injection capabilities are very tightly controlled on macOS. Apple does a good job on protecting their own apps.
Unfortunately third party apps are not in a very good shape. This opens up the road for plenty of XPC vulnerabilities, which will typically allow users to escalate their privileges to
root. TCC bypasses are also common, which will allow users to access sensitive locations. (e.g.: LPE in Microsoft AutoUpdate or TCC bypass with Zoom)
Process injection typically comes down to the following three main scenarios:
- dylib injection via environment variables
- dylib hijacking (or proxying)
- shell code injection through task ports
Seeing how commonly someone can perform the above, and being uncomfortable with it, as I’m also a Mac user, I decided to write a small app that can prevent these attacks.
Motivation for limiting file system link creation
With the release of 1.1, Shield will detect suspicious symbolic and hardlink creations. Such links can be easily abused by attackers, I wrote about several exploits here: Exploiting directory permissions on macOS · theevilbit blog.
Development of Shield
With KEXTs going away I decided to try my luck with the new Endpoint Security framework. I must start with admitting, that I don’t consider myself as a developer, and I never really done development work so I probably write poor code, but I tried my best to make a solid app with security in mind, although I’m sure the code can be optimized.
With that I greatly relied on code which was developed by Patrick Wardle and open sourced as part of his Objective-See tools. In fact, with some modification, I reused his process monitoring library, and some code from LuLu. His work is much-much appreciated! I can’t be thankful enough. It helped a lot to understand how we create an ES (Endpoint Security) agent, and how everything comes together. It also helped me a lot in learning about coding and how to design the code.
I also spent time looking into Stephen Davis’s Crescendo codebase. Although it’s written in Swift, and I wrote Shield in Objective-C, it helped me to understand other aspects of ES, like talking to the agent, and how to install it, if we don’t run it as a daemon, but a system extension. Again, big thanks also to him!
After many nights spent coding, I arrived to a stage that I’m satisfied enough with the results to release it to the public. I can’t tell how much I learned about Objective-C, coding, making projects in Xcode, structuring apps in Xcode, etc… From my original plans of running Shield as a ES daemon, now it runs as a System Extension (SE), which contains the main application logic, and that is where the protection is happening, there is a Helper Tool to autorun it in the menubar and the main app to control the SE.
Now it also supports allow list functionality, as many application use injection techniques for legitimate reasons.
Since the initial release in 2021 January I think the product matured enough to release as stable. Thanks a lot for everyone for bug reports and feature requests! Keep them coming. The more people use it the better, as “one measurement is not measurement”.
Although I tried to place many comments, later on I will make a document for the code so others can more easily contribute.
Getting the EndpointSecurity entitlement
Developing the code is one part of the story. For the whole app to actually become a reality I had to get the EndpointSecurity entitlement from Apple. Anyone enrolled in the developer program can apply for it… and that is where things get annoying.
I requested the entitlement back in 2020 March. A month passed without any feedback, and after a month I got the development version. This allowed me to create a distribution profile, which allowed me to load the system extension (SEXT) on my computer only. It’s good for development, but of course not for distribution.
As I developed the code, I wrote Apple that I need the production version of the same entitlement. Months passed and I got no reply from Apple at all. Around July they came back with a question, which I answered right away, yet nothing happened. This started to be not only frustrating but greatly demotivating. I basically stopped development the code. I lost motivation. If I won’t be able to release it, what’s the point of developing it after a certain point beyond my own entertainment.
More months passed, we got into Christmas and New Year, and finally I got it. I didn’t believe it. 10 months passed and I got the entitlement. Apple even apologized for that it took so long. I have no idea what happened and why it took so long.
Now, let’s see how we can use it, and what it does.
The application doesn’t have a normal window mode app, it’s menubar only. When we start it, we will see a new menubar icon, in a form of a dot showing up.
Clicking on it, brings up some basic controls, as shown below.
Before we can really do anything, we need to install the system extension. When we start the application it will start the installation process, and popup windows how to do it. We need to approve it in Security and Privacy just like when we install a new kernel extension. Once it’s approved it will be loaded, however I don’t autostart the ES client, so by default it will be stopped.
If we want to uninstall the agent, we need to click on the related menu option. Note that it doesn’t delete the app, it will just uninstall the SEXT. For that to fully complete we need to reboot macOS, as macOS can’t fully remove an SEXT without a reboot.
Once the SEXT is installed we can start the Endpoint Security client, by either clicking “Start” or opening preferences, and toggling “ENABLED”. The state of the buttons is refreshed from the SEXT.
There are not many options, so I will cover them one by one.
Blocking mode means that if it detects an injection attempt, that is configured next, it will block it. In case of environment variables, which is typically happening at the time of the process starting up, it will mean that the process that is the target of the injection can’t start, it will be blocked. If there is an injection attempt we will get a notification, and it will be also logged to the system log.
When we get a notification we can always chose allowing it for the next time, so we don’t get alerted next time.
If we switch off blocking mode, we will still get alerts, and logs, but they will be allowed.
Once blocking mode is disabled, we can enable “learning” mode, which means that everything that would create an alert will be automatically added to the list of allowed injections, and we won’t be alerted.
The next option is the ability to monitor Apple binaries. For now this is unchangeable, platform binaries are ignored. The main reason for this is that they do extreme amount of
task_for_pid calls, and just handling of those without any action causes 20% CPU usage. Right now, these processes are pruned very early, and thus we get a nice 0% CPU utilization. I have a todo here to improve the logic, so system binaries can be monitored as well, although I think it’s not that a huge issue for now, because as noted earlier, generally platform binaries are very well protected against these attacks.
Next we can enable or disable specific protections.
The environment variable injection is monitoring the presence of any of the following three as of the time of writing:
ELECTRON_RUN_AS_NODE. If any of these present, the app will not launch. I figured out this can cause issues with Firefox.
The next setting is for
task_for_pid calls, when one process wants to get the task port of another. This will block debugging, as a debugger will perform this call. So if you need to debug, you may want to switch this off temporary.
The following one is specific to Electron apps. Typically someone can use
--remote-debugging-port command line argument to start an Electron app in debugging mode, and thus inject code to it. Here I simply check if this argument is present and if yes, the app will be blocked.
The last option in process injection settings is to enable protection against dylib hijacking and proxying by enforcing library validation. Unfortunately I could only achieve this by doing static code signature checks on dylibs on disk, so due to disk IO, it can delay large apps (like Xcode) to start, otherwise it’s hardly noticeable. I enabled caching and that makes it manageable.
The next two settings are related to file system links. Typically the most common abuse is to place a link with our user privileges, and point it to a higher privilege location. The detection is very simple for both hardlink and symlinks. If the process creating the link has a different privilege level than the target file, we create an alert. Unfortunately in the case of symlinks blocking is not possible, as we don’t have information about the destination of the link prior creation. This is a limitation of Apple’s EndpointSecuriy framework. The simple logic would eliminate all of the symbolic link exploits I did in the past.
Notification for these events will be different from the ones we get for process injection. Here we will see the process’s UID, as well as the link’s target file (source) UID, along with the actual link’s path (destination) to where it points (source).
Finally there is a switch to autostart the Shield main app (menu item) upon startup. This will install and uninstall a standard Login item.
All of the preferences that are configured, are saved into
The last thing to discuss is the “Allowed Injections” menu.
Here we will get an overview of all the allowed injection, that won’t generate a notification anymore, and will be always allowed. Unsigned binaries will need to be allowed on this list, as well as some other apps use injection legitimately. For example “GitHub Desktop” uses “ELECTRON_RUN_AS_NODE” when we push a change to our repository.
The view is split between allowed file link and process injection events, and we can remove items from both. The button “Clear All” will wipe both lists.
The code for the application is hosted on GitHub, please contribute, or if you run into bugs, please report or simply submit feature requests. It will take some to implement or fix items, as I mostly do this in my free time, next to a full time job, family and other hobbies, like yoga and hiking :)