
Prova de conceito e análise técnica para CVE-2026-43783, uma escalada de privilégios local no macOS via DesktopServicesHelper XPC chown arbitrário para obter root.
Publicado originalmente em PT SWARM ou andrd3v.github.io
O daemon que o Finder usa para "reparar" permissões em arquivos do iCloud mostrou-se disposto a reparar permissões em qualquer coisa - incluindo diretórios do sistema. Neste artigo veremos como o DesktopServicesHelper funciona internamente e por que uma única requisição XPC foi suficiente para obter root.
Eu estava procurando alvos com com.apple.private.tcc.allow com o valor kTCCServiceSystemPolicyAllFiles e o DesktopServicesHelper chamou minha atenção. O DesktopServicesHelper fica em /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper, e o Finder o utiliza para operações de arquivo que exigem privilégios elevados: mover arquivos, definir permissões, trabalhar com a Lixeira.

Carreguei o binário no IDA e fui para start(). Depois da configuração habitual de timer e fila, ele cria um listener XPC - o ponto de entrada do daemon. O listener registra um serviço mach sob um nome específico e começa a escutar por conexões de entrada. Qualquer processo que conheça esse nome pode se conectar e enviar uma requisição. Aqui está a parte principal:
// 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();
A função sub_1000709F4 retorna o nome do serviço mach. Em start(), ela é chamada com o argumento 0, o que significa que o daemon se registra sob o nome com.apple.DesktopServicesHelper.
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 é um bloco handler (callback) que o daemon invoca para cada evento recebido. O XPC é baseado em mensagens: o cliente constrói um dicionário com chaves e valores, envia-o ao daemon, e o daemon analisa o conteúdo e decide o que fazer. Todos os eventos recebidos acabam neste handler. Vamos ver o que há dentro.

A função de invocação do bloco é sub_100032044. Em uma nova conexão, o daemon primeiro recupera o euid e o egid do cliente - ID de usuário efetivo e ID de grupo - os identificadores do usuário e do grupo sob os quais o processo que se conecta é executado:
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
Em seguida, ele atribui um manipulador para mensagens recebidas na conexão:
v15[2] = sub_10003AB2C; // block's invoke function
xpc_connection_set_event_handler(connection: v14, handler: v15);
xpc_connection_resume(connection: v14);
sub_10003AB2C - é aqui que chegam as solicitações XPC reais do cliente. Agora precisamos entender como o daemon decide quais solicitações executar e quais rejeitar.
sub_10003AB2C é o dispatcher principal. É aqui que o daemon decide o que fazer com uma solicitação. A função é grande, então vamos dividi-la.
Primeiro, ela extrai a string "request" da mensagem - o nome do comando que o cliente deseja executar:
reply = xpc_dictionary_create_reply(original: v12);
string = xpc_dictionary_get_string(xdict: v12, key: "request");
// ...
// logging: "Got Request '%{public}@'"
Em seguida, o daemon recupera o token de auditoria do processo que está se conectando e verifica se ele está sendo executado dentro do App Sandbox. O App Sandbox é um mecanismo de isolamento de aplicações do macOS que restringe o acesso ao sistema de arquivos, à rede e a outros recursos. É aqui que ele se ramifica:
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
}
}
Se o cliente estiver em sandbox - o nome do pedido é verificado contra uma allowlist de sete strings. Se corresponder - a verificação passa e o pedido prossegue. Caso contrário - sub_100034250 mata o daemon.
Se o cliente não estiver em sandbox - o daemon verifica a entitlement com.apple.private.tcc.allow com o valor kTCCServiceSystemPolicyAllFiles. A ausência da entitlement também significa morte.
Após passar na verificação, o pedido atravessa duas tabelas de handlers. Primeiro sub_10002F7C8, depois 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
Agora vamos examinar as tabelas de handlers. A primeira tabela (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