The Evolution of TCC on Ventura

author iconBy Michael Cowell

In the surprisingly stable first beta release of macOS Ventura, there are a number of simple yet impactful security enhancements. This blog post will ignore lower-level changes, opting instead to talk about higher level changes that users are likely to interact with, and some of the attacks they’re meant to prevent.

The (not so slow) creep of TCC

For those that aren’t familiar, the TCC framework (Transparency, Control and Consent) was introduced in OSX Mavericks, and prevents even root-level processes from accessing sensitive resources such as the webcam and microphone. Over time, more and more of macOS is being protected via TCC access prompts, which are identical in look and feel to the prompts you see when permitting access to such things on mobile platforms.

TCC organises resources and access in the form of pairs of (service, client) where client is a bundle ID or file path. These rules can be modified via the System Settings application, and are stored in two separate places. A system-wide DB at /Library/Application Support/com.apple.TCC/TCC.db, and a per-user one in ~/Library/Application Support/com.apple.TCC/TCC.db.

Both databases are inaccessible blocked by SIP, meaning that root applications cannot insert their own rules. If a system application needs access to the TCC databases, it is provided via private Apple entitlements.

In Ventura, TCC’s remit once again expands, now covering the /Applications folder. When installing an application from a .pkg file, you will now see a prompt

Looking at the user TCC database, we see that the service name for modifying app bundles is kTCCServiceSystemPolicyAppBundles.

If we refuse the prompt, we see that the installer seems to proceed but then bails with an error. Looking at the logs, we see that installd failed to move the package contents from the PackageKit sandbox to /Applications:

installd[992]: PackageKit: Install Failed: Error Domain=PKInstallErrorDomain Code=120 "An unexpected error occurred while moving files to the final destination." UserInfo={NSLocalizedDescription=An unexpected error occurred while moving files to the final destination.

This prompt seems to have been carefully considered – Installer doesn’t appear to be added to the TCC database when this prompt is approved, meaning that subsequent packages don’t inherit the result of this prompt. 

In its current state, it appears that tccd, the process that enforces TCC, is only protecting bundles owned by root (e.g installed via a .pkg). Attempting to modify such an application will result in the following warning

Oddly enough, on Beta 2 after triggering this prompt once, it doesn’t reappear on subsequent writes, requiring you to remove the (disabled) app from the App Management section and restarting the application to see the prompt again. 

This prompt only covering package-installed applications means that, at present, it is better to install an application from a .pkg than from the more common scenario of mounting a DMG and copying out the app bundle. In cases such as Firefox, which is typically offered as a DMG but also distributed in a .pkg for sysadmins, there is now an incentive behind dropping the DMG distribution.

But what is the point of all this? In our first scenario, we’ll show how we can abuse loaded libraries to compromise process integrity, and in the second use launch daemons contained in app bundles to elevate and persist.

Scenario 1

For this change, we’ll consider two separate attack scenarios.

The first is dylib hijacking. When installing applications on macOS, it’s most common to receive the software in a .dmg image, and then move the .app bundle to a shortcut to /Applications. In doing this, the bundle is copied across owned by the current user (when installed via a .pkg file, on the other hand, the owner is system).

A worryingly large percentage of popular applications allow unsigned libraries to be loaded via the com.apple.security.cs.disable-library-validation entitlement (such as Firefox, VeraCrypt, and at one time Microsoft Teams). Apple have previously attempted to limit the damage of unsigned libraries in their own software by replacing it with com.apple.private.security.clear-library-validation. In the latter case, the executable will have library validation enabled, and only disabled via a call to csops. If you consider an application that can load plugins, library validation can remain enabled until the user attempts to load a plugin, thereby protecting compromised users that don’t use the plugin feature.

By disabling library validation and copying the application bundle across as the current user, it means that the current user can modify/overwrite libraries inside the bundle that are loaded. The overhead to hijacking a dylib is reducing substantially by adding an LC_REEXPORT_LIBRARY load command, instructing the linker to look in a dylib at an alternative path for any symbols not found in the dylib. The simplest scenario is therefore:

  1. Write a dylib with a dylib constructor to perform your malicious act
  2. Rename a legitimate library
  3. Add a LC_REEXPORT_DYLIB command pointing to the renamed legitimate library
  4. Move the library to where the legitimate library was originally

Luckily, detecting this sort of thing is fairly easy – tools such as Dylib Hijack Scanner by Objective-See can search for the load command.

Scenario 2

Scenario two is where we can race the installer for applications we know launch resources from an application bundle as a Launch Daemon, potentially providing both escalation to root and execution on reboot.

For example, consider Microsoft Teams. On installation, Teams installs a daemon at /Library/LaunchDaemons/com.microsoft.teams.TeamsUpdaterDaemon.plist to enable the (unprivileged) client to request an install of an update. The executable being loaded is /Applications/Microsoft Teams.app/Contents/TeamsUpdaterDaemon.xpc/Contents/MacOS/TeamsUpdaterDaemon. Admin users have write access to the /Applications folder, so a malicious application running as an admin user is able to achieve escalation to root if they can race the installer and create the directory first.

This turns out to be very easy to do. During installation, the package contents are unpacked to a protected directory under /tmp, and a directory with the bundle ID of the package being installed is created inside. Using this, we can build a glob pattern that will match Microsoft Teams:

/tmp/PKInstallSandbox.*/Scripts/com.microsoft.teams.*

Once we see this file, we know the user is in the process of installing Teams. As mentioned above, the contents of a package are first unpacked to the sandbox and only moved to /Applications at the end of the installation, giving us have ample time in which to create the directory. Once we’ve done this, we can move the daemon and replace it with our malicious binary, thereby achieving both root permissions and persistence on reboot.

Login Items

Login Items are a feature an application can use to register to start when the user logs in. While adding an item to login items and LaunchDaemons/LaunchAgents are similar in function, most malware opts to use LaunchDaemons/LaunchAgents items, which can be installed by writing a property list file to either /Library/Launch[Agents|Daemons]. The benefit of using Agents and Daemons over Login Items is that the latter is featured prominently in System Settings (formerly System Preferences)

Software such as BlockBlock by Objective-See has been developed to attempt to limit the impact of malware persisting this way. BlockBlock hooks file i/o affecting these directories and presents the user with a prompt to approve or block the installation of items.

Unusually, Apple appears to have implemented a much poorer version of BlockBlock in Ventura.

First, the good: Ventura now provides the user with a notification when a Login Item is added, and the user can click this to load the list of Login Items on their user account. More importantly, it also brings includes a list of launch agents and daemons

An example launch agent/daemons list on Ventura

While this is positive, the implementation highlights some odd design choices. First, users are able to quickly find the associated plist file only if it’s unsigned, with the button missing for signed applications. Secondly, the list appears to seldom update, leading to a list of items that can’t be opened because they don’t actually exist.

Even if we overlook the latter issue, the implementation of this feature, again, appears to be half baked – why are launch services not also covered under a new TCC prompt? Such a change appears easy enough to implement, and the need for the prompt is surely at least as needed as the prompt for Applications.

A better approach

The attack scenario is well-trodden ground in this instance – write a LaunchAgent or Launch Daemon (depending on current access) to persist across reboots. In the current betas, nothing changes here – malicious actors are more likely to use launch plists, so no notification will be triggered.

Instead, how could we prevent launch misuse? If the Agents, Daemons and HelperTools directories were protected by a TCC prompt, both for read and write, then persisting becomes significantly harder. Write access would prevent new agents, but including read prevents the binaries for existing plists being replaced. Like with Privileged Helpers, a directory for launch items could be standardised and eventually also protected by prompts.

The decision to notify (not even alert) for only the most visible of login items means that for now BlockBlock is still necessary. Further exasperating the situation is the fact that the list of items doesn’t appear to update very frequently, leading to situations as seen in the item above where many of the launch items don’t exist anymore.

Luckily for attackers, there will always be other ways to persist. Czaba’s series of blog posts features numerous ways to gain execution, and is far from exhaustive. It’s a shame to efforts to tackle persistence abuse miss the majority of abuse scenarios.

Conclusion

It’s clear that TCC will continue to cover more and more of macOS, and the choice to extend it to application bundles is certainly positive for user security. 

At the same time, only covering package-installed applications is disappointing, as the majority of software for macOS is instead distributed in DMG images. More disappointing still is the decision not to cover launch directories with TCC, and a general lack of polish in the user interface for managing such items.