I’m providing an XCode project capable of monitoring the system for TCC events:
Are you annoyed with many prompts that macOS displays do to, well pretty much anything? Blame TCC!
"TCC stands for Transparency Consent and Control and is a framework developed by Apple to manage access to sensitive user data on macOS. The primary goal of TCC is to empower users with transparency regarding how their data is accessed and used by applications." -Stuart Ashenbrenner (Huntress)
TCC, as Stuart notes, aims to protect sensitive data on macOS …largely from malicious code that would otherwise surreptitiously steal it.
You may well know that TCC aims to protect both sensitive files and directories (such as your Documents
directory), built-in devices (such as your mic and webcam), and location data …but it actually covers far more.
Using the following (taken from Stuart’s TCC write-ups), we can extract embedded strings from the TCC daemon (tccd
), that starts with kTCCService
, which are the service identifiers used by TCC to represent specific permission types.
% strings /System/Library/PrivateFrameworks/TCC.framework/Support/tccd | grep -iEo "^kTCCService.*" | sort kTCCServiceAccessibility kTCCServiceAddressBook kTCCServiceAppleEvents kTCCServiceAudioCapture kTCCServiceBluetoothAlways kTCCServiceCalendar kTCCServiceCalls kTCCServiceCamera ... kTCCServiceFaceID ... kTCCServiceMicrophone ... kTCCServicePasteboard kTCCServicePhotos ... kTCCServiceReminders kTCCServiceRemoteDesktop kTCCServiceScreenCapture ...
On my box, over 100 are listed!
Since TCC is designed to protect these resources, it poses a challenge for malware that often tries to access them. Generally malware takes one of two approaches: exploiting TCC bugs, or obtaining explicit user permission access to the TCC protected item. The former is rather uncommon, while the latter is much more widespread as TCC can be trivially “circumvented” if the user acquiesces (sometimes as simply as clicking ‘Allow’ on a single TCC alert).
Let’s look at few examples of malware that exploited TCC shortcomings or bugs, starting with the rather archaic ColdRoot
malware that attempted to take advantage of the fact that on older versions of macOS the TCC database was directly modifiable ๐ซฃ
__const:001D2804 text "UTF-16LE", 'touch /private/var/db/.AccessibilityAPIEnabled && s'
__const:001D2804 text "UTF-16LE", 'qlite3 "/Library/Application Support/com.apple.TCC/'
__const:001D2804 text "UTF-16LE", 'TCC.db" "INSERT or REPLACE INTO access (service, cl'
__const:001D2804 text "UTF-16LE", 'ient, client_type, allowed, prompt_count) VALUES (',27h
__const:001D2804 text "UTF-16LE", 'kTCCServiceAccessibility',27h,', ',27h,0
A more recent example is XCSSET
that included a 0day to directly bypass TCC protections!
It’s far more common for malware to simply defer to the user to approve that (TCC) access either directly, or indirectly when the malware attempts to access the resource.
For example, if we analyze the WindTape malware, we see it will execute macOS’ screencapture binary in order to (continually) capture the user’s screen:
On recent versions of macOS, unless the user grants a TCC exception, this will be thwarted.
Since the majority of macOS malware circumvents TCC through explicit user approval, it would be incredibly helpful for any security tool to detect this โ and possibly override the user’s risky decision. Until now the best (only?) option was to ingest log messages generated by the TCC subsystem. This approach was implemented in a tool dubbed Kronos
, written by Calum Hall Luke Roberts (now, of Phorion fame). Unfortunately, as they note, this approach did have it drawbacks:
"Interfacing with TCC to augment an existing security system has not been easy. The OS does not want to help us. We're using debug log messages from a private Apple service. These might change format (sad regex), introduce new or remove information or even just remove them entirely. " -Calum/Luke
However until now, this log-based approach was arguably the only option.
Of course an obvious solution (as pointed out by many!) was to have Apple’s Endpoint Security generate TCC events. And after many many many years of begging, Apple has heard our pleas and is set to answer our prayers.
In macOS 15.4, Apple will (finally) add TCC events to Endpoint Security …hallelujah!
Let’s now look at the specifics of the new TCC event notification, starting with the event name itself.
ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
EventIn the macOS 15.4 SDK files, specifically EndpointSecurity/ESTypes.h
we find a brand new Endpoint Security event: ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
:
We can subscribe to this event (much like we would for any Endpoint Security event) in the following manner:
es_client_t* client = NULL;
es_event_type_t events[] = {ES_EVENT_TYPE_NOTIFY_TCC_MODIFY};
es_new_client(&client, ^(es_client_t *client, const es_message_t *message) {
//TODO: add code to handle (ES_EVENT_TYPE_NOTIFY_TCC_MODIFY) events
});
es_subscribe(client, events, sizeof(events)/sizeof(events[0])))
We can compile this with XCode 16.3 and run it on a macOS 15.4 (beta) system. Of course, all the standard Endpoint Security “prerequisites” apply. For example, one must be granted the com.apple.developer.endpoint-security.client
entitlement, and, the code must run with root privileges with full disk access.
Now although we have subscribed to the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
event, which was easy enough, we don’t really know what it does (other than assuming from its name that it will be delivered when something triggers TCC a modification …such as approving access to a TCC-protected resource). As such, let’s press on, taking a close look at the details of the event.
es_event_tcc_modify_t
StructureIf you have subscribed to ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
events, when a TCC modification occurs (more on this shortly) an Endpoint Security message (es_message_t
) will be delivered the handler block that was registered (as the final parameter) in the es_new_client
call. For ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
events, the event in the Endpoint Security (es_message_t
) message will be named tcc_modify
and is a es_event_tcc_modify_t
structure:
es_client_t* client = NULL;
es_event_type_t events[] = {ES_EVENT_TYPE_NOTIFY_TCC_MODIFY};
es_new_client(&client, ^(es_client_t *client, const es_message_t *message) {
es_event_tcc_modify_t* event = message->event.tcc_modify;
});
We find the definition (and description) of the es_event_tcc_modify_t
structure in the EndpointSecurity/ESMessage.h
file:
/**
* @brief TCC Modification Event. Occurs when a TCC permission is granted
* or revoked.
*
* @field service The TCC service for which permissions are being modified.
* @field identity The identity of the application that is the subject of the permission.
* @field identity_type The identity type of the application string (Bundle ID, path, etc).
* @field update_type The type of TCC modification event (Grant/Revoke etc)
* @field instigator_token Audit token of the instigator of the modification.
* @field instigator (Optional) The process information for the instigator.
* @field responsible_token (Optional) Audit token of the responsible process for the modification.
* @field responsible (Optional) The process information for the responsible process.
* @field right The resulting TCC permission of the operation/modification.
* @field reason The reason the TCC permissions were updated.
*
* @note This event type does not support caching.
*/
typedef struct {
es_string_token_t service;
es_string_token_t identity;
es_tcc_identity_type_t identity_type;
es_tcc_event_type_t update_type;
audit_token_t instigator_token;
es_process_t *_Nullable instigator;
audit_token_t *_Nullable responsible_token;
es_process_t *_Nullable responsible;
es_tcc_authorization_right_t right;
es_tcc_authorization_reason_t reason;
} es_event_tcc_modify_t;
First and foremost, it sheds light into when exactly the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
event will be delivered: “when a TCC permission is granted or revoked”. As we’ll see this is, yes, rather limited as for example items with TCC entitlements won’t trigger any TCC ES messages.
The header file details each of the members of the es_event_tcc_modify_t
structure making our jobs in understanding this all, rather trivial. And while member of the members are well known ES types (e.g. es_string_token_t
), a few of the members point to other TCC-specify members, so let’s take a look at those now.
First up, we have the identity_type
member, that is a es_tcc_identity_type_t
. The header file reveals that this is a simple enum, that, “Represent[s] the identity type of an application which has access to a TCC service”
typedef enum {
ES_TCC_IDENTITY_TYPE_BUNDLE_ID,
ES_TCC_IDENTITY_TYPE_EXECUTABLE_PATH,
ES_TCC_IDENTITY_TYPE_POLICY_ID,
ES_TCC_IDENTITY_TYPE_FILE_PROVIDER_DOMAIN_ID,
} es_tcc_identity_type_t;
Next, we have the update_type
member that is an es_tcc_event_type_t
. This is another enum, that “Represent[s] the type of TCC modification event.”
/**
* @typedef ess_tcc_event_type_t
*
* Represent the type of TCC modification event.
*
* - ES_TCC_EVENT_TYPE_UNKNOWN: Unknown prior state.
* - ES_TCC_EVENT_TYPE_CREATE: A new TCC authorization record was created.
* - ES_TCC_EVENT_TYPE_MODIFY: An existing TCC authorization record was modified.
* - ES_TCC_EVENT_TYPE_DELETE: An existing TCC authorization record was deleted.
*/
typedef enum {
ES_TCC_EVENT_TYPE_UNKNOWN,
ES_TCC_EVENT_TYPE_CREATE,
ES_TCC_EVENT_TYPE_MODIFY,
ES_TCC_EVENT_TYPE_DELETE,
} es_tcc_event_type_t;
…fairly self-explanatory, ya?
Next we have the right
member. This is a es_tcc_authorization_right_t
that, “represents the type of authorization permission an application has to a TCC Service.”:
/**
* ess_tcc_authorization_right_t
*
* Represents the type of authorization permission an application has to a TCC Service.
*/
typedef enum {
ES_TCC_AUTHORIZATION_RIGHT_DENIED, // 0
ES_TCC_AUTHORIZATION_RIGHT_UNKNOWN, // 1
ES_TCC_AUTHORIZATION_RIGHT_ALLOWED, // 2
ES_TCC_AUTHORIZATION_RIGHT_LIMITED, // 3
ES_TCC_AUTHORIZATION_RIGHT_ADD_MODIFY_ADDED, // 4
ES_TCC_AUTHORIZATION_RIGHT_SESSION_PID, // 5
ES_TCC_AUTHORIZATION_RIGHT_LEARN_MORE, // 6
} es_tcc_authorization_right_t;
Finally, in the es_event_tcc_modify_t
structure, we have the reason
member, whose type is es_tcc_authorization_reason_t
. This, “represents the reason a TCC permission was updated”:
/**
* ess_tcc_authorization_reason_t
*
* Represents the reason a TCC permission was updated.
*/
typedef enum {
ES_TCC_AUTHORIZATION_REASON_NONE = 0,
ES_TCC_AUTHORIZATION_REASON_ERROR,
ES_TCC_AUTHORIZATION_REASON_USER_CONSENT, /// User answered a prompt
ES_TCC_AUTHORIZATION_REASON_USER_SET, /// User changed the authorization right via Preferences
ES_TCC_AUTHORIZATION_REASON_SYSTEM_SET, /// A system process changed the authorization right
ES_TCC_AUTHORIZATION_REASON_SERVICE_POLICY,
ES_TCC_AUTHORIZATION_REASON_MDM_POLICY,
ES_TCC_AUTHORIZATION_REASON_SERVICE_OVERRIDE_POLICY,
ES_TCC_AUTHORIZATION_REASON_MISSING_USAGE_STRING,
ES_TCC_AUTHORIZATION_REASON_PROMPT_TIMEOUT,
ES_TCC_AUTHORIZATION_REASON_PREFLIGHT_UNKNOWN,
ES_TCC_AUTHORIZATION_REASON_ENTITLED,
ES_TCC_AUTHORIZATION_REASON_APP_TYPE_POLICY,
ES_TCC_AUTHORIZATION_REASON_PROMPT_CANCEL,
} es_tcc_authorization_reason_t;
Ok, enough with header files and enums, let’s write some code in the es_new_client
handler block to parse out the contents of the es_event_tcc_modify_t
for each ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
message.
First, let’s just print out the process that triggered the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
event. This can be found directly in the es_message_t
, and is delivered for any/all ES events, regardless of their type:
es_process_t* process = message->process;
NSLog(@"Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message");
NSLog(@"process: %d:%@", audit_token_to_pid(process->audit_token), [NSString stringWithESToken:process->executable->path]);
You’ll see we converted the process’ audit token to a process id via macOS’s audit_token_to_pid
function. Also take note of the custom stringWithESToken:
category that I added to the NSString
as a simple way to convert an ES string (es_string_token_t
) to a more manageable Objective-C NSString
object:
@implementation NSString (ESToken)
+ (instancetype)stringWithESToken:(es_string_token_t)token {
if (token.length == 0 || token.data == NULL) {
return @"";
}
return [[NSString alloc] initWithBytes:token.data
length:token.length
encoding:NSUTF8StringEncoding];
}
@end
Now, into the specifics of parsing out the members of the es_event_tcc_modify_t
structure, starting with the service
and identity
members:
es_event_tcc_modify_t* event = message->event.tcc_modify;
...
NSString* service = [NSString stringWithESToken:event->service];
NSString* identity = [NSString stringWithESToken:event->identity];
NSLog(@"service: %@", service);
NSLog(@"identity: %@", identity);
As these are simple ES strings (type: es_string_token_t
), we can use the same stringWithESToken:
helper category to parse them out (into Obj-C objects).
Next we have the identity_type
, which is a es_tcc_identity_type_t
. To covert each value of the es_tcc_identity_type_t
enum to a human readable string, ChatGPT wrote me the following helper function:
static NSString *NSStringFromTCCIdentityType(es_tcc_identity_type_t type) {
switch (type) {
case ES_TCC_IDENTITY_TYPE_BUNDLE_ID:
return @"Bundle ID";
case ES_TCC_IDENTITY_TYPE_EXECUTABLE_PATH:
return @"Executable Path";
case ES_TCC_IDENTITY_TYPE_POLICY_ID:
return @"Policy ID";
case ES_TCC_IDENTITY_TYPE_FILE_PROVIDER_DOMAIN_ID:
return @"File Provider Domain ID";
default:
return [NSString stringWithFormat:@"Unknown (%d)", type];
}
}
We can now invoke it as such:
NSLog(@"identity type: %@", NSStringFromTCCIdentityType(event->identity_type));
We take a similar approach for the update_type
member, using another helper function (thanks ChatGPT!), that given an es_tcc_event_type_t
prints out a human readable update type (such as “ES_TCC_EVENT_TYPE_CREATE” or “ES_TCC_EVENT_TYPE_DELETE”):
NSLog(@"update type: %@", NSStringFromTCCEventType(event->update_type));
Next, we have a few optional members of the es_event_tcc_modify_t
that means they can be NULL (specifically, they are declared as _Nullable
). These include:
es_process_t *_Nullable instigator;
audit_token_t *_Nullable responsible_token;
es_process_t *_Nullable responsible;
The instigator
is a pointer to a standard ES process structure (es_process_t
) and is “the process information for the instigator” which, we’ll see is the process that was the initiator of the ES TCC message. (Note: This may be different from the process (such as tccd
) that actually submits the ES event to the ES subsystem).
Next is an (optional) audit token (responsible_token
) and pointer to another (optional) process structure (responsible
) that though is often NULL, when set, represents the original process that triggered the TCC event in the first place.
As these optional members are standard types, we can parse them out as follows …though making sure to only do so if they are non-NULL:
if(NULL != event->instigator) {
NSLog(@"instigator process: %@", [NSString stringWithESToken:event->instigator->executable->path]);
}
if(NULL != event->responsible_token) {
NSLog(@"responsible process pid: %d", audit_token_to_pid(*event->responsible_token));
}
if(NULL != event->responsible) {
NSLog(@"responsible process: %@", [NSString stringWithESToken:event->responsible->executable->path]);
}
Finally, the es_event_tcc_modify_t
structure ends with an authorization right and reason (aptly named right
and reason
). Just as was the case with other ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
ES-specific types, we invoke (ChatGPT created) helper function to convert the es_tcc_authorization_right_t
and es_tcc_authorization_reason_t
enum values to human readable strings:
NSLog(@"right: %@", NSStringFromTCCAuthorizationRight(message->event.tcc_modify->right));
NSLog(@"reason: %@", NSStringFromTCCAuthorizationReason(message->event.tcc_modify->reason));
Let’s build and test our code, both to ensure it’s working, but also get more insight into the members of the es_event_tcc_modify_t
structure.
Before we run it against some malware, let’s start with legitimate software โ to highlight that TCC events can also be generated for, wellโฆ not malware.
First, let’s open TextEdit, which triggers the following:
# EndpointSecurity_TCC.app/Contents/MacOS/EndpointSecurity_TCC Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 448:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: Ubiquity identity: com.apple.TextEdit identity type: Bundle ID update type: Create instigator process: /System/Library/PrivateFrameworks/iCloudDriveCore.framework/Versions/A/Support/bird responsible process pid: 1341 responsible process: /System/Applications/TextEdit.app/Contents/MacOS/TextEdit right: Allowed reason: Service Policy
After printing out a simple message that a ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
event was received, we print the process that generated the ES message. No surprises here it’s the system TCC daemon: /System/Library/PrivateFrameworks/TCC.framework/Support/tccd
. The remaining output shows that due to system policy (ES_TCC_AUTHORIZATION_REASON_SYSTEM_SET
), TextEdit
is allowed to access iCloud data (service
: Ubiquity
).
Looking at the es_tcc_authorization_reason_t enum, we see values such as ES_TCC_AUTHORIZATION_REASON_ENTITLED. Many Apple binaries have TCC entitlements such as:
“com.apple.private.tcc.allow” =
(
kTCCServiceMicrophone,
kTCCServiceCamera,
kTCCServiceScreenCapture
);
However, if you execute such binaries, no TCC events are currently delivered to the Endpoint Security subsystem and therefore, not to ES clients either.
Let’s continue testing with KnockKnock, an Objective-See tool that scans your system for persistently installed software, including persistent malware. It works best when granted Full Disk Access (kTCCServiceSystemPolicyAllFiles
). As such, let’s grant it FDA, while running our TCC monitor:
# EndpointSecurity_TCC.app/Contents/MacOS/EndpointSecurity_TCC Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 154:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: SystemPolicyAllFiles identity: com.objective-see.KnockKnock identity type: Bundle ID update type: Modify instigator process: /System/Library/ExtensionKit/Extensions/SecurityPrivacyExtension.appex/Contents/MacOS/SecurityPrivacyExtension right: Allowed reason: User Set (via Preferences)
As I manually granted KnockKnock FDA via the Security and Privacy pane of the System Settings app, it make sense that the reported process is SecurityPrivacyExtension.appex
(Generally speaking, most panes in the System Settings app are backed by individual .appex bundles that run as standalone processes.)
Next, we see the service
member (which recall is standard ES string) contains the value SystemPolicyAllFiles
which maps to the TCC policy (kTCCServiceSystemPolicyAllFiles
) for Full Disk Access (FDA).
Next we the identity
and its type that, well, identify the item that was granted FDA. As TCC generally tracks items by bundle ID (vs. path that could easily change, if for example the item was moved), we see KnockKnock’s bundle ID, and identity type set to ES_TCC_IDENTITY_TYPE_BUNDLE_ID
which we covered to the string Bundle ID
.
Following this, we have the update type (event->update_type
), which was reported as ES_TCC_EVENT_TYPE_MODIFY
(which we converted to Modify
). I would have though that as KnockKnock was new, ES would have reported ES_TCC_EVENT_TYPE_CREATE
…but, that’s what we were given.
The optional instigator process
is SecurityPrivacyExtension.appex
. As I manually granted KnockKnock FDA via the Security and Privacy pane of the System Settings app, it make sense that the instigator process is SecurityPrivacyExtension.appex
(Generally speaking, most panes in the System Settings app are backed by individual .appex bundles that run as standalone processes.)
Since the responsible process is NULL, output ends with the right and reason. The right was reported as ES_TCC_AUTHORIZATION_RIGHT_ALLOWED
(that we converted to the string Allow
), which make sense as I granted KnockKnock FDA. And the reason was reported as ES_TCC_AUTHORIZATION_REASON_USER_SET
, meaning the user performed the action via the System Preferences app.
Just to confirm that our code is correct, we can use macOS’s built-in (though closed source) eslogger
tool that we execute from a root prompt, along with the tcc_modify
commandline option.
# eslogger tcc_modify
{
"process": {
...
"signing_id": "com.apple.tccd",
"executable": {
"path": "\/System\/Library\/PrivateFrameworks\/TCC.framework\/Support\/tccd",
},
},
"event_type": 147,
"event": {
"tcc_modify": {
"instigator_token": {
...
},
"instigator": {
"signing_id": "com.apple.settings.PrivacySecurity.extension",
"executable": {
"path": "\/System\/Library\/ExtensionKit\/Extensions\/SecurityPrivacyExtension.appex\/Contents\/MacOS\/SecurityPrivacyExtension"
}
},
"reason": 3,
"identity_type": 0,
"identity": "com.objective-see.KnockKnock",
"responsible_token": null,
"responsible": null,
"service": "SystemPolicyAllFiles",
"right": 2,
"update_type": 2
}
},
...
}
Hooray, everything matches (though yes, eslogger
merely displays enum values in their native numeric format).
Now let’s remove KnockKnock’s FDA:
# EndpointSecurity_TCC.app/Contents/MacOS/EndpointSecurity_TCC Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 154:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: SystemPolicyAllFiles identity: com.objective-see.KnockKnock identity type: Bundle ID update type: Modify instigator process: /System/Library/ExtensionKit/Extensions/SecurityPrivacyExtension.appex/Contents/MacOS/SecurityPrivacyExtension right: Denied reason: User Set (via Preferences)
Everything is the same as when we granted it FDA, except the event->right
is set to denied (ES_TCC_AUTHORIZATION_RIGHT_DENIED
).
Now, let’s test some malware, starting with JokerSpy
, which rather uniquely contains a component (xcc
) specifically designed to query TCC (which is nicely prints to standard out):
% /Users/user/Downloads/JokerSpy/xcc Idle Time: 0.035372875 Active App: Terminal The screen is currently UNLOCKED! FullDiskAccess: YES ScreenRecording: NO Accessibility: NO
Although not all these checks trigger ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
events, at least one, the accessibility check does:
# EndpointSecurity_TCC.app/Contents/MacOS/EndpointSecurity_TCC Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 154:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: Accessibility identity: com.apple.Terminal identity type: Bundle ID responsible process is NULL update type: Modify instigator process: /System/Library/PrivateFrameworks/UniversalAccess.framework/Versions/A/Resources/universalAccessAuthWarn.app/Contents/MacOS/universalAccessAuthWarn right: Denied reason: User Set (via Preferences)
Unfortunately the information provided in the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
message/event is a bit …confusing? First, there is nothing tying the event to the actual malicious binary/process xcc
. And though, yes xcc
was run via the Terminal, neither the identity
, instigator
, nor responsible
(which here is NULL) references the malware. So how’s a security tool supposed to map the TCC check to the malware? Good question, I don’t know ๐คท๐ปโโ๏ธ.
It is well understood that when one runs a command-line utility that requires TCC access / permissions, one has to grant Terminal.app the permission (e.g. Full Disk Access to run an ES client). As such, it does make sense why Terminal (com.apple.Terminal
) is identified in the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY ES message. However, the point still stands that it would be immensely helpful to also have the message include the actual binary (e.g. JokerSpy/xcc).
Also, the reported reason (ES_TCC_AUTHORIZATION_REASON_USER_SET
) โ which, according to a comment in ESTypes.h
, is used when “the user changed the authorization right via Preferences” โ doesn’t seem particularly accurate in this context, since it was the malware, not a user, that performed a check rather than setting an authorization.
You should also be aware that TCC ES messages may be delivered as one expects. For example, if we execute the WindTape
malware, we can see it executes macOS’ built-in screencapture
utility in order grab a screen capture …normally something protected by TCC.
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "pid" : 2562, "name" : "screencapture", "path" : "/usr/sbin/screencapture", "arguments" : [ "/usr/sbin/screencapture", "-x", "-C", "/Users/user/Library/lsd.app/Contents/Resources/27-03-2025 09:39:59.jpg" ], ... } }
However no TCC ES messages appear to be generated. Digging into the system logs, we see:
tccd Service kTCCServiceScreenCapture does not allow prompting; returning denied.
Recall the header files state that the ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
is only delivered when a “TCC permission is granted or revoked”. As no prompt was shown and the TCC just flat out denied the screen capture all together, this explains why no ES message was delivered to our ES client (that’s listening for ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
). Still, it’d be nice to know this via ES!
However if we manually execute screencapture
from the Terminal, first an TCC alert is shown:
This generates a ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
(right
: Denied
) …and if we approve screencapture
, another (right
: Approved
):
# EndpointSecurity_TCC.app/Contents/MacOS/EndpointSecurity_TCC Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 154:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: ScreenCapture identity: com.apple.Terminal identity type: Bundle ID update type: Modify instigator process: /System/Library/PrivateFrameworks/UniversalAccess.framework/Versions/A/Resources/universalAccessAuthWarn.app/Contents/MacOS/universalAccessAuthWarn responsible process is NULL right: Denied reason: User Set (via Preferences) Received 'ES_EVENT_TYPE_NOTIFY_TCC_MODIFY' message process: 154:/System/Library/PrivateFrameworks/TCC.framework/Support/tccd service: ScreenCapture identity: com.apple.Terminal identity type: Bundle ID update type: Modify instigator process: /System/Library/PrivateFrameworks/UniversalAccess.framework/Versions/A/Resources/universalAccessAuthWarn.app/Contents/MacOS/universalAccessAuthWarn responsible process is NULL right: Allowed reason: User Set (via Preferences)
In macOS 15.4, Apple will (finally!) add support for TCC events via Endpoint Security! Hooray ๐ฅณ
However, we showed that currently there is only a single such event, ES_EVENT_TYPE_NOTIFY_TCC_MODIFY
, which (at least in the 15.4 betas) seems perhaps a bit incomplete, or at the very least is rather nuanced.
Hopefully Apple continues iron out any wrinkles before the official release of 15.4 as well as continues to improve TCC visibility and interactions via Endpoint Security. I mean a ES_EVENT_TYPE_AUTH_TCC_*
would be delightful, right? ๐ค๐ฝ