
Prueba de concepto y análisis técnico de CVE-2026-43783, una escalada de privilegios local en macOS a través de DesktopServicesHelper XPC con chown arbitrario para obtener root.
Publicado originalmente en PT SWARM o andrd3v.github.io
El daemon que Finder utiliza para "reparar" los permisos de los archivos de iCloud resultó estar dispuesto a reparar permisos de cualquier cosa, incluidos los directorios del sistema. En este artículo veremos cómo funciona DesktopServicesHelper internamente y por qué una sola solicitud XPC fue suficiente para obtener root.
Estaba buscando objetivos con com.apple.private.tcc.allow con el valor kTCCServiceSystemPolicyAllFiles y DesktopServicesHelper llamó mi atención. DesktopServicesHelper se encuentra en /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper, y Finder lo utiliza para operaciones de archivos que requieren privilegios elevados: mover archivos, establecer permisos, trabajar con la Papelera.

Cargué el binario en IDA y fui a start(). Pasada la configuración habitual de temporizadores y colas, crea un listener XPC, el punto de entrada del daemon. El listener registra un servicio mach bajo un nombre específico y comienza a escuchar conexiones entrantes. Cualquier proceso que conozca este nombre puede conectarse y enviar una solicitud. Aquí está la parte clave:
// 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();
La función sub_1000709F4 devuelve el nombre del servicio mach. En start() se llama con el argumento 0, lo que significa que el daemon se registra bajo el nombre com.apple.DesktopServicesHelper.
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 es un bloque manejador (callback) que el daemon invoca para cada evento entrante. XPC está basado en mensajes: el cliente construye un diccionario con claves y valores, lo envía al daemon, y el daemon analiza el contenido y decide qué hacer. Todos los eventos entrantes terminan en este manejador. Veamos su interior.

La función de invocación del bloque es sub_100032044. En una nueva conexión, el daemon primero obtiene el euid y el egid del cliente - ID de usuario efectivo e ID de grupo - los identificadores del usuario y del grupo bajo los cuales se ejecuta el proceso que se conecta:
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
Luego asigna un manejador para los mensajes entrantes en la conexión:
v15[2] = sub_10003AB2C; // block's invoke function
xpc_connection_set_event_handler(connection: v14, handler: v15);
xpc_connection_resume(connection: v14);
sub_10003AB2C - aquí es donde llegan las solicitudes XPC reales del cliente. Ahora necesitamos entender cómo el daemon decide qué solicitudes ejecutar y cuáles rechazar.
sub_10003AB2C es el despachador principal. Aquí es donde el daemon decide qué hacer con una solicitud. La función es grande, así que vamos a desglosarla.
Primero, extrae la cadena "request" del mensaje - el nombre del comando que el cliente quiere ejecutar:
reply = xpc_dictionary_create_reply(original: v12);
string = xpc_dictionary_get_string(xdict: v12, key: "request");
// ...
// logging: "Got Request '%{public}@'"
Luego, el daemon recupera el token de auditoría del proceso que se conecta y verifica si se está ejecutando dentro de App Sandbox. App Sandbox es un mecanismo de aislamiento de aplicaciones de macOS que restringe el acceso al sistema de archivos, la red y otros recursos. Aquí es donde se bifurca:
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
}
}
Si el cliente está en sandbox, el nombre de la solicitud se compara con una lista de permitidos de siete cadenas. Si coincide, la comprobación pasa y la solicitud continúa. Si no, sub_100034250 mata al daemon.
Si el cliente no está en sandbox, el daemon comprueba el entitlement com.apple.private.tcc.allow con el valor kTCCServiceSystemPolicyAllFiles. La ausencia del entitlement también significa la muerte.
Tras pasar la comprobación, la solicitud atraviesa dos tablas de controladores. Primero sub_10002F7C8, luego 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
Ahora veamos las tablas de controladores. La primera tabla (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