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*)
:
Entitlement Notes
com.apple.developer.endpoint-security.client
Required for use by third party clients (documented)
com.apple.private.endpoint-security.dataless-manipulation
Enforced by EndpointSecurityEventManager::sendSyscallFileProviderUpdate
and EndpointSecurityEventManager::sendSyscallFileProviderMaterialization
com.apple.private.endpoint-security.client
Enforced 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
If I have some time later on I'll do a part II filling in a few of the unknowns here, but I just felt like getting this out as it's pretty detailed already.
EndpointSecurity, along with Network Extensions and the 2019 edition of DriverKit clearly demonstrate AAPL does NOT want anyone in the MacOS Kernel. *OS variants designated the kernel as off limits from day I - that worked well. So it's time to kick everyone out of the MacOS kernel as well. Kernel developers in MacOS - start looking for other jobs (but maybe still get my Volume II ;-)
EPS is still in its infancy, and will no doubt be improved (it had better be!).. But for passive process monitoring alone it is a huge overkill. The BSM Audit subsystem (and my supraudit tool ) can accomplish this with probably less overhead. KAuth was dead on arrival (somewhere in 10.4) since it can't even reliably process execution(!) and MACF is so much better. For passive monitoring (i.e. no way to block), BSM is downright perfect (not to mention standardized) as were so many innovations of the once great Solaris.
That said, for active monitoring (i.e. potentially blocking) one HAS to be in the kernel.. MACF is indeed orders of magnitude better, as is evident by its use as substrate for the SandBox and that despicable AMFI, but even though up till now AAPL turned a half blind eye to its use, it is generally the end of the line - no kexts = no MACF or KAuth use, either. EPS is thus the new normal, providing the required kernel component but delegating the decision making via upcalls to user mode.
TG Annoucement - The
upcoming MOXiI training in NYC (Dec 2
nd , 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.