• Objective-See
    a non-profit 501(c)(3) foundation.
    • About
    • #OBTS
    • Book Series
    • Objective-We
    • Our Store/Swag
    • Malware Collection
  • blog
  • tools

TCCing is Believing
Apple finally adds TCC events to Endpoint Security!
by: Patrick Wardle / March 27, 2025

The Objective-See Foundation is supported by:

Kandji

Jamf


MoonLock (by MacPaw)
Palo Alto Networks

iVerify

Huntress


Want to play along?

I’m providing an XCode project capable of monitoring the system for TCC events:

EndpointSecurity_TCC.xcodeproj


Note: You will have to compile this with the macOS 15.4 (beta) SDK!

Background

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 can read all about TCC here:

“Full Transparency: Controlling Apple’s TCC”

“Full Transparency: Controlling Apple’s TCC Part II”

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!

Fun fact, its not just malware that attempts to circumvent TCC, remember when DropBox did too?

“Discovering how Dropbox Hacks your Mac”

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:

WindTape spawns screencapture in order to generate a screen shot

On recent versions of macOS, unless the user grants a TCC exception, this will be thwarted.

TCC Events in Endpoint Security (ES)

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.

To learn more about this approach, see Calum and Luke's #OBTS v6.0 talk:

“The Clock is TCCing”

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!

As macOS 15.4 is not yet released, here we're using macOS 15.4 developer beta 4 (24E5238a), and XCode 16.3 beta 3

Let’s now look at the specifics of the new TCC event notification, starting with the event name itself.


The ES_EVENT_TYPE_NOTIFY_TCC_MODIFY Event

In the macOS 15.4 SDK files, specifically EndpointSecurity/ESTypes.h we find a brand new Endpoint Security event: ES_EVENT_TYPE_NOTIFY_TCC_MODIFY:

A new ES event: ES_EVENT_TYPE_NOTIFY_TCC_MODIFY

Note that the header file explicitly states the event is (only) "available beginning in macOS 15.4"

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.

The es_event_tcc_modify_t Structure

If 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;
        
});
    
Shortly, we'll add some code to dump all members of the es_event_tcc_modify_t. But first, let's take a look at the Endpoint Security header file(s) to get the "official" scoop.

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));

Testing

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:

Granting KnockKnock Full Disk Access

#  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:

A TCC alert is displayed when screencapture is (manually) spawned

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)

Conclusion

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? ๐Ÿคž๐Ÿฝ

๐Ÿ’• Support:

Love these blog posts? You can support them via my Patreon page!



This website uses cookies to improve your experience.
  • ✉
  • 
  • 
  • 
  • Signup for our newsletter ยป