Bypassing TCC: Three Ways Around macOS Privacy Controls
TCC (Transparency, Consent, and Control) is the system behind every “App X would like to access your Camera” prompt. It gates the microphone, camera, contacts, calendar, photos, and the sensitive parts of your filesystem. The promise is simple: an app touches a protected resource only if you said yes. The reality is that TCC binds permissions to an application’s identity, and identity on macOS is more forgeable, more inheritable, and more delegable than the prompt suggests.
This post covers how TCC actually stores and checks consent, then three real bypasses that each
attack a different weak point: a full bypass through coreaudiod that uses a private TCC API to
grant yourself permissions (CVE-2020-29621), an inheritance bypass through Spotlight importer
plugins, and an injection bypass that steals Signal’s microphone access (CVE-2020-24259). The
sandbox post ended on a prediction: find a process that already has the permission and get it to
act for you. TCC is where that prediction pays off three times.
How TCC Works
TCC is managed by tccd, a daemon living in the private TCC.framework. There are two
instances: a system-wide one running as root
(com.apple.tccd.system), and a per-user one for each logged-in user. Both expose XPC Mach
services that clients hit when they want a protected resource. When an app tries to use the
microphone, the request flows to tccd, which checks its records and either allows, denies, or
prompts.
The records live in SQLite consent databases, and their differing protection is the first thing an attacker maps:
# filename: tcc-dbs.txt
System DB: /Library/Application Support/com.apple.TCC/TCC.db (SIP-protected; only tccd writes)
Per-user DB: $HOME/Library/Application Support/com.apple.TCC/TCC.db (less protected)
The system database is SIP-protected: even root cannot add entries directly; only tccd may. The
per-user database is softer: with Full Disk Access (or, until Big Sur 11.3, an SSH session,
which carried FDA implicitly) you can open and read it. Inside, each row ties a service to a
bundle identifier and an allow/deny decision. The service names are kTCCService* constants:
# filename: tcc-services.txt
kTCCServiceMicrophone kTCCServiceCamera kTCCServicePhotos
kTCCServiceAddressBook kTCCServiceCalendar kTCCServiceAccessibility
kTCCServiceSystemPolicyAllFiles (Full Disk Access) kTCCServiceAppleEvents
A real row looks like kTCCServiceMicrophone|us.zoom.xos|0|2|4|1|...: Zoom is allowed the
microphone. And here is the structural weakness that all three bypasses exploit: a permission is
granted to a bundle identifier, and the security of that grant is only as strong as the security of
the binary claiming that identity. TCC trusts that the process running as us.zoom.xos really is
Zoom and really is the code Zoom shipped. Break either assumption and the permission is yours.
Some permissions (Microphone, Camera) can be granted by a regular user via a prompt. Others (Full Disk Access) require admin authentication and a manual add in System Preferences. The distinction matters: the easy-to-grant services are also the easy-to-abuse ones, because the bar for an app to request them is low.
The Private API: CVE-2020-29621 (coreaudiod)
The cleanest bypass does not forge anything: it asks TCC to grant the permission, using an API
Apple meant to keep to itself. The target is coreaudiod, the core audio daemon, and the path
runs through its entitlements.
coreaudiod is extensible: third parties can ship audio drivers and plugins that it loads. To make
that work, it carries com.apple.security.cs.disable-library-validation:
# filename: coreaudiod-ent.txt
% codesign -dv --entitlements :- /usr/sbin/coreaudiod
Identifier=com.apple.audio.coreaudiod
[Key] com.apple.security.cs.disable-library-validation
That entitlement, the same one from the dylib-injection post, means coreaudiod will load a
plugin not signed by Apple. And any code loaded into coreaudiod inherits coreaudiod’s
privileges and TCC standing. So if you can get a plugin loaded into it, your code runs with the
audio daemon’s identity.
The second half is the private API. Searching the system binaries for the symbol
_TCCAccessSetForBundleId turns it up in syspolicyd, and reverse-engineering the call recovers
its signature:
// filename: tcc-private-api.c
// Private. Grants a TCC service to an arbitrary bundle id.
int (*_TCCAccessSetForBundleId)(CFStringRef service, CFStringRef bundleId, int granted);
This function writes a grant into TCC. It is exactly the operation the system uses internally to record consent, exposed as a callable symbol. Put the two halves together:
- Build an audio plugin and drop it in
/Library/Audio/Plug-Ins/HAL/(loading drivers needs root, but root is a much lower bar than defeating SIP). Rather than a full driver, the plugin is just a__attribute__((constructor)): the function-hooking post’s trick. - When
coreaudiodloads the plugin, the constructor runs inside the audio daemon and calls_TCCAccessSetForBundleIdto grant any service to any bundle id.
The result is a full TCC bypass: you grant yourself camera, microphone, or whatever you like, through Apple’s own internal grant routine, executed from inside a daemon that was happy to load your unsigned code. No prompt, no user, no forgery: you used the supported (private) machinery for exactly its intended purpose, just not by the intended caller.
The Inheritance Bug: Spotlight Importers
The second bypass exploits how a system process loads attacker-controlled plugins. Spotlight
indexes file contents by parsing them with importer plugins: .mdimporter bundles run by the
metadata server mds. The OS ships importers in /System/Library/Spotlight, but users can add
their own in /Library/Spotlight, ~/Library/Spotlight, or inside an app bundle’s
Contents/Library/Spotlight/. mdimport -L lists the registered set.
The bug is the same shape as the QuickLook sandbox escape: a system process loads a plugin you
control, and your code inherits that process’s privileges. When mds (or its workers) loads your
malicious .mdimporter to index a file, your code runs in a context with Spotlight’s TCC standing
and filesystem reach: broader than your sandboxed app’s. You register an importer for a file type,
get the indexer to process a file of that type, and your plugin code executes inside the privileged
indexer. The permission was never yours; you borrowed the indexer’s by getting your code to run as
the indexer. This is the “become a more-privileged process” pattern from the sandbox post, retargeted
at TCC.
The Injection Bug: CVE-2020-24259 (Signal)
The third bypass is the most direct. TCC granted Signal microphone access: the user approved it, legitimately. Signal is a normal, trusted app with a real reason to use the mic. The attack is to become Signal.
Signal’s entitlements are the giveaway:
# filename: signal-ent.txt
% codesign -dv --entitlements :- /Applications/Signal.app
Identifier=org.whispersystems.signal-desktop
[Key] com.apple.security.cs.disable-library-validation
[Key] com.apple.security.cs.allow-dyld-environment-variables
[Key] com.apple.security.device.microphone
Three entitlements line up into an exploit. device.microphone is the permission you want.
disable-library-validation means Signal loads dylibs not signed by its team. And
allow-dyld-environment-variables means DYLD_INSERT_LIBRARIES works against it despite hardened
runtime. So you inject your own dylib into Signal (straight DYLD_INSERT_LIBRARIES, or a dylib
proxying attack against one of the 39 dylibs in its bundle) and your code runs as Signal, with
org.whispersystems.signal-desktop’s microphone grant. TCC sees Signal asking for the mic, which is
allowed, and hands over the audio. The microphone is recording for an attacker, authorized by a
permission the user granted to a completely different, trusted application.
This is the binding weakness in its purest form. TCC’s grant is to an identity; injection lets you assume the identity; the permission follows. An Electron app with a plugin-friendly entitlement set and a sensitive permission is the ideal victim, and there are many.
Why This Matters
Three bypasses, three different mechanisms (a private grant API, plugin inheritance, code injection) and one root cause: TCC ties permissions to application identity, and macOS gives an attacker
several ways to wear an application’s identity. coreaudiod let you run code that called the grant
function directly. Spotlight let you run code as the privileged indexer. Signal let you run code as
Signal itself. In every case the permission check was working correctly; the attacker simply made
the check evaluate the wrong code.
The defensive lesson is uncomfortable: a permission system that keys off identity cannot be stronger than the integrity of that identity, and “this process is really the app it claims to be” is exactly what injection, plugin loading, and library-validation opt-outs undermine. TCC has been hardened repeatedly (locking the system DB behind SIP, removing SSH’s implicit FDA) but the structural issue persists wherever an app holds both a sensitive permission and a feature that lets foreign code run under its name.
There is one more identity to forge, and it is the filesystem’s. The next post looks at symlink and hardlink attacks, where the trick is not impersonating an app but confusing privileged code about which file a path actually points to.
References
- MITRE. CVE-2020-29621 (coreaudiod), CVE-2020-24259 (Signal).
- Apple. TCC.framework, Spotlight Importer Programming Guide. developer.apple.com.
- Wardle, P. / Various. TCC research and consent database internals.