
Proof-of-concept и технический разбор CVE-2026-43783 — локальное повышение привилегий в macOS через произвольный chown в XPC DesktopServicesHelper для получения root.
Первоначально опубликовано на PT SWARM или andrd3v.github.io
Демон, который Finder использует для «исправления» прав доступа к файлам iCloud, оказался готов исправлять права доступа к чему угодно — включая системные каталоги. В этой статье мы рассмотрим, как DesktopServicesHelper работает под капотом и почему одного XPC-запроса оказалось достаточно для получения root.
Я искал цели с com.apple.private.tcc.allow со значением kTCCServiceSystemPolicyAllFiles, и DesktopServicesHelper привлёк моё внимание. DesktopServicesHelper находится по адресу /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper, и Finder использует его для файловых операций, требующих повышенных привилегий: перемещение файлов, установка прав доступа, работа с Корзиной.

Загрузил бинарный файл в IDA и перешёл к start(). После обычной настройки таймера и очереди он создаёт XPC-прослушиватель — точку входа демона. Прослушиватель регистрирует mach-сервис под определённым именем и начинает ожидать входящие соединения. Любой процесс, знающий это имя, может подключиться и отправить запрос. Вот ключевая часть:
// 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();
Функция sub_1000709F4 возвращает имя mach-сервиса. В start() она вызывается с аргументом 0, что означает, что демон регистрируется под именем com.apple.DesktopServicesHelper.
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 — это блок-обработчик (callback), который демон вызывает для каждого входящего события. XPC основан на сообщениях: клиент формирует словарь с ключами и значениями, отправляет его демону, а демон разбирает содержимое и решает, что делать. Все входящие события попадают в этот обработчик. Заглянем внутрь.

Функция вызова блока — sub_100032044. При новом подключении демон сначала получает euid и egid клиента — эффективный идентификатор пользователя и идентификатор группы — идентификаторы пользователя и группы, под которыми работает подключающийся процесс:
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
Затем он назначает обработчик для входящих сообщений на соединении:
v15[2] = sub_10003AB2C; // block's invoke function
xpc_connection_set_event_handler(connection: v14, handler: v15);
xpc_connection_resume(connection: v14);
sub_10003AB2C — именно сюда приходят фактические XPC-запросы от клиента. Теперь нам нужно понять, как демон решает, какие запросы выполнять, а какие отклонять.
sub_10003AB2C — это главный диспетчер. Здесь демон решает, что делать с запросом. Функция большая, поэтому давайте разберём её по частям.
Сначала она извлекает строку "request" из сообщения — имя команды, которую клиент хочет выполнить:
reply = xpc_dictionary_create_reply(original: v12);
string = xpc_dictionary_get_string(xdict: v12, key: "request");
// ...
// logging: "Got Request '%{public}@'"
Затем демон извлекает аудит-токен подключающегося процесса и проверяет, запущен ли он внутри App Sandbox. App Sandbox — это механизм изоляции приложений macOS, ограничивающий доступ к файловой системе, сети и другим ресурсам. Здесь происходит ветвление:
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
}
}
Если клиент работает в песочнице — имя запроса проверяется по списку разрешённых из семи строк. Если совпадает — проверка пройдена и запрос проходит. Если нет — sub_100034250 убивает демон.
Если клиент не работает в песочнице — демон проверяет наличие права com.apple.private.tcc.allow со значением kTCCServiceSystemPolicyAllFiles. Отсутствие права также означает смерть.
После прохождения проверки запрос проходит через две таблицы обработчиков. Сначала sub_10002F7C8, затем 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
Теперь посмотрим на таблицы обработчиков. Первая таблица (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