
Proof-of-Concept und technischer Writeup für CVE-2026-43783, eine lokale Rechteausweitung unter macOS über DesktopServicesHelper XPC beliebiges chown, um Root-Rechte zu erlangen.
Ursprünglich veröffentlicht auf PT SWARM oder andrd3v.github.io
Der Daemon, den Finder verwendet, um Berechtigungen von iCloud-Dateien zu „reparieren“, zeigte sich bereit, Berechtigungen an allem zu reparieren – einschließlich Systemverzeichnissen. In diesem Artikel schauen wir uns an, wie DesktopServicesHelper unter der Haube funktioniert und warum eine einzige XPC-Anfrage ausreichte, um Root zu erlangen.
Ich war auf der Suche nach Zielen mit com.apple.private.tcc.allow und dem Wert kTCCServiceSystemPolicyAllFiles, und DesktopServicesHelper fiel mir ins Auge. DesktopServicesHelper befindet sich unter /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper, und Finder verwendet ihn für Dateioperationen, die erhöhte Privilegien erfordern: Verschieben von Dateien, Setzen von Berechtigungen, Arbeiten mit dem Papierkorb.

Ich lud die Binärdatei in IDA und ging zu start(). Nach dem üblichen Timer- und Queue-Setup erstellt sie einen XPC-Listener – den Einstiegspunkt des Daemons. Der Listener registriert einen Mach-Dienst unter einem bestimmten Namen und beginnt, auf eingehende Verbindungen zu lauschen. Jeder Prozess, der diesen Namen kennt, kann sich verbinden und eine Anfrage senden. Hier ist der entscheidende Teil:
// get the service name
v16 = (const char *)sub_1000709F4(a1: 0);
// register the listener
mach_service = xpc_connection_create_mach_service(name: v16, targetq: nullptr, flags: 1u);
// set the incoming event handler
xpc_connection_set_event_handler(connection: mach_service, handler: &stru_1000B4B48);
xpc_connection_resume(connection: mach_service);
// start the event loop
CFRunLoopRun();
Die Funktion sub_1000709F4 gibt den Mach-Service-Namen zurück. In start() wird sie mit dem Argument 0 aufgerufen, was bedeutet, dass der Daemon sich unter dem Namen com.apple.DesktopServicesHelper registriert.
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 ist ein Handler-Block (Callback), den der Daemon für jedes eingehende Ereignis aufruft. XPC ist nachrichtenorientiert: Der Client erstellt ein Dictionary mit Schlüsseln und Werten, sendet es an den Daemon, und der Daemon analysiert den Inhalt und entscheidet, was zu tun ist. Alle eingehenden Ereignisse landen in diesem Handler. Schauen wir uns das Innere an.

Die Invoke-Funktion des Blocks ist sub_100032044. Bei einer neuen Verbindung ruft der Daemon zunächst die euid und egid des Clients ab – effektive Benutzer-ID und Gruppen-ID – die Bezeichner des Benutzers und der Gruppe, unter denen der verbindende Prozess läuft:
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
Dann weist es einen Handler für eingehende Nachrichten auf der Verbindung zu:
v15[2] = sub_10003AB2C; // block's invoke function
xpc_connection_set_event_handler(connection: v14, handler: v15);
xpc_connection_resume(connection: v14);
sub_10003AB2C – hier treffen die eigentlichen XPC-Anfragen des Clients ein. Jetzt müssen wir verstehen, wie der Daemon entscheidet, welche Anfragen ausgeführt und welche abgelehnt werden.
sub_10003AB2C ist der Haupt-Dispatcher. Hier entscheidet der Daemon, was mit einer Anfrage geschehen soll. Die Funktion ist groß, daher wollen wir sie aufschlüsseln.
Zuerst extrahiert sie den String "request" aus der Nachricht – den Befehlsnamen, den der Client ausführen möchte:
reply = xpc_dictionary_create_reply(original: v12);
string = xpc_dictionary_get_string(xdict: v12, key: "request");
// ...
// logging: "Got Request '%{public}@'"
Dann ruft der Daemon das Audit-Token des verbindenden Prozesses ab und prüft, ob dieser in der App Sandbox läuft. Die App Sandbox ist ein Isolationsmechanismus für macOS-Anwendungen, der den Zugriff auf das Dateisystem, das Netzwerk und andere Ressourcen einschränkt. Hier verzweigt sich der Ablauf:
xpc_dictionary_get_audit_token(a1: v12, a2: buf);
qmemcpy(__p, buf, sizeof(__p));
if ( (unsigned int)sandbox_check_by_audit_token(a1: __p, a2: 0, a3: 0) != 0 )
{
// client is sandboxed - check against allowlist
if ( (sub_10003B25C(a1: &v32, a2: "Handshake") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "exit") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "SetDefaultPermissionOnHomeSubdirectories") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "MoveAsideICloudToArchiveInHome") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "RepairPermissionsForCloudItems") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "CopyClientActive") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "DeleteItem") & 1) == 0 )
{
sub_100034250(a1: v12, a2: &v32); // not in the list - kill the daemon
}
}
else
{
// client is not sandboxed - check entitlement
*(_QWORD *)buf = sub_1000343C0(a1: v12, a2: CFSTR("com.apple.private.tcc.allow"));
v19 = (const __CFArray *)sub_10003B1E0();
v20 = v19;
if ( v19 == nullptr
|| (v35.length = CFArrayGetCount(theArray: v19),
v35.location = 0,
CFArrayContainsValue(theArray: v20, range: v35, value: CFSTR("kTCCServiceSystemPolicyAllFiles")) == 0) )
{
sub_100034250(a1: v12, a2: &v32); // no entitlement - kill the daemon
}
}
Wenn der Client in einer Sandbox läuft, wird der Anforderungsname gegen eine Allowlist aus sieben Zeichenketten geprüft. Bei Übereinstimmung besteht die Prüfung und die Anforderung wird durchgelassen. Andernfalls beendet sub_100034250 den Daemon.
Wenn der Client nicht in einer Sandbox läuft, prüft der Daemon auf die Entitlement com.apple.private.tcc.allow mit dem Wert kTCCServiceSystemPolicyAllFiles. Fehlt die Entitlement, bedeutet das ebenfalls das Ende.
Nach bestandener Prüfung durchläuft die Anforderung zwei Handler-Tabellen. Zuerst sub_10002F7C8, dann sub_10002A194:
// first table (8 commands, two arguments)
v21 = sub_10002F7C8(a1: buf);
// ...
if ( v21 != 0 )
v22(a1: v12, a2: v11); // found - invoke
// second table (21 commands, three arguments)
v27 = sub_10002A194(a1: buf);
// ...
if ( v27 != nullptr )
v27(a1: v12, a2: reply, a3: v11); // found - invoke
Sehen wir uns nun die Handler-Tabellen an. Die erste Tabelle (sub_10002F7C8):
__int64 __fastcall sub_10002F7C8(__int64 a1)
{
__int64 result; // x0
void **v3; // x21
void **v4; // x8
_BYTE v5[32]; // [xsp+8h] [xbp-128h] BYREF
__int64 v6; // [xsp+28h] [xbp-108h] BYREF
__int64 v7; // [xsp+48h] [xbp-E8h] BYREF
__int64 v8; // [xsp+68h] [xbp-C8h] BYREF
__int64 v9; // [xsp+88h] [xbp-A8h] BYREF
__int64 v10; // [xsp+A8h] [xbp-88h] BYREF
__int64 v11; // [xsp+C8h] [xbp-68h] BYREF
__int64 v12; // [xsp+E8h] [xbp-48h] BYREF
__int64 v13; // [xsp+108h] [xbp-28h] BYREF