I made an app based on Apple’s Endpoint Security framework, that can protect against some of the process injection techniques on macOS.
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 3rd 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 3rd 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, although sometimes they also fail to do it. (e.g.: CVE-2019-8805)
Unfortunately third party apps are not in a very good shape. This opens up the road to 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 3 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.
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 reuse 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.
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.
For now I treat it as a beta, as it was only me who tested it, and although I didn’t run into any issues in the past 3 months (beyond blocking my own exploits 🙃), I’m still only a single user, and as our professor said in the university, “One measurement is not measurement”.
Currently the app can protect against all injection techniques listed above.
Although I tried to place many comments, later on I will make a document for the code so others can more easily contribute. 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 click on the related menu item, we will need to approve it in Security and Privacy just like, when we install a new kernel extension.
Once it’s approved we need to grant it “Full Disk Access” rights on the privacy pane, as shown in the image below.
Once it’s done, the extension will be loaded, however as of now, 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 SE. For that to fully complete we need to reboot macOS, as in Catalina macOS can’t fully remove an SE without a reboot.
Once the SE is installed we can start the Endpoint Security client, by either clicking “Start” or opening preferences, and toggling “start / stop”. The state of the buttons is refreshed from the SE, and I rarely run into that it’s not done properly, but reopening preferences helps :) bug #1.
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
/Library/Application Support/Shield/shield.log. If we switch this option OFF, we will still get alerts, and logs, but they will be allowed.
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. 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
--inspect 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 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.
Finally there is a switch to autostart the Shield main app (menu item) upon startup. This will install and uninstall a standard Login item.
I would like to highlight that at this point I consider this tool as BETA if not ALPHA, and thus purposefully I didn’t implement the ability to auto run the Endpoint Security client. So even if the main app starts after boot, you need to explicitly start the ES client. Once I get enough user feedback, I will add this option.
All of the preferences that are configured, are saved into
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 :)