ClickFix has quickly become a widely adopted infection technique targeting both macOS and Windows users. Rather than relying on software vulnerabilities or exploit chains, it leverages something much simpler: convincing a user to copy and paste a command into a terminal.
The mechanics are straightforward, yet the impact can be significant, allowing attackers to bypass even OS-level protections such as Gatekeeper and Notarization on macOS. This technique is now being leveraged not only by opportunistic cybercriminals, but also by more sophisticated threat actors. As its adoption increases, it becomes important to examine practical ways to reduce its effectiveness. (You can read more about ClickFix attacks in MacPaw’s Moonlock Labs write-up: “How ClickFix attacks trick users and infect devices with malware”).
Long before macOS 26.4 (ok, like a month 😄), when Apple added native ClickFix protection, I had already added ClickFix protection to BlockBlock:
Motivated by @moonlock_lab’s recent findings on ClickFix attacks surfacing via Google Search results and even LLMs 🤯, I’ve added basic protection against most of these attacks to BlockBlock 🍎🛡️
— Patrick Wardle (@patrickwardle) February 16, 2026
Read: “ClickFix: Stopped at ⌘+V”https://t.co/Ix9kHVr6UW
And since then, it has proven its worth, detecting even brand new attacks and malware, such as the recently discovered Infiniti Stealer:
BlockBlock's 'ClickFix' protection vs. the (new) Infiniti Stealer 😎
— Patrick Wardle (@patrickwardle) March 27, 2026
BlockBlock is free and open-source 😇https://t.co/NaribLBa8P https://t.co/EpRuyyJyEa pic.twitter.com/xDwJtVqZYs
In short, BlockBlock leverages the NSEvent class’s addGlobalMonitorForEventsMatchingMask:handler: method to detect when ⌘ (Command)+V is pressed:
1[NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
2 handler:^(NSEvent *event) {
3
4 if ((event.modifierFlags & NSEventModifierFlagCommand) &&
5 ([[event.charactersIgnoringModifiers lowercaseString] isEqualToString:@"v"])) {
6
7 // Command+V was pressed!
8 // ...is it malicious, or not?
9 }
10
11}];…and while this is a decent approach, it’s not ideal, as it’s somewhat reactive and requires a component running in the user’s session. The real solution was, as noted, a dedicated Endpoint Security (ES) paste event:
However, at the time of adding ClickFix protection to BlockBlock there was no public Endpoint Security events related to the clipboard (pasting).
In macOS 26.4 several undocumented ES_EVENT_TYPE_RESERVED_* ones were added.
…turns out, as we’ll see, Apple agreed that ClickFix protections should be built atop Endpoint Security—but, as is often the case, only for themselves 😓
In macOS 26.4, as Mr. Macintosh pointed out, Apple added native ClickFix protection, built directly into the OS:
New warning in macOS Tahoe 26.4
— Mr. Macintosh (@ClassicII_MrMac) March 25, 2026
You will now be warned when you paste terminal commands from Safari or other apps, flagging anything that could harm your Mac.
I'm really glad Apple added this. It’s a small but helpful change that protects users. pic.twitter.com/Gecbt3KKdD
Shortly after releasing the ClickFix protection in BlockBlock, Koh Nakagawa, (who had recently gave a great talk at #OBTS 8 on “XUnprotect: Reverse Engineering macOS XProtect Remediator”) DM’d me the following:
"I recently found a new ES event called es_event_paste_t. You can find this string in the newly added XProtect component, `xprotectd` (located in /usr/libexec). " -Koh
And indeed, this was the case:
% strings - /usr/libexec/xprotectd | grep -i paste es_event_paste_t
…this was on macOS 26.3, before macOS 26.4 (which saw native ClickFix protection in macOS).
I was intrigued and dug into this further. This blog post shares my findings, now expanded to reflect the broader implementation in macOS 26.4.
Other regular #OBTS speaker, Ferdous Saljooki, recently posted an informative thread on X, that gives a solid overview of Apple’s implementation.
Much of the findings here collaborate his, and built upon them to provide additional technical insight.
My goals were as follows:
The last was really my main motivation!
Ok, enough yapping—let’s dive into some reversing.
As Koh (and Sal) noted, we can find references to paste protection in xprotectd, the XProtect daemon. As of macOS 26.4, there are a myriad of strings related to Apple’s new ClickFix protection:
% strings - /usr/libexec/xprotectd | grep -i paste PasteContent PasteEvent es_event_paste_t PasteBlockAlertDisplayer [ApplePlatformTracer] Set deny-paste: [ApplePlatformTracer] Set paste summary: com.apple.XProtect.PasteProtection Prompting user about blocked paste event Paste into terminal ...
As xprotectd is written in Swift 🤮, it’s a pain to statically reverse. For example, although we see the es_event_paste_t string, there are no cross-references to it:
So instead, let’s debug it!
In a macOS 26.4 VM, we disable SIP (via csrutil disable in Recovery Mode), and after booting back in macOS 26.4, disable AMFI too:
# nvram boot-args="amfi_get_out_of_my_way=1"
This allows us to debug Apple system binaries, such as xprotectd, and modify entitlements (which we’ll get to shortly!).
The XProtect daemon is managed by its Launch Daemon property list: com.apple.security.xprotectd.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Program</key>
<string>/usr/libexec/xprotectd</string>
<key>ProcessType</key>
<string>Interactive</string>
<key>EnablePressuredExit</key>
<false/>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.apple.security.xprotectd</string>
</dict>
</plist>
Referencing this plist, we can unload xprotectd (again, only possible if SIP is off):
# launchctl unload /System/Library/LaunchDaemons/com.apple.security.xprotectd.plist
…which then allows us to start is under a debugging session:
# lldb /usr/libexec/xprotectd (lldb) target create "/usr/libexec/xprotectd" Current executable set to '/usr/libexec/xprotectd' (arm64e).
Hooray, we’re all set to debug to our heart’s content. But wait—where, or rather on what, should we set breakpoints?
Going on the reasonable assumption that Apple’s ClickFix protection is built atop a new Endpoint Security event, we can set a breakpoint on calls to es_subscribe and observe which events xprotectd subscribes to. The es_subscribe function is invoked by ES clients and tells the ES subsystem which events they’re interested in receiving callbacks for. (Thus, if xprotectd is subscribing to a new ES paste-related event, that would be apparent in the es_subscribe call.)
Here’s the function declarion:
1es_return_t es_subscribe(es_client_t *client, const es_event_type_t *events, uint32_t event_count);Most notably it takes an array of events ES (and and count) subscribe to (which, in theory for xprotectd should include a new paste event!?).
Let’s set a breakpoint on es_subscribe:
(lldb) b es_subscribe Breakpoint 11: where = libEndpointSecurity.dylib`es_subscribe, address = 0x00000001a479b61c
With our breakpoint set, if we allow xprotectd to execute, our breakpoint hits!
Recalling the function definition, we can dump the number of ES events and then the actual events that es_subscribe was called with:
(lldb)
* thread #3, name = 'Task 1', queue = 'com.apple.xprotectd.ContainerClient', stop reason = breakpoint 1.1
frame #0: 0x00000001a479b61c libEndpointSecurity.dylib`es_subscribe
libEndpointSecurity.dylib`es_subscribe:
-> 0x1a479b61c <+0>: pacibsp
Target 0: (xprotectd) stopped.
(lldb) reg read $x2
x2 = 0x0000000000000007
(lldb) x/7dw $x1
0x10006d7d8: 0
0x10006d7dc: 9
0x10006d7e0: 11
0x10006d7e4: 15
0x10006d7e8: 10
0x10006d7ec: 13
0x10006d7f0: 68
Ok, so here, es_subscribe is being called with 7 events ($x2 holds value of the 3rd argument, uint32_t event_count), which we then print out ($x1 holds the 2nd argument, es_event_type_t *events).
We can then map these numbers, 0, 9 …68, to the ES events found in ESTypes.h:
0 — ES_EVENT_TYPE_AUTH_EXEC
9 — ES_EVENT_TYPE_AUTH_OPEN
10 — ES_EVENT_TYPE_AUTH_RENAME
11 — ES_EVENT_TYPE_AUTH_SIGNAL
13 — ES_EVENT_TYPE_AUTH_UNLINK
15 — ES_EVENT_TYPE_AUTH_MMAP
68 — ES_EVENT_TYPE_AUTH_COPYFILE
OK, those make sense for xprotectd to subscribe to, but they are all well-known ES events—nothing new, undocumented, or paste-related. Let’s press on.
After a few more (irrelevant) calls to es_subscribe, we strike gold!
(lldb)
* thread #3, name = 'Task 1', queue = 'com.apple.xprotectd.PlatformMonitor', stop reason = breakpoint 1.1
frame #0: 0x00000001a479b61c libEndpointSecurity.dylib`es_subscribe
libEndpointSecurity.dylib`es_subscribe:
-> 0x1a479b61c <+0>: pacibsp
Target 0: (xprotectd) stopped.
(lldb) reg read $x2
x2 = 0x0000000000000003
(lldb) x/3dw $x1
0x100072010: 149
0x100072014: 148
0x100072018: 15
ES event 15, is ES_EVENT_TYPE_AUTH_MMAP, but 148 and 149 maps to ES_EVENT_TYPE_RESERVED_0 and ES_EVENT_TYPE_RESERVED_1. Intriguing!
Ok, so we can see xprotectd is subscribing to two new, undocumented (“reserved”) Endpoint Security events. Next, we need to figure out what they are!
In a previous blog post, “Building a Firewall …via Endpoint Security!?”, I showed how to reverse ES_EVENT_TYPE_RESERVED_5 and ES_EVENT_TYPE_RESERVED_6, which turned out to be network connection AUTH and NOTIFY events, providing a hook into all outbound network connections. In that case, it was relatively “easy” as we could subscribe to those events and then simply dump the ES message (and event-specific data) when networking events occurred.
Unfortunately, we can’t do that here. If you try to subscribe to ES_EVENT_TYPE_RESERVED_0 or ES_EVENT_TYPE_RESERVED_1, even with a properly entitled ES client, the subscription will fail:
# sudo ./esClient subscribing to RESERVED_0 (type=148): failed (result=1) subscribing to RESERVED_1 (type=149): failed (result=1)
So, instead, let’s look at how xprotectd processes events of this type. To do that, we need to talk about the es_new_client function, which, as Apple notes, “creates a new client instance and connects it to the Endpoint Security system.” Here’s its declaration:
1es_new_client_result_t es_new_client(es_client_t **client, es_handler_block_t handler);ES clients should invoke this function to obtain an instance of an ES client (which they then pass to subsequent ES APIs, such as es_subscribe). Note that its second parameter is a callback block, invoked whenever events the client subscribes to (via es_subscribe) occur anywhere on the system.
Since xprotectd subscribes to ES_EVENT_TYPE_RESERVED_0 and ES_EVENT_TYPE_RESERVED_1, if we can find the relevant preceding call to es_new_client, we can locate where the callback (event handler) is implemented. From there, we can set a breakpoint and examine the event-specific data to determine what these new ES events represent.
As xprotectd calls es_new_client multiple times, we need to let each call complete (via the finish lldb command), then dump the newly created ES client and match it with the one passed to the es_subscribe call that includes ES_EVENT_TYPE_RESERVED_0 and ES_EVENT_TYPE_RESERVED_1. This gives us the correct es_new_client / es_subscribe pairing (since there are multiple instances).
Once we’ve identified the relevant es_new_client call, we can dump its second argument (found in the x1 register)—its callback block:
Process 1403 stopped
* thread #3, name = 'Task 1', queue = 'com.apple.root.default-qos.cooperative', stop reason = breakpoint 2.1
frame #0: 0x00000001a479b7fc libEndpointSecurity.dylib`es_new_client
libEndpointSecurity.dylib`es_new_client:
-> 0x1a479b7fc <+0>: pacibsp
Target 0: (xprotectd) stopped.
(lldb) reg read $x1
x1 = 0x0000000aa9068090
The callback passed to es_new_client is a block literal, represented in memory as a heap-allocated block object (here, 0x0000000aa9068090). On arm64, the block’s invoke pointer is at offset +0x10, which points to the block’s entry stub. In this instance, the first captured field is at +0x20, and it contains a PAC-signed pointer to the real callback handler. Stripping the PAC bits with a mask yields the underlying address, 0x10001ec94, which is the es_callback function.
(lldb) x/gx 0x0000000aa9068090+0x20 0xaa90680b0: 0x1d5d00010001ec94 (lldb) p/x 0x1d5d00010001ec94 & 0x000000ffffffffff (long) 0x000000010001ec94
Hooray, so now we have the address of the function, 0x000000010001ec94, that will be invoked by the ES subsystem whenever ES_EVENT_TYPE_RESERVED_0 or ES_EVENT_TYPE_RESERVED_1 occurs anywhere on the system!
Now, before we continue, we need to look at the declaration of the callback so we can understand what it’s invoked with:
1typedef void (^es_handler_block_t)(es_client_t *_Nonnull, const es_message_t *_Nonnull);Most importantly, its second argument is an ES message. This is what we want to inspect, both to confirm it is ES_EVENT_TYPE_RESERVED_0 or ES_EVENT_TYPE_RESERVED_1, and—more importantly—to determine what these events represent and what event-specific data each message contains.
First, let’s summarize what we’ve figured out so far:
ES_EVENT_TYPE_RESERVED_0 and ES_EVENT_TYPE_RESERVED_10x000000010001ec94…we’re assuming that one of these events is related to Apple’s new macOS 26.4 ClickFix protection. And this is relevant because, as Sal correctly noted, this protection is only activated if SIP is enabled:
"SIP must be enabled, xprotectd checks this at startup and disables itself if SIP is off." - Ferdous
But, ugh, we were forced to disable SIP in order to debug xprotectd. Not to worry—we can just find that code and, in a debugger, ensure that check is skipped so ClickFix protection remains enabled (allowing us to continue debugging it).
Here’s the function from xprotectd that performs this check:
1100020e7c int64_t sub_100020e7c()
2
3100020e7c {
4100020e7c int32_t result = _csr_check(2);
5100020e9c char x19;
6100020e9c
7100020e9c if (!result)
8100020ee0 x19 = 0;
9100020e9c else
10100020e9c {
11100020ea8 void* const var_30_1 = &data_10006f4a0;
12100020eb0 int64_t var_28_1 = sub_100020fb0();
13100020ec0 char const* const var_48 = "CopyPasteBlocking";
14100020ec0 int64_t var_40_1 = 0x11;
15100020ec4 char var_38_1 = 2;
16100020ed0 x19 = FeatureFlags.isFeatureEnabled(&var_48);
17100020ed8 result = sub_100002684(&var_48);
18100020e9c }
19100020e9c
20100020eec pasteProtectionEnabled = x19 & 1;
21100020efc return result;
22100020e7c }As we can see in the decompilation, it first checks SIP via a call to _csr_check. If SIP is off, the ClickFix protection feature is unconditionally disabled. If SIP is on, it consults Apple’s internal FeatureFlags framework with the key CopyPasteBlocking to make the final decision. The result is cached in the global data_1000803a8 for use elsewhere in the process.
To “bypass” this, we set a breakpoint on the call to _csr_check, let it execute, then change its return value (found in $x0, more specifically $w0) from 0 (since we have SIP off) to 1 (pretending SIP is on):
(lldb)
Process 4154 stopped
* thread #3, name = 'Task 1', queue = 'com.apple.root.default-qos.cooperative', stop reason = instruction step over
frame #0: 0x0000000100020e9c xprotectd`___lldb_unnamed_symbol_100020e7c + 32
xprotectd`___lldb_unnamed_symbol_100020e7c:
-> 0x100020e9c <+32>: cbz w0, 0x100020ee0 ; <+100>
Target 0: (xprotectd) stopped.
(lldb) reg read $w0
w0 = 0x00000000
(lldb) reg write $w0 1
(lldb) reg read $w0
w0 = 0x00000001
Now, with ClickFix protection fully activated, let’s set a breakpoint on the ES callback function that xprotectd registered for ES_EVENT_TYPE_RESERVED_0 and ES_EVENT_TYPE_RESERVED_1. If we copy something from Safari and paste it into Terminal, that breakpoint is hit—woohoo!
(lldb)
* thread #4, queue = 'BBReaderQueue', stop reason = breakpoint 13.1
frame #0: 0x000000010001ec94 xprotectd`___lldb_unnamed_symbol_10001ec94
xprotectd`___lldb_unnamed_symbol_10001ec94:
-> 0x10001ec94 <+0>: pacibsp
Recall ES callback functions are invoked with an ES message for the subscribed event. In this case, we’ve just broken into the debugger after attempting to paste something into Terminal.
In this section, we’ll examine the message.
First, all delivered ES messages begin with a standard, well-documented header:
1typedef struct {
2 ...
3 es_process_t *_Nonnull process;
4 es_action_type_t action_type;
5 union {
6 es_event_id_t auth;
7 es_result_t notify;
8 } action;
9 es_event_type_t event_type;
10 es_events_t event;
11 ...
12} es_message_t;The most relevant members for us here include the responsible process (which generated the event), the action type (AUTH or NOTIFY), the event identifier (e.g. ES_EVENT_TYPE_RESERVED_1), and of course the event-specific data.
In a debugger, since we’ve stopped at the callback, we can dump these for the message we just received (triggered when we attempted to paste):
(lldb) x/20gx $x1 0x106d7a028: 0x000000000000000a 0x0000000069cb3b77 0x106d7a038: 0x00000000307ecc1a 0x0000002205b53e43 0x106d7a048: 0x0000002207b774e2 0x0000000106d7a120 0x106d7a058: 0x0000000000000000 0x538c041d00000000 0x106d7a068: 0xe18a7fa57b4a675d 0x0000000091c5f203 0x106d7a078: 0x0000000000000000 0x0000000000000000 0x106d7a088: 0x0000000000000095 0x0000000106d7a2c0 0x106d7a098: 0x0000000000000000 0x0000000000000000 0x106d7a0a8: 0x0000000000000000 0x0000000000000000 0x106d7a0b8: 0x0000000000000000 0x00x00000000000000
Giving Claude the es_message_t definition along with this data, it can construct the following:
1es_message_t @ 0x106d7a028:
2
3+0x00: 0x000000000000000a → version = 10 (uint32_t, padded)
4
5+0x08: 0x0000000069cb3b77 → time.tv_sec (struct timespec, 16 bytes)
6+0x10: 0x00000000307ecc1a → time.tv_nsec
7
8+0x18: 0x0000002205b53e43 → mach_time
9
10+0x20: 0x0000002207b774e2 → deadline
11
12+0x28: 0x0000000106d7a120 → process
13
14+0x30: 0x0000000000000000 → seq_num
15
16+0x38: 0x538c041d00000000 → action_type = 0 (ES_ACTION_TYPE_AUTH) + action.auth (es_event_id_t)
17+0x40: 0xe18a7fa57b4a675d → action cont
18+0x48: 0x0000000091c5f203 → action cont
19
20+0x50: 0x0000000000000000 → (padding/alignment)
21+0x58: 0x0000000000000000 → (padding/alignment)
22
23+0x60: 0x0000000000000095 → event_type = 0x95 = 149
24
25+0x68: 0x0000000106d7a2c0 → event (es_events_t)
26
27+0x70: 0x0000000000000000 → thread * (null)
28+0x78: 0x0000000000000000 → global_seq_num
29
30+0x80: 0x0000000000000000 → opaque[]First, we can see event_type is 0x95 (149d), which maps to ES_EVENT_TYPE_RESERVED_1, and since action_type is set to 0, we know it is an AUTH type ES event.
Let’s now examine 0x0000000106d7a120, which is a pointer to an es_process_t that describes the responsible process.
1typedef struct {
2 audit_token_t audit_token;
3 pid_t ppid;
4 pid_t original_ppid;
5 pid_t group_id;
6 pid_t session_id;
7 uint32_t codesigning_flags;
8 bool is_platform_binary;
9 bool is_es_client;
10 es_cdhash_t cdhash;
11 es_string_token_t signing_id;
12 es_string_token_t team_id;
13 es_file_t *_Nonnull executable;
14 es_file_t *_Nullable tty; /* field available only if message version >= 2 */
15 struct timeval start_time; /* field available only if message version >= 3 */
16 audit_token_t responsible_audit_token; /* field available only if message version >= 4 */
17 audit_token_t parent_audit_token; /* field available only if message version >= 4 */
18 es_cs_validation_category_t cs_validation_category; /* field available only if message version >= 10 */
19} es_process_t;We’ll skip most of the offset details, but note that at offset +0x58 is a pointer to the signing ID of the responsible process:
(lldb) x/x 0x0000000106d7a120+0x58 0x106d7a178: 0x0000000106d7a2a0 (lldb) x/s 0x0000000106d7a2a0 0x106d7a2a0: "com.apple.pboard"
Ah, the responsible process for generating this event is "com.apple.pboard", the pasteboard daemon—makes sense!
Recall that ES messages contain event-specific data. Here, we’re examining an ES_EVENT_TYPE_RESERVED_1, which we’ve confirmed is an AUTH event, and whose responsible process is the pasteboard daemon. Moreover, this event message was delivered when we attempted to paste.
From the message, we find a pointer to the event at 0x0000000106d7a2c0. Let’s dump it:
(lldb) x/40gx 0x0000000106d7a2c0 0x106d7a2c0: 0x000001f5000001f5 0x000001f500000014 0x106d7a2d0: 0x000005e300000014 0x00001050000186a2 0x106d7a2e0: 0x0000000106d7a3a8 0x000001f5000001f5 0x106d7a2f0: 0x000001f500000014 0x000002ac00000014 0x106d7a300: 0x00000711000186a2 0x0000000106d7a580 0x106d7a310: 0x0000000106d7a7c8 0x0000000000000010 0x106d7a320: 0x0000000106d7a713 0x0000000000000000 0x106d7a330: 0x0000000000000000 0x000000000000005a 0x106d7a340: 0x0000000106d7a724 0x2f6d65747379532f 0x106d7a350: 0x2f73656d756c6f56 0x2f746f6f62657250 0x106d7a360: 0x6578657470797243 0x79532f7070412f73 0x106d7a370: 0x7070412f6d657473 0x6e6f69746163696c 0x106d7a380: 0x6972616661532f73 0x6e6f432f7070612e 0x106d7a390: 0x614d2f73746e6574 0x616661532f534f63 0x106d7a3a0: 0x0000000000006972 0x000001f5000001f5 0x106d7a3b0: 0x000001f500000014 0x000005e300000014 0x106d7a3c0: 0x00001050000186a2 0x0000000100000001 0x106d7a3d0: 0x00000001000005e3 0x8bcf000126016a01 0x106d7a3e0: 0x18064544dc8f68a8 0x310e4e99823745a6 0x106d7a3f0: 0x0000000000001296 0x0000000000000010
It’s likely an es_event_paste_t structure, but unfortunately Apple has yet to document its structure or members.
We can immediately see it contains various audit tokens (0x000001f5000001f5... appears to be the start of an audit_token_t struct), along with other pointers (e.g. 0x0000000106d7a3a8) and ASCII values. I’ll spare you the (arguably boring) details of manually reconstructing this memory, and instead show a structure definition that describes it:
1// es_event_paste_t
2// based on reverse engineering...
3typedef struct {
4 audit_token_t source_token; // +0x00 — source app
5 es_process_t *source_process; // +0x20 — source es_process_t *
6 audit_token_t target_token; // +0x28 — target app
7 es_process_t *target_process; // +0x48 — target es_process_t *
8 es_process_t *target_responsible; // +0x50 — target responsible process
9 es_string_token_t source_bundle_id; // +0x58 — source bundle identifier
10 uint64_t unknown_1; // +0x68
11 uint64_t unknown_2; // +0x70
12 es_string_token_t paste_contents; // +0x78 — actual pasteboard data
13} es_eInstead of manually dumping these values from raw memory in a debugger (which is a pain), let’s pivot briefly and write our own ES client to subscribe to ES_EVENT_TYPE_RESERVED_1. With a proposed definition for the es_event_paste_t structure, if we can receive these messages ourselves, we can easily print out the values in code to confirm.
But wait—recall that when we tried to subscribe to these events at the start of the blog post, the ES subsystem told us to GTFO:
# sudo ./esClient subscribing to RESERVED_0 (type=148): failed (result=1) subscribing to RESERVED_1 (type=149): failed (result=1)
The reason Apple doesn’t allow us to subscribe to these events—specifically ES_EVENT_TYPE_RESERVED_1 (the paste event)—is that it’s private, and thus only available to clients that possess the com.apple.private.endpoint-security.client entitlement.
No surprise, xprotectd does:
codesign -d --entitlements - /usr/libexec/xprotectd
Executable=/usr/libexec/xprotectd
[Dict]
[Key] com.apple.developer.endpoint-security.client
[Value]
[Bool] true
[Key] com.apple.private.endpoint-security.client
[Value]
[Bool] true
[Key] com.apple.private.endpointsecurity.publisher
[Value]
[Bool] true
[Key] com.apple.private.security.signal-exempt
[Value]
[Bool] true
[Key] com.apple.private.security.syspolicy.report-metrics
[Value]
[Bool] true
[Key] com.apple.private.syspolicy.perform-evaluations
[Value]
[Bool] true
[Key] com.apple.private.tcc.allow
[Value]
[Array]
[String] kTCCServiceSystemPolicyAllFiles
[String] kTCCServiceEndpointSecurityClient
[Key] com.apple.private.xpc.launchd.per-user-lookup
[Value]
[Bool] true
But in our VM, since we’ve turned off AMFI (boot-args="amfi_get_out_of_my_way=1"), we can simply grant our ES client the com.apple.private.endpoint-security.client entitlement 😉
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.endpoint-security.client</key>
<true/>
<key>com.apple.private.endpoint-security.client</key>
<true/>
</dict>
</plist>
% codesign -s - --entitlements entitlements.plist --force esClient
If we run this in a VM, hooray, we’re allowed to subscribe:
# sudo ./esClient subscribing to RESERVED_1 (type=149): success (result=0)
Now, in our own es_new_client, let’s add the following code that makes use of our newly constructed es_event_paste_t structure:
1//msg type ES_EVENT_TYPE_RESERVED_1/149
2if(message->event_type == ES_EVENT_TYPE_RESERVED_1)
3{
4 es_event_paste_t *paste = *(es_event_paste_t **)&message->event;
5
6 printf(" paste source pid: %d\n", audit_token_to_pid(paste->source_token));
7 printf(" paste source bundle: %.*s\n", (int)paste->source_bundle_id.length, paste->source_bundle_id.data);
8 printf(" paste target pid: %d\n", audit_token_to_pid(paste->target_token));
9 if(paste->target_process)
10 printf(" paste target path: %s\n", paste->target_process->executable->path.data);
11 printf(" paste contents (%zu bytes): %.*s\n",
12 paste->paste_contents.length,
13 (int)paste->paste_contents.length,
14 paste->paste_contents.data);
15
16 es_respond_auth_result(client, message, ES_AUTH_RESULT_ALLOW, false);
17 }Compile and run—and it looks like we’ve got our own ES-based pasteboard monitor. Amazing!
# sudo ./esClient
subscribing to RESERVED_1 (type=149): success (result=0)
[*] event type: 149
action_type: AUTH
resp. process: /usr/libexec/pboard
es_event_paste_t:
paste source pid: 1507
paste source bundle: com.apple.Safari
paste target pid: 684
paste target path: /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
paste contents (90 bytes): echo 'ZWNobyAiQ2xpY2tGaXggdGVzdDogaGFybWxlc3MgcGF5bG9hZCBleGVjdXRlZCIK' | base64 -d | bash
First, this confirms we got the es_event_paste_t structure correct. You can see within it the source process (where the data was copied from, e.g. Safari), the target process (where it was pasted, e.g. Terminal), and the actual paste contents (here, a simulated ClickFix attack).
Worth noting, this is wholly separate from xprotectd—our ES-based pasteboard monitor receives any and all paste events! (which, yes, lays the foundation for building an ES-based ClickFix monitor).
But alas, because Apple has gated this powerful ES event behind the com.apple.private.endpoint-security.client entitlement, 3rd-party security tools such as BlockBlock cannot leverage it. Thanks, Apple 😤
Today we:
xprotectdES_EVENT_TYPE_RESERVED_1 (149) as a new Endpoint Security paste AUTH eventes_event_paste_t structureUnfortunately, Apple has chosen to gate this behind private entitlements, meaning 3rd-party security tools are locked out of functionality that Apple itself clearly relies on 🤨