Endpoint Security

Apple's Endpoint Security is a significant enhancement in MacOS 15, aimed at further enabling third party security software functionality, while at the same time keeping it out of the kernel. It's somewhat documented (in usr/include/EndpointSecurity/*.h headers), but, as usual, "it just works"™ - which might be good for them, but not for me. Since it demonstrates textbook principles from MOXiI 2 Volumes II and III, I figured it would make for a good extended hands-on example for readers - and anyone else interested in reverse engineering.

The Kext

The heart of the EndpointSecurity architecture is the EndpointSecurity.kext, which provides the absolutely necessary kernel component of the framework. Looking at its property list, we see:

morpheus@Zephyr (~) % jlutil /System/Library/Extensions/EndpointSecurity.kext/Contents/Info.plist
        BuildMachineOSBuild: 18A391011
        CFBundleDevelopmentRegion: en
        CFBundleExecutable: EndpointSecurity
        CFBundleIdentifier: com.apple.iokit.EndpointSecurity
        CFBundleInfoDictionaryVersion: 6.0
        CFBundleName: EndpointSecurity
        CFBundlePackageType: KEXT
        CFBundleShortVersionString: 1.0
        CFBundleSupportedPlatforms[0]: MacOSX
        CFBundleVersion: 1
        DTCompiler: com.apple.compilers.llvm.clang.1_0
        DTPlatformBuild: 11L374m
        DTPlatformName: macosx
        DTPlatformVersion: 10.15
        DTSDKBuild: 19A577a
        DTSDKName: macosx10.15internal
        DTXcode: 1100
        DTXcodeBuild: 11L374m
        IOKitPersonalities:
        EndpointSecurityDriver:
                CFBundleIdentifier: com.apple.iokit.EndpointSecurity
                IOClass: EndpointSecurityDriver
                IOMatchCategory: EndpointSecurityDriver
                IOProviderClass: IOResources
                IOResourceMatch: IOKit
                IOUserClientClass: EndpointSecurityDriverClient
        LSMinimumSystemVersion: 10.15
        NSHumanReadableCopyright: Copyright © 2018 Apple Inc. All rights reserved.
        OSBundleLibraries:
                com.apple.driver.AppleMobileFileIntegrity: 1.0.5
                com.apple.kpi.bsd: 18.0
                com.apple.kpi.dsep: 18.0
                com.apple.kpi.iokit: 18.0
                com.apple.kpi.libkern: 18.0
                com.apple.kpi.mach: 18.0
                com.apple.kpi.private: 18.0
                com.apple.kpi.unsupported: 18.0
        OSBundleRequired: Root

The dependency on AMFI can easily be seen (via jtool2 -S) to be for a single function,

AppleMobileFileIntegrity::AMFIEntitlementGetBool(proc*, char const*, bool*)

which taps AMFI's role of purveyor of fine entitlement (MOXiI2 III/7). Using (*sigh*) otool(1), we find the following entitlements are checked by an inlined proc_has_entitlement(proc*, char const*):
EntitlementNotes
com.apple.developer.endpoint-security.clientRequired for use by third party clients (documented)
com.apple.private.endpoint-security.dataless-manipulationEnforced by EndpointSecurityEventManager::sendSyscallFileProviderUpdate and EndpointSecurityEventManager::sendSyscallFileProviderMaterialization
com.apple.private.endpoint-security.clientEnforced by EndpointSecurityClient::create(ScopedPointer, proc*, ScopedPointer). Held by endpointsecurityd

The dependency on MACF (com.apple.kpi.dsep is far more interesting. The Mandatory Access Control Framework (MACF, a.k.a com.apple.kpi.dsep, discussed in MOXiI2 vIII/4) is a sort-of-private KPI, which third party developers such as Patrick Wardle and Jonathan Zdziarski were quick to use, but with Apple's new kext-signing requirement any users of this KPI could potentially be refused a certificate.

Looking through the disassembly, it's easy to find the call to mac_policy_register() (from EndpointSecurityEventManager::activateHooks()), which sets the MACF hooks that the kext will claim. Unsurprisingly, there is no corresponding call to mac_policy_unregister(), since the kext is not unloadable (though there is a deactivateHooks(). You can also see how the kext calls on the lesser KAuth facility (III/2) to obtain process notifications:

EndpointSecurityEventManager::activateHooks():
0000000000007b08        pushq   %rbp
0000000000007b09        movq    %rsp, %rbp
0000000000007b0c        pushq   %r15
0000000000007b0e        pushq   %r14
0000000000007b10        pushq   %rbx
0000000000007b11        pushq   %rax
0000000000007b12        movq    %rdi, %rbx
0000000000007b15        leaq    _gLogLevel_(%rip), %r15
0000000000007b1c        cmpl    $0x4, (%r15)
0000000000007b20        jb      0x7b4a
0000000000007b22        leaq    -0x7b29(%rip), %rdi
0000000000007b29        movq    0x21520(%rip), %rsi
0000000000007b30        leaq    EndpointSecurityEventManager::activateHooks()::_os_log_fmt(%rip), %rcx ## EndpointSecurityEventManager::activateHooks()::_os_log_fmt
0000000000007b37        leaq    0x1a67a(%rip), %r8 ## literal pool for: "bool EndpointSecurityEventManager::activateHooks()"
0000000000007b3e        movl    $0x2, %edx
0000000000007b43        xorl    %eax, %eax
0000000000007b45        callq   __os_log_internal
0000000000007b4a        movq    0x10(%rbx), %rdi
0000000000007b4e        callq   _IOLockLock
0000000000007b53        movb    $0x1, %r14b
0000000000007b56        cmpb    $0x0, 0x44(%rbx)
0000000000007b5a        jne     0x7c1b
0000000000007b60        leaq    0x1a684(%rip), %rdi ## literal pool for: "com.apple.kauth.fileop"
0000000000007b67        leaq    EndpointSecurityEventManager::es_fileop_scope_cb(ucred*, void*, int, unsigned long, unsigned long, unsigned long, unsigned long)(%rip), %r
si ## EndpointSecurityEventManager::es_fileop_scope_cb(ucred*, void*, int, unsigned long, unsigned long, unsigned long, unsigned long)
0000000000007b6e        movq    %rbx, %rdx
0000000000007b71        callq   _kauth_listen_scope
0000000000007b76        movq    %rax, 0x38(%rbx)
0000000000007b7a        testq   %rax, %rax
0000000000007b7d        je      0x7bce
0000000000007b7f        cmpb    $0x0, 0x2560a(%rip)
0000000000007b86        jne     0x7c17
0000000000007b8c        leaq    0x40(%rbx), %rsi
0000000000007b90        leaq    mac_policy(%rip), %rdi ## mac_policy
0000000000007b97        xorl    %edx, %edx
0000000000007b99        callq   _mac_policy_register
...

The MACF policy can be seen in memory (here shown with Xn00p):

morpheus@Zephyr (~/Documents/OSXBook/2nd/src/xnoop) % sudo xnoop dump _mac_policy_list
Mac Policy List:
Loaded: 6/5.  Max: 512 Static Max: 6. Chunks: 1. Free Hint: 6
Entries @0xffffff8027cd6000:
Entries[0]: Mac Policy @0xffffff7f97a60b08 (static)
Loadtime Flags: NONE	Runtime Flags: REGISTERED 
 	Name @0xffffff7f97a5ae98: AMFI (Apple Mobile File Integrity)
	1 Label names @0xffffff7f97a614b8: 0. amfi 
..
Entries[1]: Mac Policy @0xffffff7f981b42b0 (static)
Loadtime Flags: NONE	Runtime Flags: REGISTERED 
 	Name @0xffffff7f981a35e8: Sandbox (Seatbelt sandbox policy)
	1 Label names @0xffffff7f981b4300: 0. sb 
...
Entries[2]: Mac Policy @0xffffff7f9854a280 (static)
Loadtime Flags: NONE	Runtime Flags: REGISTERED 
 	Name @0xffffff7f9854939c: Quarantine (Quarantine policy)
..
Entries[3]: Mac Policy @0xffffff7f9881b010 (static)
Loadtime Flags: UNLOADOK	Runtime Flags: REGISTERED 
 	Name @0xffffff7f9881afd4: TMSafetyNet (Safety net for Time Machine)
	1 Label names @0xffffff7f9881b060: 0. tm 
..
Entries[4]: Mac Policy @0xffffff8028840b10 (static)
Loadtime Flags: NONE	Runtime Flags: REGISTERED 
 	Name @0xffffff7f9855847d: ASP (Apple System Policy)
	1 Label names @0xffffff7f9855b8e0: 0. aspk 
..
Entries[5]: Mac Policy @0xffffff7f97a9a4b8 (static)
Loadtime Flags: NONE	Runtime Flags: REGISTERED 
 	Name @0xffffff7f97a93294: EndpointSecurity (Endpoint Security Kernel Extension)
	1 Label names @0xffffff7f97a9a5a8: 0. EndpointSecurity 
	EndpointSecurity Hook   6:       mpo_cred_check_label_update_execve - 0xffffff7f97a7a532
	EndpointSecurity Hook   9:            mpo_cred_label_associate_fork - 0xffffff7f97a7e31e
	EndpointSecurity Hook  18:             mpo_cred_label_update_execve - 0xffffff7f97a7a53e
	EndpointSecurity Hook  36:                      mpo_file_check_mmap - 0xffffff7f97a7d1fe
	EndpointSecurity Hook  91:                   mpo_mount_check_umount - 0xffffff7f97a7dc3e
	EndpointSecurity Hook 117:                       mpo_policy_syscall - 0xffffff7f97a839f2
	EndpointSecurity Hook 120:                   mpo_vnode_check_rename - 0xffffff7f97a7e8c8
	EndpointSecurity Hook 122:                mpo_iokit_check_nvram_get - 0xffffff7f97a7b992
	EndpointSecurity Hook 135:                            mpo_reserved2 - 0xffffff7f97a7d7b6
	EndpointSecurity Hook 160:                  mpo_proc_check_get_task - 0xffffff7f97a81bf4
	EndpointSecurity Hook 164:                  mpo_proc_check_mprotect - 0xffffff7f97a7df3e
	EndpointSecurity Hook 169:                    mpo_proc_check_signal - 0xffffff7f97a7eed4
	EndpointSecurity Hook 243:                     mpo_proc_notify_exit - 0xffffff7f97a80446
	EndpointSecurity Hook 255:                   mpo_vnode_check_create - 0xffffff7f97a8019c
	EndpointSecurity Hook 257:             mpo_vnode_check_exchangedata - 0xffffff7f97a8088a
	EndpointSecurity Hook 264:                     mpo_vnode_check_link - 0xffffff7f97a7cada
	EndpointSecurity Hook 267:                     mpo_vnode_check_open - 0xffffff7f97a7a5a4
	EndpointSecurity Hook 270:                 mpo_vnode_check_readlink - 0xffffff7f97a824e0
	EndpointSecurity Hook 275:              mpo_vnode_check_setattrlist - 0xffffff7f97a81fa4
	EndpointSecurity Hook 276:               mpo_vnode_check_setextattr - 0xffffff7f97a7f35e
	EndpointSecurity Hook 277:                 mpo_vnode_check_setflags - 0xffffff7f97a80c54
	EndpointSecurity Hook 278:                  mpo_vnode_check_setmode - 0xffffff7f97a7f6fa
	EndpointSecurity Hook 279:                 mpo_vnode_check_setowner - 0xffffff7f97a81022
	EndpointSecurity Hook 282:                 mpo_vnode_check_truncate - 0xffffff7f97a82884
	EndpointSecurity Hook 283:                   mpo_vnode_check_unlink - 0xffffff7f97a7c4a0
	EndpointSecurity Hook 284:                    mpo_vnode_check_write - 0xffffff7f97a8137c
	EndpointSecurity Hook 303:                  mpo_vnode_notify_create - 0xffffff7f97a7fd08
	EndpointSecurity Hook 317:                     mpo_iokit_check_open - 0xffffff7f97a8186c
	EndpointSecurity Hook 323:                    mpo_proc_check_cpumon - 0xffffff7f97a83e90
	EndpointSecurity Hook 324:                    mpo_vnode_notify_open - 0xffffff7f97a7a608
	EndpointSecurity Hook 329:                      mpo_kext_check_load - 0xffffff7f97a7c058
	EndpointSecurity Hook 330:                    mpo_kext_check_unload - 0xffffff7f97a7c094

The kext is (thankfully) symbolicated, so it's also easy to deduce all these hooks by (again) dumping its __DATA.__const:

jtool2 -d __ZL10mac_policy,80  /System/Library/Extensions/EndpointSecurity.kext/Contents/MacOS/EndpointSecurity
Dumping 80 bytes from 0x2a4b8 (Offset 0x2a4b8, __DATA.__const):
__ZL10mac_policy:
0x2a4b8:          0x23294               "EndpointSecurity"
0x2a4c0:          0x232a5               "Endpoint Security Kernel Extension"
0x2a4c8:          0x2a5a8               __ZL10labelnames
0x2a4d0: 01 00 00 00 00 00 00 00    ........
0x2a4d8:          0x2a5b0               __ZL7mac_ops
0x2a4e0: 00 00 00 00 00 00 00 00    ........
0x2a4e8: 00 00 00 00 00 00 00 00    ........
0x2a4f0: 00 00 00 00 00 00 00 00    ........
0x2a4f8: 00 00 00 00 00 00 00 00    ........
0x2a500: 00 00 00 00 00 00 00 00    ........

#
# Now dump the operations
#
jtool2 -d __ZL7mac_ops,2650  /System/Library/Extensions/EndpointSecurity.kext/Contents/MacOS/EndpointSecurity | grep -v "00 00 00 00 00 00 00 00" | c++filt
Dumping 2650 bytes from 0x2a5b0 (Offset 0x2a5b0, __DATA.__const):
mac_ops:
0x2a5e0:           0xa532               EndpointSecurityEventManager::es_cred_check_label_update_execve(ucred*, vnode*, long long, vnode*, label*, label*, label*, proc*, void*, unsigned long)
0x2a5f8:           0xe31e               EndpointSecurityEventManager::es_cred_label_associate_fork(ucred*, proc*)
0x2a640:           0xa53e               EndpointSecurityEventManager::es_cred_label_update_execve(ucred*, ucred*, proc*, vnode*, long long, vnode*, label*, label*, label*, unsigned int*, void*, unsigned long, int*)
0x2a6d0:           0xd1fe               EndpointSecurityEventManager::es_file_check_mmap(ucred*, fileglob*, label*, int, int, unsigned long long, int*)
0x2a888:           0xdc3e               EndpointSecurityEventManager::es_mount_check_umount(ucred*, mount*, label*)
0x2a958:          0x139f2               EndpointSecurityEventManager::es_policy_syscall(proc*, int, unsigned long long)
0x2a970:           0xe8c8               EndpointSecurityEventManager::es_vnode_check_rename(ucred*, vnode*, label*, vnode*, label*, componentname*, vnode*, label*, vnode*, label*, componentname*)
0x2a980:           0xb992               EndpointSecurityEventManager::es_proc_notify_exec_complete(proc*)
0x2a9e8:           0xd7b6               EndpointSecurityEventManager::es_mount_check_mount_late(ucred*, mount*)
0x2aab0:          0x11bf4               EndpointSecurityEventManager::es_proc_check_get_task(ucred*, proc*)
0x2aad0:           0xdf3e               EndpointSecurityEventManager::es_proc_check_mprotect(ucred*, proc*, unsigned long long, unsigned long long, int)
0x2aaf8:           0xeed4               EndpointSecurityEventManager::es_proc_check_signal(ucred*, proc*, int)
0x2ad48:          0x10446               EndpointSecurityEventManager::es_proc_notify_exit(proc*)
0x2ada8:          0x1019c               EndpointSecurityEventManager::es_vnode_check_create(ucred*, vnode*, label*, componentname*, vnode_attr*)
0x2adb8:          0x1088a               EndpointSecurityEventManager::es_vnode_check_exchangedata(ucred*, vnode*, label*, vnode*, label*)
0x2adf0:           0xcada               EndpointSecurityEventManager::es_vnode_check_link(ucred*, vnode*, label*, vnode*, label*, componentname*)
0x2ae08:           0xa5a4               EndpointSecurityEventManager::es_vnode_check_open(ucred*, vnode*, label*, int)
0x2ae20:          0x124e0               EndpointSecurityEventManager::es_vnode_check_readlink(ucred*, vnode*, label*)
0x2ae48:          0x11fa4               EndpointSecurityEventManager::es_vnode_check_setattrlist(ucred*, vnode*, label*, attrlist*)
0x2ae50:           0xf35e               EndpointSecurityEventManager::es_vnode_check_setextattr(ucred*, vnode*, label*, char const*, uio*)
0x2ae58:          0x10c54               EndpointSecurityEventManager::es_vnode_check_setflags(ucred*, vnode*, label*, unsigned long)
0x2ae60:           0xf6fa               EndpointSecurityEventManager::es_vnode_check_setmode(ucred*, vnode*, label*, unsigned short)
0x2ae68:          0x11022               EndpointSecurityEventManager::es_vnode_check_setowner(ucred*, vnode*, label*, unsigned int, unsigned int)
0x2ae80:          0x12884               EndpointSecurityEventManager::es_vnode_check_truncate(ucred*, ucred*, vnode*, label*)
0x2ae88:           0xc4a0               EndpointSecurityEventManager::es_vnode_check_unlink(ucred*, vnode*, label*, vnode*, label*, componentname*)
0x2ae90:          0x1137c               EndpointSecurityEventManager::es_vnode_check_write(ucred*, ucred*, vnode*, label*)
0x2af28:           0xfd08               EndpointSecurityEventManager::es_vnode_notify_create(ucred*, mount*, label*, vnode*, label*, vnode*, label*, componentname*)
0x2af98:          0x1186c               EndpointSecurityEventManager::es_iokit_check_open(ucred*, OSObject*, unsigned int)
0x2afc8:          0x13e90               EndpointSecurityEventManager::es_vnode_check_lookup_preflight(ucred*, vnode*, label*, char const*, unsigned long)
0x2afd0:           0xa608               EndpointSecurityEventManager::es_vnode_notify_open(ucred*, vnode*, label*, int)
0x2aff8:           0xc058               EndpointSecurityEventManager::es_kext_check_load(ucred*, char const*)
0x2b000:           0xc094               EndpointSecurityEventManager::es_kext_check_unload(ucred*, char const*)

The IOUserClient

As evident from its Info.plist, The EndpointSecurity.kext also provides an IOUserClient:

jtool2 -d __DATA.__const   /System/Library/Extensions/EndpointSecurity.kext/Contents/MacOS/EndpointSecurity | c++filt | less -R
EndpointSecurityExternalClient::externalMethod(unsigned int, IOExternalMethodArguments*, IOExternalMethodDispatch*, OSObject*, void*)::es_client_ext_methods:
0x29b80:           0x26dc               EndpointSecurityExternalClient::operationResult(OSObject*, void*, IOExternalMethodArguments*)
0x29b88: 00 00 00 00 C 00 00 00    ........
0x29b90: 00 00 00 00 00 00 00 00    ........
0x29b98:           0x2c62               EndpointSecurityExternalClient::subscribe(OSObject*, void*, IOExternalMethodArguments*)
0x29ba0: 00 00 00 00 FF FF FF FF    ........
0x29ba8: 00 00 00 00 00 00 00 00    ........
0x29bb0:           0x2c72               EndpointSecurityExternalClient::unsubscribe(OSObject*, void*, IOExternalMethodArguments*)
0x29bb8: 00 00 00 00 FF FF FF FF    ........
0x29bc0: 00 00 00 00 00 00 00 00    ........
0x29bc8:           0x2c7e               EndpointSecurityExternalClient::unsubscribeAll(OSObject*, void*, IOExternalMethodArguments*)
0x29bd0: 00 00 00 00 00 00 00 00    ........
0x29bd8: 00 00 00 00 00 00 00 00    ........
0x29be0:           0x3358               EndpointSecurityExternalClient::muteProc(OSObject*, void*, IOExternalMethodArguments*)
0x29be8: 00 00 00 00 20 00 00 00    .... ...
0x29bf0: 00 00 00 00 00 00 00 00    ........
0x29bf8:           0x3368               EndpointSecurityExternalClient::unmuteProc(OSObject*, void*, IOExternalMethodArguments*)
0x29c00: 00 00 00 00 20 00 00 00    .... ...
0x29c08: 00 00 00 00 00 00 00 00    ........
0x29c10:           0x3374               EndpointSecurityExternalClient::mutedProcs(OSObject*, void*, IOExternalMethodArguments*)
0x29c18: 00 00 00 00 00 00 00 00    ........
0x29c20: 01 00 00 00 FF FF FF FF    ........
0x29c28:           0x36fc               EndpointSecurityExternalClient::setAutomata(OSObject*, void*, IOExternalMethodArguments*)
0x29c30: FF FF FF FF FF FF FF FF    ........
0x29c38: 00 00 00 00 00 00 00 00    ........
0x29c40:           0x39c8               EndpointSecurityExternalClient::subs(OSObject*, void*, IOExternalMethodArguments*)
0x29c48: 00 00 00 00 00 00 00 00    ........
0x29c50: 01 00 00 00 FF FF FF FF    ........

So, we have some 9 (0-8) methods exposed. Indeed, looking through libEndpointSecurity.framework we can see the user mode wrappers for these, as in the following examples. The method selector (= ordinal) is the second argument, i.e. %esi:

_es_mute_process:
000000000000188d        pushq   %rbp
000000000000188e        movq    %rsp, %rbp
0000000000001891        movq    %rsi, %rdx
0000000000001894        movl    (%rdi), %edi
0000000000001896        movl    $0x20, %ecx
000000000000189b        movl    $0x4, %esi
00000000000018a0        xorl    %r8d, %r8d
00000000000018a3        xorl    %r9d, %r9d
00000000000018a6        callq   0xa1ae ## symbol stub for: _IOConnectCallStructMethod
00000000000018ab        xorl    %ecx, %ecx
00000000000018ad        testl   %eax, %eax
00000000000018af        setne   %cl
00000000000018b2        movl    %ecx, %eax
00000000000018b4        popq    %rbp
00000000000018b5        retq
_es_unmute_process:
00000000000018b6        pushq   %rbp
00000000000018b7        movq    %rsp, %rbp
00000000000018ba        movq    %rsi, %rdx
00000000000018bd        movl    (%rdi), %edi
00000000000018bf        movl    $0x20, %ecx
00000000000018c4        movl    $0x5, %esi
00000000000018c9        xorl    %r8d, %r8d
00000000000018cc        xorl    %r9d, %r9d
00000000000018cf        callq   0xa1ae ## symbol stub for: _IOConnectCallStructMethod
00000000000018d4        xorl    %ecx, %ecx
00000000000018d6        testl   %eax, %eax
00000000000018d8        setne   %cl
00000000000018db        movl    %ecx, %eax
00000000000018dd        popq    %rbp

Thanks to the header documentation (/usr/include/EndpointSecurity/ESClient.h) we can see the definitions of the above:

/**
 * Suppress events relating to the process with `audit_token`
 * @param client The client for which events will be suppressed
 * @param audit_token The audit token of the process for which events will be suppressed
 * @return es_return_t indicating success or error
 */
OS_EXPORT
API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos)
es_return_t
es_mute_process(es_client_t * _Nonnull client, const audit_token_t * _Nonnull audit_token);

Which tells us that the mach_port_t that is used as the first argument of the IOConnectCall is at the beginning of the es_client_t, and that the "Struct" argument is the audit_token_t - this is also evident by the size of 0x20 (in the fourth argument, %ecx), which makes sense becaue an audit_token_t is eight uint32_t fields (q.v. MOXiI III/2).

libEndpointSecurity.dylib

Turning to analyze select functions of the user mode client dylib, let's start at where clients do - es_new_client:

/**
 * Initialise a new es_client_t and connect to the ES subsystem
 * @param client Out param. On success this will be set to point to the newly allocated es_client_t.
 * @param handler The handler block that will be run on all messages sent to this client
 * @return es_new_client_result_t indicating success or a specific error.
 * @discussion Messages are handled strictly serially and in the order they are delivered.
 *                         Returning control from the handler causes the next available message to be dequeued.
 *                 Messages can be responded to out of order by returning control before calling es_respond_*.
 *                         The es_message_t is only guaranteed to live as long as the scope it is passed into.
 *                         The memory for the given es_message_t is NOT owned by clients and it must not be freed.
 *                         For out of order responding the handler must copy the message with es_copy_message.
 *                         Callers are required to be entitled with com.apple.developer.endpoint-security.client.
 *                         The application calling this interface must also be approved by users via Transparency, Consent & Control
 *                         (TCC) mechanisms using the Privacy Preferences pane and adding the application to Full Disk Access.
 *                         When a new client is successfully created, all cached results are automatically cleared.
 * @see es_copy_message
 * @see es_new_client_result_t
 */
OS_EXPORT
API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos)
es_new_client_result_t
es_new_client(es_client_t * _Nullable * _Nonnull client, es_handler_block_t _Nonnull handler);

Diving to the disassembly:

What follows is MANUAL reversing with otool(1) - I'm not even using jtool2 since it's Intel (move to ARM already AAPL!). Yes, you can shove the kext and dylib through HexRays' decompilers and maybe get better results - but then, who's the l33t reverser? You, or them?
_es_new_client:
0000000000001ff8        pushq   %rbp
0000000000001ff9        movq    %rsp, %rbp
0000000000001ffc        pushq   %r15
0000000000001ffe        pushq   %r14
0000000000002000        pushq   %r13
0000000000002002        pushq   %r12
0000000000002004        pushq   %rbx
0000000000002005        subq    $0x28, %rsp
0000000000002009        movq    0xb008(%rip), %rax ## literal pool symbol address: ___stack_chk_guard
0000000000002010        movq    (%rax), %rax
0000000000002013        movq    %rax, -0x30(%rbp)

	// I figure out the semantics of these in the IOConnectMapMemory, later:
	 mach_vm_addres_t	mapAddr = NULL;  // -0x48(%rbp)
	 mach_vm_size_t		size = 0;        // -0x50(%rbp)

0000000000002017        xorl    %eax, %eax
0000000000002019        movq    %rax, -0x48(%rbp)
000000000000201d        movq    %rax, -0x50(%rbp)

	int rc = 2;

0000000000002021        movl    $0x2, %r12d

// Check if client is NULL, if it is, don't bother.
	if (!client) { return (rc); } // 0x238e is the exit

0000000000002027        testq   %rdi, %rdi
000000000000202a        je      0x238e
	
0000000000002030        movq    %rsi, %rbx       // %rbx = handler
0000000000002033        movq    %rdi, %r14       // %r14 = client
	client = malloc (sizeof(es_client_t));

0000000000002036        movl    $0x4058, %edi
000000000000203b        callq   0xa2b6 ## symbol stub for: _malloc
0000000000002040        movq    %rax, (%r14)

	if (!client) { return (rc); } // 0x238e is the exit..

0000000000002043        testq   %rax, %rax
0000000000002046        je      0x238e

We see that the es_client_t is pretty big - 16,472 bytes. The structure is opaque, but we can disassemble more to shine some light into it:

	// client at offset 0x4050 = 0
000000000000204c        xorl    %ecx, %ecx
000000000000204e        xchgb   %cl, 0x4050(%rax)
0000000000002054        movq    (%r14), %rax
	// client at offset 0x4051 = 0
0000000000002057        xorl    %ecx, %ecx
0000000000002059        xchgb   %cl, 0x4051(%rax)
000000000000205f        xorl    %eax, %eax
	// client at offset 0x4052 = 0
0000000000002061        movq    (%r14), %rcx
0000000000002064        xorl    %edx, %edx
0000000000002066        xchgb   %dl, 0x4052(%rcx)
	// client at offset 0x4053 = 0
000000000000206c        movq    (%r14), %rcx
000000000000206f        xchgb   %al, 0x4053(%rcx)

	// if (! handler)
0000000000002075        testq   %rbx, %rbx
0000000000002078        je      0x21d9
	{
		rc = 1;
		return (rc);
00000000000021d9        movl    $0x1, %r12d
00000000000021df        jmp     0x2386
	}

	// At this point we have handler - need to copy it as a block (MOXiI I/8)

000000000000207e        movq    %rbx, %rdi
0000000000002081        callq   0xa1f6 ## symbol stub for: __Block_copy

	// client at offset 0x4038 = _Block_copy (handler)

0000000000002086        movq    (%r14), %rcx
0000000000002089        movq    %rax, 0x4038(%rcx)

	// _eslog is a library global controlling os_log output (as __os_log_default)
	// This will make the dylib output very verbose progress messages through os_log.

	if (os_log_type_enabled (__eslog,1))
0000000000002090        movq    __eslog(%rip), %rbx
0000000000002097        movq    %rbx, %rdi
000000000000209a        movl    $0x1, %esi
000000000000209f        callq   0xa2da ## symbol stub for: _os_log_type_enabled
00000000000020a4        testb   %al, %al
00000000000020a6        je      0x20d3
	{

		os_log_impl(__mh_execute_header,
			    __eslog,
			    1, 
			    "esavd: IOServiceMatching...",
			    &var,  // -0x40(%rbp)
			    0x2);

00000000000020a8        leaq    -0x40(%rbp), %r8
00000000000020ac        movw    $0x0, (%r8)
00000000000020b2        leaq    -0x20b9(%rip), %rdi
00000000000020b9        leaq    0xa720(%rip), %rcx ## literal pool for: "esavd: IOServiceMatching..."
00000000000020c0        movq    %rbx, %rsi
00000000000020c3        movl    $0x1, %edx
00000000000020c8        movl    $0x2, %r9d
00000000000020ce        callq   0xa24a ## symbol stub for: __os_log_impl
	 }

	  CFMutableDictionaryRef driverMatchingDict = IOServiceMatching("EndpointSecurityDriver");

00000000000020d3        leaq    0x859c(%rip), %rdi ## literal pool for: "EndpointSecurityDriver"

	
00000000000020da        callq   0xa1ea ## symbol stub for: _IOServiceMatching
00000000000020df        movq    __eslog(%rip), %r15

	if (driverMatchingDict == NULL) 

00000000000020e6        testq   %rax, %rax
00000000000020e9        je      0x21e4
	{
		// Logs "Failed to get driver class." and bails
		return (rc);
	}

	
	//  os_log for "esavd: IOServiceGetMatchingService..." omitted here..
00000000000020ef        movq    %rax, %rbx
...
0000000000002129        callq   0xa24a ## symbol stub for: __os_log_impl

	io_service_t  driverService = IOServiceGetMatchingService(kIOMasterPortDefault, driverMatchingDict);

000000000000212e        movq    0xaefb(%rip), %rax ## literal pool symbol address: _kIOMasterPortDefault
0000000000002135        movl    (%rax), %edi
0000000000002137        movq    %rbx, %rsi
000000000000213a        callq   0xa1e4 ## symbol stub for: _IOServiceGetMatchingService

	if (!driverService) {
000000000000213f        movq    __eslog(%rip), %r15
0000000000002146        testl   %eax, %eax
0000000000002148        je      0x2219
		
	// logs for "Failed to get matching service." and bails
	}

	// os_log for "esavd: IOServiceOpen..."
000000000000214e        movl    %eax, %ebx
	..
0000000000002187        callq   0xa24a ## symbol stub for: __os_log_impl
	
	 mach_port_t myself = mach_task_self(); // r12
	
	kern_return_t kr = IOServiceOpen(driverService, // io_service_t service, 
			   myself, // task_port_t owningTask, 
			   1, // uint32_t type, 
			   &client); // io_connect_t *connect);

000000000000218c        movq    0xaea5(%rip), %r12 ## literal pool symbol address: _mach_task_self_
0000000000002193        movl    (%r12), %esi
0000000000002197        movq    (%r14), %rcx
000000000000219a        movl    %ebx, %edi
000000000000219c        movl    $0x1, %edx
00000000000021a1        callq   0xa1f0 ## symbol stub for: _IOServiceOpen

	if (kr > 0xe00002e5) {
00000000000021a6        movl    %eax, %r15d
00000000000021a9        cmpl    $0xe00002e5, %eax
00000000000021ae        jg      0x2265
		// for 0xe00002e6, log "Failed to open service: %d", rc = 3, fail
		..
		return (3);
	}

	if (kr == $0xe00002c1) {
00000000000021b4        cmpl    $0xe00002c1, %r15d
00000000000021bb        je      0x2330
	{
		// log "Failed to open service: %d" ,rc = 5
		return (5);

	}

	// Other errors, whatever..
00000000000021c1        cmpl    $0xe00002e2, %r15d
00000000000021ce        movl    $0x4, %r12d
00000000000021d4        jmp     0x233e
00000000000021d9        movl    $0x1, %r12d
00000000000021df        jmp     0x2386

	// ever the optimists, we continue here:
000000000000227b 	movq    __eslog(%rip), %rbx
	// log "esavd: IODataQueueAllocateNotificationPort..." omitted..
00000000000022bf        callq   0xa24a ## symbol stub for: __os_log_impl
	
	mach_port_t dqnp = IODataQueueAllocateNotificationPort();
00000000000022c4        xorl    %eax, %eax
00000000000022c6        callq   0xa1c6 ## symbol stub for: _IODataQueueAllocateNotificationPort

	// client at offset 0x4 = dqnp
	client->dqnp = dqnp;

00000000000022cb        movl    %eax, %ebx
00000000000022cd        movq    (%r14), %r13
00000000000022d0        movl    %eax, 0x4(%r13)

	// os_log for 
00000000000022d4        leal    0x1(%rbx), %eax
00000000000022d7        cmpl    %r15d, %eax
00000000000022da        ja      0x23b4
	// os_log "Failed to allocate notification port" , rc = 2, bail

	// other errors already covered jumped over..

00000000000023b4        movq    __eslog(%rip), %r15
	// os_log ""esavd: IOConnectSetNotificationPort..." omitted
..
00000000000023f2        callq   0xa24a ## symbol stub for: __os_log_impl

	kr = IOConnectSetNotificationPort(client->connectPort, // io_connect_t connect, 
					  0, // uint32_t type, 
					  client->dqnp, // mach_port_t port, 
					  NULL); // uintptr_t reference);
00000000000023f7        movq    (%r14), %r13
00000000000023fa        movl    0x4(%r13), %ebx
00000000000023fe        movl    (%r13), %edi
0000000000002402        xorl    %esi, %esi
0000000000002404        movl    %ebx, %edx
0000000000002406        xorl    %ecx, %ecx
0000000000002408        callq   0xa1ba ## symbol stub for: _IOConnectSetNotificationPort

	if (kr) {
000000000000240d        movq    __eslog(%rip), %r15
0000000000002414        testl   %eax, %eax
0000000000002416        je      0x2451
	
		// os_log "Failed to set data queue notification port: 0x%x"..

	}

0000000000002451        movq    %r15, %rdi
0000000000002454        movl    $0x1, %esi
0000000000002459        callq   0xa2da ## symbol stub for: _os_log_type_enabled
	// os_log "esavd: IOConnectMapMemory..."
	..
0000000000002488        callq   0xa24a ## symbol stub for: __os_log_impl

	// Remember the variables up there? That's how I know what their semantics were:

	 kr = IOConnectMapMemory(client->connect, // io_connect_t connect, 
				 0, // uint32_t memoryType, 
				 myself, // task_port_t intoTask, 
				 &mapAddr; // mach_vm_address_t *atAddress, 
				 &size, // mach_vm_size_t *ofSize, 
				 1); // IOOptionBits options);

000000000000248d        movq    (%r14), %rax
0000000000002490        movl    (%rax), %edi
0000000000002492        movl    (%r12), %edx
0000000000002496        leaq    -0x48(%rbp), %rcx
000000000000249a        leaq    -0x50(%rbp), %r8
000000000000249e        movl    $0x0, %esi
00000000000024a3        movl    $0x1, %r9d
00000000000024a9        callq   0xa1b4 ## symbol stub for: _IOConnectMapMemory

	if (kr != KERN_SUCCESS) 

00000000000024ae        testl   %eax, %eax
00000000000024b0        je      0x2520
	{

	// failure and os_log of "Failed to map memory: 0x%x" omitted

	}

	// client at offset 0x8 gets mapped memory:
	client->mapAddr = mapAddr;

0000000000002520        movq    -0x48(%rbp), %rax
0000000000002524        movq    (%r14), %rcx
0000000000002527        movq    %rax, 0x8(%rcx)

	dispatch_queue_t wq = dispatch_queue_create("com.apple.endpointsecurity.endpointSecurityFramework.writerQueue", // const char *label, 
	0, // dispatch_queue_attr_t attr);
000000000000252b        leaq    0x815b(%rip), %rdi ## literal pool for: "com.apple.endpointsecurity.endpointSecurityFramework.writerQueue"
0000000000002532        xorl    %esi, %esi
0000000000002534        callq   0xa274 ## symbol stub for: _dispatch_queue_create

	// client at offset 0x10 gets wq
0000000000002539        movq    (%r14), %rcx
000000000000253c        movq    %rax, 0x10(%rcx)

	client->wq = wq;

	dispatch_queue_t rq = dispatch_queue_create("com.apple.endpointsecurity.endpointSecurityFramework.readerQueue", // const char *label,
        0, // dispatch_queue_attr_t attr);
0000000000002540        leaq    0x8187(%rip), %rdi ## literal pool for: "com.apple.endpointsecurity.endpointSecurityFramework.readerQueue"
0000000000002547        xorl    %esi, %esi
0000000000002549        callq   0xa274 ## symbol stub for: _dispatch_queue_create

	// client at offset 0x18 gets rq
000000000000254e        movq    (%r14), %rcx
0000000000002551        movq    %rax, 0x18(%rcx)
	client->rq = rq;

	// Next we create a dispatch source from the notification port

	dispatch_source_t writerDispatchSrc = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, // dispatch_source_type_t type, 
			client->dqnp, //  uintptr_t handle, 
			0,            // unsigned long mask, 
			client->wq); // dispatch_queue_t queue);
	
0000000000002555        movq    (%r14), %rax
0000000000002558        movl    0x4(%rax), %esi
000000000000255b        movq    0x10(%rax), %rcx
000000000000255f        movq    0xaaba(%rip), %rdi ## literal pool symbol address: __dispatch_source_type_mach_recv
0000000000002566        xorl    %edx, %edx
0000000000002568        callq   0xa28c ## symbol stub for: _dispatch_source_create

	// client at offset 0x20 gets writerDispatchSrc

	client->writerDispatchSrc = writerDispatchSrc;
000000000000256d        movq    (%r14), %rcx
0000000000002570        movq    %rax, 0x20(%rcx)
	if (!client->notifSrc) 
0000000000002577        cmpq    $0x0, 0x20(%rax)
000000000000257c        je      0x25fc
	{
		// "os_log Failed to setup writer dispatch source"
	}

	if (!client->rq)
000000000000257e        cmpq    $0x0, 0x18(%rax)
0000000000002583        je      0x263b
	{

		// os_log "Failed to setup reader queue"
	}
	
	// client at offset 0x4028 = NULL;
0000000000002589        xorl    %r12d, %r12d
000000000000258c        xorl    %ecx, %ecx
000000000000258e        xchgq   %rcx, 0x4028(%rax)

	// client at offset 0x4030 = NULL;
0000000000002595        movq    (%r14), %rax
0000000000002598        xorl    %ecx, %ecx
000000000000259a        xchgq   %rcx, 0x4030(%rax)

	void *sbctx = sb_context_new();

00000000000025a1        callq   _sb_context_new

	// client at 0x4040 gets sb_context_new()

	client->sbctx = sbctx;
00000000000025a6        movq    (%r14), %rcx
00000000000025a9        movq    %rax, 0x4040(%rcx)
00000000000025b0        movq    (%r14), %rax
	if (!client->sbctx) { return 2; }
00000000000025b3        cmpq    $0x0, 0x4040(%rax)
00000000000025bb        je      0x2515
	
	// client at offset 0x4048 gets 0
00000000000025c1        movq    $0x0, 0x4048(%rax)

	dispatch_source_set_event_handler_f (client->writerDispatchSrc, __es_copy_new_events(%rip));
00000000000025cc        movq    (%r14), %rax
00000000000025cf        movq    0x20(%rax), %rdi
00000000000025d3        leaq    __es_copy_new_events(%rip), %rsi
00000000000025da        callq   0xa292 ## symbol stub for: _dispatch_source_set_event_handler_f


	dispatch_set_context (client->writerDispatchSrc, client);

00000000000025df        movq    (%r14), %rsi
00000000000025e2        movq    0x20(%rsi), %rdi
00000000000025e6        callq   0xa280 ## symbol stub for: _dispatch_set_context

	dispatch_activate (client->writerDispatchSrc);
00000000000025eb        movq    (%r14), %rax
00000000000025ee        movq    0x20(%rax), %rdi
00000000000025f2        callq   0xa262 ## symbol stub for: _dispatch_activate
00000000000025f7        jmp     0x238e

	return (client);
}

Following the above, we can now see what the es_client_t really looks like, at least partially:

typedef 
struct es_client {

/* 0x0000 */	io_connect_t		driverConnection;
/* 0x0004 */	mach_port_t 		dqnp;
/* 0x0008 */	mach_vm_address_t	mapAddr;
/* 0x0010 */	dispatch_queue_t	wq;
/* 0x0018 */	dispatch_queue_t	rq;
/* 0x0020 */	dispatch_source_t	writerDispatchSrc;
/* 0x0028 */	char			mysteriousPage[0x4000]; // Not a "page" in the machine sense (unaligned and 16k).
/* 0x4028 */	void			*unknown0x4028;
/* 0x4030 */	void			*unknown0x4030;
/* 0x4038 */	void			*handler; // Supplied to es_new_client
/* 0x4040 */	void 			*sb_context; // from sb_context_new()
/* 0x4048 */	void 			*unknown0x4048; // NULL
/* 0x4050 */	char			unknown0x4050; // 0
/* 0x4051 */	char			unknown0x4051; // 0
/* 0x4052 */	char			unknown0x4052; // 0
/* 0x4053 */	char			unknown0x4053; // 0
} *es_client_t;

Reading events

Leaving the unknowns unknowns for now, we move to __es_copy_new_events, which as we've seen is the event handler function for the writer dispatch source:

__es_copy_new_events(void *context) 
{
000000000000267f        pushq   %rbp
0000000000002680        movq    %rsp, %rbp
0000000000002683        pushq   %r15
0000000000002685        pushq   %r14
0000000000002687        pushq   %r13
0000000000002689        pushq   %r12
000000000000268b        pushq   %rbx
000000000000268c        subq    $0x38, %rsp
	rbx = context (= client);
0000000000002690        movq    %rdi, %rbx

	client->unknown0x4052 = 1;

0000000000002693        movb    $0x1, %al
0000000000002695        xchgb   %al, 0x4052(%rdi)
	
	mach_msg_header_t	m; // -0x58(%rbp)

	mach_msg_return_t  kr =       
		mach_msg(&m, // mach_msg_header_t *msg,
		 	MACH_RCV_MSG, // = 2 mach_msg_option_t option,
			0, // mach_msg_size_t send_size,
			0x20, // mach_msg_size_t rcv_size, %rdx
			client->dqnp; // mach_port_name_t rcv_name, %r8d
			 MACH_MSG_TIMEOUT_NONE) // mach_msg_timeout_t timeout, %r9d
			MACH_PORT_NULL); // mach_port_name_t notify); (%rsp)

000000000000269b        movl    0x4(%rdi), %r8d
000000000000269f        movl    $0x0, (%rsp)
00000000000026a6        leaq    -0x58(%rbp), %rdi
00000000000026aa        xorl    %r12d, %r12d
00000000000026ad        movl    $0x2, %esi
00000000000026b2        xorl    %edx, %edx
00000000000026b4        movl    $0x20, %ecx
00000000000026b9        xorl    %r9d, %r9d
00000000000026bc        callq   0xa2a4 ## symbol stub for: _mach_msg

	if (kr != MACH_MSG_SUCCESS)
00000000000026c1        testl   %eax, %eax
00000000000026c3        jne     0x27d4
	{
		client->unknown0x4052 = 0;
		return 0;
	}

	if (client->unknown0x4051 != 1) 
		{
00000000000026c9        movb    0x4051(%rbx), %al
00000000000026cf        testb   $0x1, %al
00000000000026d1        jne     0x27b4
			goto 0x27b4;
		}
	else {
			
			local_at_0x38 = client->mysteriousPage;
			%r12 = 0
			%r14 = $0xcccccccd; 

00000000000026d7        leaq    0x28(%rbx), %rax   // Event buffer (mysteriousPage)
00000000000026db        movq    %rax, -0x38(%rbp)
00000000000026df        xorl    %r12d, %r12d
00000000000026e2        movl    $0xcccccccd, %r14d
00000000000026e8        movq    0x8(%rbx), %rdi
`
		if (IODataQueueDataAvailable (client->mapAddr) == 0)
00000000000026ec        callq   0xa1cc ## symbol stub for: _IODataQueueDataAvailable
00000000000026f1        testb   %al, %al
00000000000026f3        je      0x27b4
		
			goto 0x27b4
		}

		if (client->unknown0x4028 - client->unknown0x4030 < 0x400)	
	
00000000000026f9        movq    0x4028(%rbx), %rax
0000000000002700        movq    0x4030(%rbx), %r13
0000000000002707        movq    %r13, %rcx
000000000000270a        subq    %rax, %rcx
000000000000270d        movq    0x8(%rbx), %rdi
0000000000002711        cmpq    $0x400, %rcx
0000000000002718        jb      0x2725
	
		{
		IOReturn ir = IODataQueueDequeue(client->mapAddr, // IODataQueueMemory *dataQueue, 
				NULL, // void *data, 
				NULL); uint32_t *dataSize)

000000000000271a        xorl    %esi, %esi
000000000000271c        xorl    %edx, %edx
000000000000271e        callq   0xa1d2 ## symbol stub for: _IODataQueueDequeue
0000000000002723        jmp     0x278b
	
		goto 0x278b;

		}

		IODataQueueEntry peek = IODataQueuePeek(client->mapAddr);  // -0x2c(%rbp)
		if (peek == 0) goto 0x278b;

0000000000002725        callq   0xa1d8 ## symbol stub for: _IODataQueuePeek
000000000000272a        testq   %rax, %rax
000000000000272d        je      0x278b

		// If we're here we definitely have events to copy:

		  void *r15 =  malloc(peek);

000000000000272f        movl    (%rax), %edi
0000000000002731        movl    %edi, -0x2c(%rbp)
0000000000002734        callq   0xa2b6 ## symbol stub for: _malloc
0000000000002739        movq    %rax, %r15

		 IOReturn ir = IODataQueueDequeue (client->mapAddr, // IODataQueueMemory *dataQueue,
                                r15, // void *data,
                                &peek); uint32_t *dataSize)

000000000000273c        movq    0x8(%rbx), %rdi
0000000000002740        movq    %rax, %rsi
0000000000002743        leaq    -0x2c(%rbp), %rdx
0000000000002747        callq   0xa1d2 ## symbol stub for: _IODataQueueDequeue

		  if (ir)
000000000000274c        testl   %eax, %eax
000000000000274e        je      0x275a
			{
0000000000002750        movq    %r15, %rdi
0000000000002753        callq   0xa29e ## symbol stub for: _free
0000000000002758        jmp     0x278b

				free(r15);
				
			}
		  else
		  {
			fixup_pointers_into_ptb(r15);
000000000000275a        movq    %r15, %rdi
000000000000275d        callq   _fixup_pointers_into_ptb
			
			
0000000000002762        movl    %r13d, %eax
0000000000002765        andl    $0x3ff, %eax
000000000000276a        shlq    $0x4, %rax
000000000000276e        movl    -0x2c(%rbp), %ecx
0000000000002771        movq    -0x38(%rbp), %rdx
0000000000002775        movq    %r15, (%rdx,%rax)
0000000000002779        movq    %rcx, 0x8(%rdx,%rax)
000000000000277e        incq    %r13
0000000000002781        xchgq   %r13, 0x4030(%rbx)
0000000000002788        incl    %r12d
		 }

000000000000278b        movl    %r12d, %eax
000000000000278e        imulq   %r14, %rax
0000000000002792        shrq    $0x22, %rax
0000000000002796        leal    (%rax,%rax,4), %eax
0000000000002799        cmpl    %eax, %r12d
000000000000279c        jne     0x27a6
		
		   __es_handle_buffered_events(client)
000000000000279e        movq    %rbx, %rdi
00000000000027a1        callq   __es_handle_buffered_events

		    if (client->unknown0x4051 == 1) goto 0x26e8
00000000000027a6        movb    0x4051(%rbx), %al
00000000000027ac        testb   $0x1, %al
00000000000027ae        je      0x26e8

			
00000000000027b4        movl    %r12d, %eax
00000000000027b7        movl    $0xcccccccd, %ecx
00000000000027bc        imulq   %rax, %rcx
00000000000027c0        shrq    $0x22, %rcx
00000000000027c4        leal    (%rcx,%rcx,4), %eax
00000000000027c7        cmpl    %eax, %r12d
00000000000027ca        je      0x27d4
		
		     __es_handle_buffered_events(client);
00000000000027cc        movq    %rbx, %rdi
00000000000027cf        callq   __es_handle_buffered_events

	
00000000000027d4        xorl    %eax, %eax
			
			client->unknown0x4052 = 0;
00000000000027d6        xchgb   %al, 0x4052(%rbx)

			return 0

00000000000027dc        addq    $0x38, %rsp
00000000000027e0        popq    %rbx
00000000000027e1        popq    %r12
00000000000027e3        popq    %r13
00000000000027e5        popq    %r14
00000000000027e7        popq    %r15
00000000000027e9        popq    %rbp
00000000000027ea        retq

In simple terms, the pattern is - kernel populates the SharedDataQueue, and fires an empty notification message. This causes mach_msg to return and the queue is polled. We also see now the meaning of two of the unknowns: 0x4051 and 0x4052 tell us if the queue is being read and/or there is data in it. The actual work of obtaining the data is via the IODataQueue abstraction (which I realize, in hindsight, I should've put into Volume II's IOKit discussion *sigh*). Using lldb on Patrick Wardle's endpoint security ProcessMonitor.app example (thanks man!), we can see the following:

root@Zephyr (~) #lldb ~/Downloads/ProcessMonitor.app/Contents/MacOS/ProcessMonitor
(lldb) target create "/Users/morpheus/Downloads/ProcessMonitor.app/Contents/MacOS/ProcessMonitor"
Current executable set to '/Users/morpheus/Downloads/ProcessMonitor.app/Contents/MacOS/ProcessMonitor' (x86_64).
(lldb) b IODataQueueDequeue
Breakpoint 1: where = IOKit`IODataQueueDequeue, address = 0x000000000001a79a

#
# Breakpoint hit - capture %rsi, which holds the output buffer we will dequeue data into:
#
Process 93554 stopped
* thread #3, queue = 'com.apple.endpointsecurity.endpointSecurityFramework.writerQueue', stop reason = breakpoint 1.1
    frame #0: 0x00007fff322a079a IOKit`IODataQueueDequeue
IOKit`IODataQueueDequeue:
->  0x7fff322a079a <+0>: pushq  %rbp
    0x7fff322a079b <+1>: movq   %rsp, %rbp
    0x7fff322a079e <+4>: movq   %rdx, %rcx
    0x7fff322a07a1 <+7>: movq   %rsi, %rdx
Target 0: (ProcessMonitor) stopped.
(lldb) reg read $rsi
     rsi = 0x0000000100206530
#
# Perform dequeue and stop right after:
#
(lldb) thread step-out
Process 93554 stopped
* thread #3, queue = 'com.apple.endpointsecurity.endpointSecurityFramework.writerQueue', stop reason = step out
    frame #0: 0x00007fff630a674c libEndpointSecurity.dylib`_es_copy_new_events + 205
libEndpointSecurity.dylib`_es_copy_new_events:
->  0x7fff630a674c <+205>: testl  %eax, %eax
    0x7fff630a674e <+207>: je     0x7fff630a675a            ; <+219>
    0x7fff630a6750 <+209>: movq   %r15, %rdi
    0x7fff630a6753 <+212>: callq  0x7fff630ae29e            ; symbol stub for: free
Target 0: (ProcessMonitor) stopped.
#
# Read value of output buffer (note %rsi will have been modified arbitrarily at this point,
# So we use value from above:
#
(lldb) mem read  0x0000000100206530
0x100206530: 01 00 00 00 00 00 00 00 11 f6 c2 5d 00 00 00 00  .........??]....
0x100206540: a4 17 94 1a 00 00 00 00 46 1b 44 b2 cf 58 00 00  ?.......F.D??X..
0x100206550: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206560: 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00  ................
0x100206570: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206590: 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002065f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206600: 70 01 00 00 70 01 00 00 05 00 00 00 05 00 00 00  p...p...........
0x100206610: 30 00 00 00 40 00 00 00 e8 00 00 00 e8 00 00 00  0...@...?...?...
0x100206620: f8 00 00 00 00 03 00 00 02 00 00 00 00 00 00 00  ?...............
0x100206630: 2f 75 73 72 2f 62 69 6e 2f 76 69 6d 00 ff ff ff  /usr/bin/vim.???  <-- PATH
0x100206640: 0c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206650: 00 00 00 00 00 00 00 00 05 00 00 01 ed 81 01 00  ............?...
0x100206660: 8d 7f 13 00 ff ff ff 0f 00 00 00 00 00 00 00 00  ....???.........
0x100206670: 00 00 00 00 00 00 00 00 bb 39 93 5d 00 00 00 00  ........?9.]....
0x100206680: 00 00 00 00 00 00 00 00 bb 39 93 5d 00 00 00 00  ........?9.]....
0x100206690: 00 00 00 00 00 00 00 00 5a 09 99 5d 00 00 00 00  ........Z..]....
0x1002066a0: 2f 6f be 0f 00 00 00 00 bb 39 93 5d 00 00 00 00  /o?.....?9.]....
0x1002066b0: 00 00 00 00 00 00 00 00 60 51 1f 00 00 00 00 00  ........`Q......
0x1002066c0: a8 09 00 00 00 00 00 00 00 10 00 00 20 00 08 00  ?........... ...
0x1002066d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x1002066e0: 00 00 00 00 00 00 00 00 63 6f 6d 2e 61 70 70 6c  ........com.appl
0x1002066f0: 65 2e 76 69 6d 00 ff ff f5 01 00 00 f5 01 00 00  e.vim.???...?...
0x100206700: 14 00 00 00 f5 01 00 00 14 00 00 00 68 6c 01 00  ....?.......hl..
0x100206710: a6 86 01 00 85 e8 02 00 71 01 00 00 71 01 00 00  ?....?..q...q...
0x100206720: 68 6c 01 00 6f 01 00 00 01 40 00 24 01 00 38 ef  hl..o....@.$..8?
0x100206730: 37 c9 8a bc 3a 32 9e 0d aa 3a 37 c4 5e 5d 4d f1  7?.?:2..?:7?^]M? <-- CODESIGN INFO
0x100206740: 07 60 00 00 00 00 00 00 00 00 00 00 00 00 00 00  .`..............
0x100206750: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x100206760: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
#
# will need to continue multiple times till all events are dequeued because library buffers:
#
(lldb) c 
{"event":"ES_EVENT_TYPE_NOTIFY_EXIT","process":{"pid":93288,"path":"/usr/bin/vim","uid":501,
"arguments":[],"ppid":369,"ancestors":[369,367,300,1],
"signing info":{"csFlags":603996161,"signatureIdentifier":"com.apple.vim",
 "cdHash":"38EF37C98ABC3A329EDAA3A37C45E5D4DF1760","isPlatformBinary":1},"exit code":0}}

This sums up my discussion, but if you investigate further, you'll see how __es_handle_buffered_events takes those events and cooks them into the more manageable format which Patrick and other clients use. Calling the user supplied handler is in ____es_handle_buffered_events_block_invoke:

____es_handle_buffered_events_block_invoke:
0000000000002a42        pushq   %rbp
0000000000002a43        movq    %rsp, %rbp
0000000000002a46        pushq   %r15
0000000000002a48        pushq   %r14
0000000000002a4a        pushq   %rbx
0000000000002a4b        subq    $0x18, %rsp
0000000000002a4f        movq    0x20(%rdi), %rax
0000000000002a53        xorl    %ecx, %ecx
0000000000002a55        xchgb   %cl, 0x4050(%rax)
0000000000002a5b        movq    0x20(%rdi), %rax
//
// Indicate events are being processed - that's unknown0x4053
//
0000000000002a5f        movb    $0x1, %cl
0000000000002a61        xchgb   %cl, 0x4053(%rax)
// 
// Check unknown0x4051, which as we've seen indicates
// we have event records (messages)
//
0000000000002a67        movq    0x20(%rdi), %rax
0000000000002a6b        movb    0x4051(%rax), %al
0000000000002a71        movq    0x20(%rdi), %rsi
0000000000002a75        testb   $0x1, %al
0000000000002a77        jne     0x2abb
0000000000002a79        movq    %rdi, %r15
0000000000002a7c        leaq    -0x28(%rbp), %r14
// Event loop:
0000000000002a80        movq    %r14, %rdi
0000000000002a83        callq   __es_claim_next_event
0000000000002a88        movq    0x20(%r15), %rsi
0000000000002a8c        testb   %al, %al
0000000000002a8e        je      no_more_events:	//0x2abb
// Calling the user supplied handler: Note 0x4038 is a block,
// But (q.v I/8) the block is an encapsulated function pointer, at 
// offset 0x10. So:
0000000000002a90        movq    0x4038(%rsi), %rdi
0000000000002a97        movq    -0x28(%rbp), %rbx
0000000000002a9b        movq    %rbx, %rdx
0000000000002a9e        callq   *0x10(%rdi)   <-- Handler function in block
//
// Free es_message_t supplied to block
//
0000000000002aa1        movq    %rbx, %rdi
0000000000002aa4        callq   0xa29e ## symbol stub for: _free

0000000000002aa9        movq    0x20(%r15), %rax
0000000000002aad        movb    0x4051(%rax), %al < -- Unknown0x4051 - More events
0000000000002ab3        movq    0x20(%r15), %rsi
0000000000002ab7        testb   $0x1, %al
0000000000002ab9        je      0x2a80
//
// No more events processed - reset unknown 0x4053
//
no_more_events:
0000000000002abb        xorl    %eax, %eax
0000000000002abd        xchgb   %al, 0x4053(%rsi)
0000000000002ac3        addq    $0x18, %rsp
0000000000002ac7        popq    %rbx
0000000000002ac8        popq    %r14
0000000000002aca        popq    %r15
0000000000002acc        popq    %rbp
0000000000002acd        retq

The endpointsecurityd

There is also new user mode daemon, /usr/libexec/endpointsecurityd. Following AAPL's great tradition of nigh-pointless man pages, we have:

#
# /usr/share/man is read only (yay....), so rig the manpath
#
morpheus@Zephyr (~) % MANPATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/share/man \
> man endpointsecurityd 

endpointsecurityd(8)      BSD System Manager's Manual     endpointsecurityd(8)

NAME
     endpointsecurityd -- Daemon that manages user space components of the EndpointSecurity (ES) sub-
     system

DESCRIPTION
     endpointsecurityd is a daemon that manages ES components.

     Applications can also interact with endpointsecurityd and opt into ES functionality by utilizing
     the libEndpointSecurity(3) library.  endpointsecurityd is responsible for initializing and
     starting ES System Extensions, as well as monitoring the health of ES clients and acting as a
     watchdog when necessary.

     You should not invoke endpointsecurityd directly.

FILES
     /System/Library/LaunchDaemons/com.apple.endpointsecurity.endpointsecurityd.plist
              The launchd.plist(5) controlling the endpointsecurityd job.

SEE ALSO
     EndpointSecurity(7), libEndpointSecurity(3), sysextd(8), launchd.plist(5)

Darwin                         27 November, 2018                        Darwin

Inspecting the property list, we find:

morpheus@Zephyr (~)% jlutil /System/Library/LaunchDaemons/com.apple.endpointsecurity.endpointsecurityd.plist
	ProgramArguments[0]: endpointsecurityd
	Program: /usr/libexec/endpointsecurityd
	ProcessType: Adaptive
	EnablePressuredExit:  true
	RunAtLoad:  false
	Label: com.apple.endpointsecurity.endpointsecurityd
	KeepAlive:
    	SuccessfulExit:  false
    	PathState:
        	/Library/SystemExtensions/EndpointSecurity/.launch_esd:  true
	MachServices:
		com.apple.endpointsecurity.endpointsecurityd.xpc: true
		com.apple.endpointsecurity.endpointsecurityd.mig: true
		com.apple.endpointsecurity.system-extensions: true
	

We know (from I/11) that the taint of MIG is such that __DATA_CONST.__const will have the dispatch table:

morpheus@Zephyr(~)% JCOLOR=1 jtool2 -d __DATA_CONST.__const /usr/libexec/endpointsecurityd |grep MIG
Dumping 520 bytes from 0x10000a0d0 (Offset 0xa0d0, __DATA_CONST.__const):
_autodetected_MIG_subsystem_51300: // 2 messages
0x10000a268:      0x100007039           MIG Subsystem 51300: Dispatcher
0x10000a270:    0xc864       0xc866     MIG Subsystem 51300: 2 messages
0x10000a278:    0x24         0x0        MIG Subsystem 51300: Msg size 36 bytes
0x10000a290:      0x100007062           _func_100007062 (MIG msg 51300 unmarshall)
0x10000a2b8:      0x1000070ae           _func_1000070ae (MIG msg 51301 unmarshall)

The only reason why MIG would be used in this new and exciting age of XPC is if there's a kernel source. Indeed, we find in the kext the two MIG messages, which are easy to find thanks to mach_msg_from_kernel_proper:

_es_early_boot_client_failures:
..
000000000001f8bc        movabsq $0xc86400000000, %rax     #### 51300..
000000000001f8c6        movq    %rax, -0x10(%rbx)
000000000001f8ca        leaq    -0x228(%rbp), %rdi
000000000001f8d1        movl    $0x204, %esi
000000000001f8d6        callq   _mach_msg_send_from_kernel_proper
...
_es_client_timeout_failures:
000000000001f8fe        pushq   %rbp
..
000000000001f94d        movabsq $0xc86500000000, %rcx     ### 51301
000000000001f957        movq    %rcx, 0x18(%rax)
000000000001f95b        movq    %rax, %rdi
000000000001f95e        movl    $0x3c, %esi
000000000001f963        callq   _mach_msg_send_from_kernel_proper

The XPC can easily be seen using XPoCe2 (I added this part to the v1.0.1 update of Volume II, btw):

root@Zephyr (~) # XPoCe `pgrep endpointsecurityd`
PID 91974 , RC: 0
xpc_dictionary_get_uint64 ( dictionary@0x7fc07df0c7d0,"protocol") 
 = " { count = 2, transaction: 0, voucher = 0x0, contents =
	"routine" => : 7801
	"protocol" => : 1
}"
xpc_dictionary_get_uint64 ( dictionary@0x7fc07de034f0,"protocol") 
 = " { count = 3, transaction: 0, voucher = 0x0, contents =
	"argv" =>  { count = 3, capacity = 3, contents =
		0: : 9
		1: : 11
		2: : 15
	}
	"routine" => : 7802
	"protocol" => : 1
}"

Looking through the disassembly of libEndpointSecurity.dylib again, we see that these XPC calls are performed as xpc_pipe_routine() calls over the es_get_server_pipe(), whose XPC contents are populated using __create_basic_request():

__es_clear_cache:
000000000000175d        pushq   %rbp
000000000000175e        movq    %rsp, %rbp
0000000000001761        pushq   %r14
0000000000001763        pushq   %rbx
0000000000001764        subq    $0x10, %rsp
0000000000001768        leaq    -0x18(%rbp), %r14
000000000000176c        movq    $0x0, (%r14)
0000000000001773        movl    $0x1e79, %edi              #### 7801
0000000000001778        callq   __create_basic_request
000000000000177d        movq    %rax, %rbx
0000000000001780        movq    %rax, %rdi
0000000000001783        movq    %r14, %rsi
0000000000001786        callq   __endpointsecurityd_routine
...
__es_analytics_subscription:
00000000000017d1        pushq   %rbp
00000000000017d2        movq    %rsp, %rbp
00000000000017d5        pushq   %r15
00000000000017d7        pushq   %r14
00000000000017d9        pushq   %r13
00000000000017db        pushq   %r12
00000000000017dd        pushq   %rbx
00000000000017de        pushq   %rax
00000000000017df        movl    %esi, %r12d
00000000000017e2        movq    %rdi, %r15
00000000000017e5        callq   __es_init
00000000000017ea        movl    $0x1e7a, %edi              #### 7802
00000000000017ef        callq   __create_basic_request
00000000000017f4        testq   %rax, %rax
00000000000017f7        je      0x186f
00000000000017f9        movq    %rax, %r14
00000000000017fc        xorl    %edi, %edi
00000000000017fe        xorl    %esi, %esi
0000000000001800        callq   0xa310 ## symbol stub for: _xpc_array_create
/// argv array created here..
..

com.apple.endpointsecurity.system-extensions is used by the system extension daemon , sysextd, which might be covered in a future writeup. Or not.

Some thoughts

TG Annoucement - The upcoming MOXiI training in NYC (Dec 2nd, 2019) is open for registration! I'll be discussing EPS and so much more in this training and its Followup 3-day Security/Insecurity, which is also the last one planned with Tg. You might want to follow @Technologeeks for announcements! You can drop i/n/f/o at TG an email if you want more details or to register.