Анатомия SAP Privileges: как устроено управление правами в macOS

Анатомия SAP Privileges: как устроено управление правами в macOS - 1

Всем привет! Меня зовут Булат Гафуров, я security-инженер в Яндексе. Сегодня я хочу подробно разобрать, как устроено Privileges — опенсорсное приложение для macOS, которое предназначено для быстрого и удобного управления правами администратора. Мы выясним, как взаимодействуют его компоненты, через что происходит обмен сообщениями и на чём строится доверие между процессами. А главное — разберёмся, почему вредоносным скриптам теперь станет сложнее повысить привилегии.

Всё началось с поиска инструмента для выдачи прав локального администратора «по требованию», так как классическая схема с двумя учётными записями нам не подходила. Целевая аудитория этого решения — сотрудники, кому редко нужны права администратора. Допустим, менеджеры. В итоге я остановился на Privileges от компании SAP — пожалуй, самом популярном решении этой задачи.

Мне стало интересно, как именно оно работает и можно ли пропустить подтверждение пользователя для повышения привилегий. Ведь если в этом механизме существует возможность обойти пользовательское подтверждение, то ею неизбежно начнут пользоваться все ClickFix-скрипты, что фактически сведёт ценность приложения к нулю. Не найдя в сети подробных технических разборов внутреннего устройства Privileges, я клонировал репозиторий и начал разбираться самостоятельно. Надеюсь, этот материал сэкономит вам время.


Для начала определим основные составляющие Privileges. Чтобы взаимодействовать с пользователем, приложению нужен интерфейс. В данном случае их даже несколько: графический интерфейс (GUI), консольная утилита (CLI) и иконка в меню-баре. 

Все они позволяют пользователю запрашивать повышение или отзыв прав, взаимодействуя с «бэкендом» — агентом (PrivilegesAgent). В его задачи входят:

  • Обработка запросов: приём команд от графического приложения и CLI.

  • Аутентификация: организация проверки личности (биометрия, пароль, смарт-карты).

  • Управление таймером: отслеживание времени действия привилегий и их автоматический отзыв по истечении сессии.

  • Уведомления: информирование пользователя о скором завершении времени работы с правами администратора.

  • Логирование: отправка событий в локальные и удалённые системы сбора логов (если настроено).

  • Обновление статуса: визуальное отображение оставшегося времени в меню-баре.

Взаимодействие с агентом строится на базе XPC. Однако здесь есть важный архитектурный нюанс: если приложение запущено в режиме песочницы (sandboxed), оно не может напрямую выполнить mach lookup, чтобы найти сервис агента.

Чтобы обойти это ограничение, в бандл встроен вспомогательный XPC Service. Он запускается без энтайтлмента com.apple.security.app-sandbox, что позволяет ему выступать своего рода мостом: фронтенд обращается к этому сервису, а тот, в свою очередь, успешно подключается к PrivilegesAgent.

В чём разница между XPC Service и MachService

Чтобы лучше понять архитектуру, нужно разобраться в типах XPC-сервисов. Согласно документации Apple, их можно разделить на три основные категории:

  • LaunchAgent: запускается в контексте пользовательского сеанса. Существует в одном экземпляре для конкретного пользователя.

  • LaunchDaemon: системный сервис, работающий от имени root. Запускается один раз на всю систему.

  • XPC Service (Bundled): специализированный тип сервиса, который живёт внутри бандла приложения (в директории Contents/XPCServices).

Ключевое отличие Bundled XPC Service: он запускается индивидуально для каждого клиента в момент создания подключения, и его жизненный цикл жёстко ограничен временем жизни вызвавшего его процесса. 

Основное различие для разработчика заключается в способе инициализации соединения.

Пример подключения к сервису через mach lookup (для Agent или Daemon): 

$ ls /Applications/Privileges.app/Contents/XPCServices
PrivilegesXPC.xpc

Здесь мы ищем сервис в глобальном пространстве имён по его идентификатору. Именно этот шаг блокирует песочница (sandbox), если у приложения нет соответствующих энтайтлментов.

Пример подключения к Bundled XPC Service: 

_connection = [[NSXPCConnection alloc] initWithMachServiceName:kMTAgentMachServiceName options:0];
[_connection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(PrivilegesAgentProtocol)]];

Здесь используется initWithServiceName. Система знает, что сервис находится внутри того же бандла, поэтому такое соединение разрешено даже для приложений в sandbox.

Таким образом, PrivilegesXPC выступает в роли доверенного посредника: приложение из песочницы обращается к нему, а он уже имеет право достучаться до основного агента.

Более подробно о работе с XPC-сервисами можно почитать в этой статье.

Механизм endpoint: как пробросить соединение в sandbox

Основная проблема песочницы в том, что она запрещает выполнение mach lookup для поиска сервиса corp.sap.privileges.agent. Однако в механизме XPC есть легальная лазейка: если процесс уже имеет готовое соединение, он может передать его другому процессу.

Для этого используется NSXPCListenerEndpoint — сериализуемый объект, который инкапсулирует информацию о соединении. Его можно переслать через XPC как обычный параметр, и получатель сможет создать прямой канал связи, даже находясь внутри sandbox.

Схема работы выглядит так:

Анатомия SAP Privileges: как устроено управление правами в macOS - 2

В агенте (PrivilegesAgent): сервис просто возвращает эндпоинт своего листенера в ответ на запрос:

Objective-C
- (void)connectWithEndpointReply:(void (^)(NSXPCListenerEndpoint *endpoint))reply {
    if (reply) { 
        reply([_listener endpoint]); 
    }
}

В приложении (Privileges.app): получив эндпоинт от промежуточного XPC-сервиса, приложение создаёт прямое соединение с агентом, минуя ограничения песочницы:

Objective-C
// Получили endpoint от промежуточного XPC Service
NSXPCListenerEndpoint *endpoint = ...;

// Создаём прямое соединение с Agent, игнорируя невозможность mach lookup
_connection = [[NSXPCConnection alloc] initWithListenerEndpoint:endpoint];
[_connection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(PrivilegesAgentProtocol)]];
[_connection resume];

Результат: приложение в sandbox получает полноценный двусторонний канал связи с агентом, хотя самостоятельно найти его в системе не могло.

PrivilegesDaemon: исполнение команд под root

Несмотря на то что агент управляет логикой и таймерами, сам он не имеет прав на изменение состава групп пользователей, так как запущен в контексте обычной пользовательской сессии. Для выполнения привилегированных операций он обращается к PrivilegesDaemon.

PrivilegesDaemon — это классический LaunchDaemon, работающий от имени root. Именно он выполняет «грязную работу» по управлению правами доступа.

Когда агент даёт команду на изменение прав, демон использует системные вызовы CSIdentityAddMember или CSIdentityRemoveMember, чтобы добавить пользователя в группу Administrators (GID 80) или исключить его из неё.

Можно ли повысить привилегии скрытно

Если в системе установлена консольная утилита, логично предположить, что её можно использовать для автоматизации. На первый взгляд кажется, что достаточно выполнить команду:

/Applications/Privileges.app/Contents/MacOS/PrivilegesCLI -a

Права администратора действительно появятся, но с одной важной оговоркой: если в настройках не включено подтверждение паролем или биометрией. В противном случае PrivilegesCLI прервётся и потребует приложить палец к датчику Touch ID, ввести пароль или воспользоваться смарт-картой.

Анатомия SAP Privileges: как устроено управление правами в macOS - 3

Здесь кроется интересный архитектурный нюанс: PrivilegesCLI сам не реализует логику проверки биометрии. Вместо этого он обращается к агенту (PrivilegesAgent), вызывая соответствующий метод. Если аутентификация проходит успешно, CLI следующим шагом вызывает метод requestAdminRightsWithReason.

Тот факт, что аутентификация и запрос прав — это два разных вызова в коде CLI, наводит на мысль: а нельзя ли просто пропустить этап подтверждения личности и сразу отправить запрос на повышение прав в агент или напрямую в демон?

К счастью, разработчики предусмотрели этот сценарий. Механизмы защиты здесь работают на уровне XPC-соединения:

  1. Проверка вызывающей стороны: и агент, и демон при получении запроса проверяют Audit Token вызывающего процесса.

  2. Code Signing: система проверяет не только идентификатор приложения (Bundle ID), но и его цифровую подпись.

  3. Ограничение доступа: если запрос на requestAdminRights приходит от процесса, подпись которого не совпадает с ожидаемой (например, от стороннего скрипта или модифицированного бинарника), соединение будет разорвано.

Таким образом, нельзя просто притвориться консольной утилитой Privileges. Чтобы успешно вызвать метод повышения прав, ваш процесс должен обладать тем же уровнем доверия и той же подписью, что и официальные компоненты SAP Privileges.

Как работает проверка подписи кода

Каждое XPC-соединение в macOS сопровождается audit_token — системным идентификатором процесса, который невозможно подделать. Когда клиент (например, CLI или приложение) подключается к агенту или демону, происходит трёхэтапная проверка безопасности.

Шаг 1. Получение собственной подписи

Сначала сервис (агент или демон) определяет параметры собственной подписи с помощью SecCodeCopySelf и SecCodeCopySigningInformation. Это нужно для того, чтобы в дальнейшем требовать от клиента подпись того же издателя.

SecCodeRef helperCodeRef = NULL;
SecCodeCopySelf(kSecCSDefaultFlags, &helperCodeRef);

SecStaticCodeRef staticCodeRef = NULL;
SecCodeCopyStaticCode(helperCodeRef, kSecCSDefaultFlags, &staticCodeRef);

CFDictionaryRef signingInfo = NULL;
SecCodeCopyStaticCode(staticCodeRef, kSecCSSigningInformation, &signingInfo);

// Извлекаем Common Name (CN) из сертификата
CFArrayRef certChain = CFDictionaryGetValue(signingInfo, kSecCodeInfoCertificates);
SecCertificateRef issuerCert = (SecCertificateRef)CFArrayGetValueAtIndex(certChain, 0);
SecCertificateCopyCommonName(issuerCert, &subjectCN); 
// В итоге получаем что-то вроде: "Developer ID Application: SAP SE (7R5ZEU67FQ)"

Шаг 2. Формирование требований (Code Requirements)

На основе полученных данных сервис формирует строку требований. Это набор правил, которым обязан соответствовать любой процесс, пытающийся подключиться по XPC.

NSString *reqString = [NSString stringWithFormat:
    @"anchor apple generic and "                        // Сертификат выдан Apple
    @"certificate leaf [subject.CN] = "%@" and "      // Подписан тем же разработчиком (SAP SE)
    @"info [CFBundleShortVersionString] >= "%@" and " // Версия не ниже текущей (защита от Rollback)
    @"info [CFBundleIdentifier] = %@",                  // Конкретный bundle identifier
    commonName, versionString, bundleIdentifier];

Логика доверия здесь следующая:

  • Демон принимает соединения только от агента (corp.sap.privileges.agent).

  • Агент принимает соединения от любого компонента Privileges (используется маска corp.sap.privileges*).

Такую проверку практически невозможно обойти без кражи закрытого ключа разработчика, так как за соблюдение правил отвечает само ядро macOS.

Больше про Code Requirement — в документации. А ещё есть пример, когда можно обойти Code Requirement, там нет anchor trusted.

Шаг 3. Проверка клиента

В момент входящего вызова сервис берёт audit_token соединения, создаёт на его основе объект SecTask и проверяет его на соответствие требованиям из шага 2.

// Получаем audit_token из входящего XPC-соединения
audit_token_t token = [newConnection auditToken];

// Создаём SecTask для проверки вызывающего процесса
SecTaskRef taskRef = SecTaskCreateWithAuditToken(NULL, token);

// Проверяем, соответствует ли клиент нашим требованиям (reqString)
OSStatus result = SecTaskValidateForRequirement(taskRef, (__bridge CFStringRef)reqString);

if (result == errSecSuccess) {
    // Клиент прошёл проверку — разрешаем взаимодействие
    acceptConnection = YES;
} else {
    // Подпись не совпадает или идентификатор поддельный — рубим соединение
    os_log_error(OS_LOG_DEFAULT, "SAPCorp: Code signature verification failed");
}

Что даёт такая архитектура

Разделение на компоненты и жёсткая проверка подписи формируют несколько эшелонов обороны:

  • Защита от подделки (Impersonation). Даже если злоумышленник полностью реверс-инжинирит протокол обмена и напишет свой XPC-клиент, он не сможет пройти проверку SecTaskValidateForRequirement. Чтобы агент или демон были обмануты, вредоносный процесс должен быть подписан тем же сертификатом разработчика (SAP SE), что в обычных условиях невозможно.

  • Защита от даунгрейд-атак. Проверка версии (CFBundleShortVersionString >= …) гарантирует, что атакующий не сможет подсунуть в систему старую, официально подписанную, но уязвимую версию компонента Privileges, чтобы эксплуатировать в ней старые баги.

Строгая изоляция компонентов. Благодаря проверке Bundle ID на уровне демона работу по управлению группами может инициировать только агент (corp.sap.privileges.agent). Никакой другой процесс — даже оригинальный PrivilegesCLI — не может отправить запрос демону напрямую. Это исключает ситуацию, когда локальный пользователь пытается вызвать методы демона в обход логики агента.

Роль PrivilegesWatcher: обратная связь и мониторинг

PrivilegesWatcher отвечает за мониторинг состояния системы. Его главная задача — следить за тем, чтобы информация в интерфейсе пользователя всегда была актуальной, даже если права изменились в обход самого приложения.

Как это работает технически:

  1. Наблюдение за файловой системой. Watcher подписывается на события в директории /var/db/dslocal/nodes/Default/groups. Именно здесь в macOS хранятся данные о группах в формате XML (plist).

  2. Отслеживание admin.plist. Как только файл admin.plist (отвечающий за состав группы администраторов) изменяется, Watcher фиксирует это событие.

Рассылка уведомлений. После фиксации изменений Watcher отправляет системное уведомление через DistributedNotificationCenter.

Это уведомление подхватывает PrivilegesAgent. Он повторно проверяет текущий статус прав пользователя и, если они действительно изменились, запускает цепочку действий:

  • Логирование → фиксирует факт изменения прав в системном журнале.

  • Обновление интерфейса → отправляет внутренние сигналы основным компонентам — Privileges (главному приложению) и PrivilegesTile (виджету или иконке в меню-баре).

Благодаря такой схеме пользователь мгновенно видит актуальный статус («Администратор» или «Стандартный пользователь») и корректный отсчёт таймера, вне зависимости от того, как именно были выданы права.

Вопросы защиты

Могу ли я подключиться отладчиком к PrivilegesCLI и пропустить подтверждение от пользователя?

Раз проверка (вызов биометрии или пароля) инициируется на стороне PrivilegesCLI, у исследователя может возникнуть логичное желание подключиться к процессу отладчиком (например, LLDB) и просто перепрыгнуть через инструкции, отвечающие за аутентификацию.

Однако здесь в игру вступает Hardened Runtime — механизм защиты целостности процессов в macOS. Поскольку PrivilegesCLI подписан с включённым Hardened Runtime и не имеет специального энтайтлмента com.apple.security.get-task-allow (который разрешает отладку), система пресечёт любую попытку прикрепиться к процессу.

При попытке отладки вы получите классическую ошибку:

error: process exited with status -1 (attach failed (Not allowed to attach to process. Look in the console messages (Console.app), near the debugserver entries, when the attach failed.))

Система защиты macOS на уровне ядра блокирует ptrace и другие механизмы инъекций в доверенные бинарники. Таким образом, даже имея доступ к исполняемому файлу в пользовательской сессии, вы не сможете изменить логику его выполнения на лету без нарушения целостности подписи. А если вы переподпишете бинарник своей подписью, чтобы убрать Hardened Runtime, то агент и демон мгновенно перестанут вам доверять.

А как насчёт PrivilegesXPC — можно ли подцепиться к нему?

Если с основным CLI всё понятно, то что мешает нам атаковать промежуточное звено — PrivilegesXPC? Оказывается, здесь защита ещё серьёзнее. Попытка отладки или прямого запуска этого сервиса провалится по двум причинам:

  • Использование Hardened Runtime (как и в случае с CLI).

  • Наличие Launch Constraints (ограничений запуска).

Если вы попробуете запустить бинарник сервиса напрямую из терминала:

/Applications/Privileges.app/Contents/XPCServices/PrivilegesXPC.xpc/Contents/MacOS/PrivilegesXPC

…система мгновенно завершит процесс: [1] 13757 killed.

Причина кроется в метаданных подписи. Давайте заглянем в них с помощью codesign:

codesign -dvvvv /Applications/Privileges.app/Contents/XPCServices/PrivilegesXPC.xpc/Contents/MacOS/PrivilegesXPC

В выводе мы увидим два ключевых блока: Parent Launch Constraints и Responsible Launch Constraints.

Parent Launch Constraints определяют, кто имеет право быть «родителем» (тем, кто непосредственно выполняет запуск) для этого процесса.

[Key] reqs
[Value]
    [Dict]
        [Key] is-init-proc
        [Value]
            [Bool] true

Здесь указано is-init-proc: true. Это означает, что запустить данный сервис может только процесс с PID 1, то есть системный менеджер процессов launchd. Любая попытка запустить его из Bash, Zsh или через fork() другого приложения будет пресечена ядром macOS.

Responsible Launch Constraints определяют требования к приложению, которое инициировало запуск сервиса (установило соединение).

[Key] reqs
[Value]
    [Dict]
        [Key] signing-identifier
        [Value]
            [String] corp.sap.privileges
        [Key] team-identifier
        [Value]
            [String] 7R5ZEU67FQ

Это означает, что «ответственным» за запуск может быть только приложение с Team ID 7R5ZEU67FQ и Bundle ID corp.sap.privileges.

Комбинация этих ограничений делает PrivilegesXPC практически неуязвимым для внешних манипуляций:

  • Вы не можете запустить его сами (мешает Parent Constraint).

  • Вы не можете заставить его запуститься из своего вредоносного кода (мешает Responsible Constraint).

  • Вы не можете вмешаться в его работу через отладчик (мешает Hardened Runtime).

Почитать больше о типах ограничений можно в доке Applying launch environment and library constraints, о возможных полях — в Defining launch environment and library constraints.

Почему не получается отключить биометрию в настройках?

Обычно приложения на macOS хранят свои параметры в системе User Defaults. Это стандартный механизм, где настройки лежат в plist-файлах по пути ~/Library/Preferences/<bundle-id>.plist. Пользователь может менять их через GUI или терминал:

defaults write corp.sap.privileges RequireAuthentication -bool false

Однако в корпоративной среде это часто не срабатывает. Причина — в строгой иерархии, по которой macOS формирует итоговый набор настроек при запуске приложения:

  1. Managed (MDM / конфигурационные профили): самый высокий приоритет. Если настройка задана администратором через профиль (Mobile Device Management), она перекрывает все остальные уровни.

  2. App (пользовательские настройки): то, что вы меняете кнопками в интерфейсе или командой defaults write.

  3. Globals: глобальные параметры, общие для всех приложений в системе.

Registration: настройки по умолчанию, которые разработчик зашил в код (через registerDefaults:).

Таким образом, если ваша компания через MDM-профиль закрепила обязательное подтверждение биометрией, любые попытки пользователя изменить этот параметр (даже через терминал) будут игнорироваться. Система просто подставит значение из «управляемого» (Managed) слоя, который имеет приоритет над пользовательским.

Полный флоу: как Privileges выдаёт права администратора

   

Анатомия SAP Privileges: как устроено управление правами в macOS - 4

Чтобы наглядно представить, какой путь проходит ваш клик в интерфейсе, давайте проследим его по всей цепочке:

  1. Запрос. Пользователь нажимает кнопку в Privileges.app или вводит команду в PrivilegesCLI.

  2. Проверка настроек. Приложение запрашивает User Defaults. Если через MDM спущен профиль с обязательной аутентификацией, приложение обязано запросить пароль или Touch ID.

  3. Выход из sandbox. Privileges.app не может напрямую найти основной сервис. Оно подключается к Bundled XPC Service, который пробрасывает NSXPCListenerEndpoint от агента.

  4. Аутентификация. PrivilegesAgent инициирует системное окно подтверждения (биометрия/пароль).

  5. Проверка доверия (шаг 1). Агент получает запрос от клиента. С помощью audit_token он проверяет, что клиент — это именно оригинальный компонент Privileges, подписанный SAP SE.

  6. Делегирование. Если проверка пройдена, агент отправляет запрос к PrivilegesDaemon.

  7. Проверка доверия (шаг 2). Демон (работающий под root) проверяет подпись агента. Благодаря Launch Constraints демон уверен, что запрос пришёл от доверенного процесса, запущенного системой (launchd).

  8. Исполнение. Демон выполняет системный вызов CSIdentityAddMember, добавляя пользователя в группу admin(GID 80).

  9. Мониторинг. PrivilegesWatcher замечает изменение в /var/db/dslocal/.../admin.plist и рассылает уведомление.

  10. Обновление. Все компоненты меняют статус в интерфейсе, иконка в меню-баре начинает отсчёт времени.

Резюме

На первый взгляд такая схема может показаться избыточной. Зачем столько промежуточных звеньев, если можно было просто сделать один SUID-бинарник?

Однако именно такая архитектура соответствует современным рекомендациям Apple в гайде Designing Secure Helpers and Daemons:

  • Принцип наименьших привилегий: GUI-приложение ничего не знает о группах, а демон ничего не знает об интерфейсе.

  • Использование sandbox: взаимодействие с пользователем максимально изолировано.

  • Разделение ответственности: каждый компонент выполняет строго ограниченный список действий.

  • Верификация: проверка метаданных, подписи и использование audit_token на каждом этапе XPC-взаимодействия.

Для системных администраторов это означает, что инструменту можно доверять (особенно при связке с MDM). Для разработчиков же это один из хороших примеров, как стоит писать системные утилиты сегодня. 

Спасибо всем, кто дочитал до конца! Исследуйте код, используйте правильные энтайтлменты и помните: безопасность начинается с архитектуры.

Автор: bulatgafurov

Источник

Оставить комментарий