Anatomia dos Privilégios do SAP: Como Funciona o Gerenciamento de Direitos no macOS

Anatomia dos Privilégios do SAP: Como Funciona o Gerenciamento de Direitos no macOS

Explore em detalhes o funcionamento interno do Privileges, um aplicativo de código aberto para macOS da SAP, que gerencia direitos de administrador. Descubra como seus componentes interagem, a troca de mensagens e a construção de confiança entre processos, dificultando a elevação de privilégios por scripts maliciosos.

MundiX News·09 de maio de 2026·13 min de leitura·👁 8 views

Olá a todos! Meu nome é Bulat Gafurov, sou engenheiro de segurança na Yandex. Hoje, quero analisar detalhadamente como funciona o Privileges – um aplicativo de código aberto para macOS, projetado para o gerenciamento rápido e conveniente de direitos de administrador. Vamos descobrir como seus componentes interagem, através de quê ocorre a troca de mensagens e em quê se baseia a confiança entre os processos. E o mais importante – entenderemos por que será mais difícil para scripts maliciosos elevarem privilégios a partir de agora.

Tudo começou com a busca por uma ferramenta para conceder direitos de administrador local "sob demanda", pois o esquema clássico com duas contas de usuário não nos servia. O público-alvo dessa solução são os funcionários que raramente precisam de direitos de administrador. Por exemplo, gerentes. No final, optei pelo Privileges da SAP – talvez a solução mais popular para essa tarefa. Fiquei curioso para saber como exatamente ele funciona e se é possível pular a confirmação do usuário para a elevação de privilégios. Afinal, se houver uma maneira de contornar a confirmação do usuário nesse mecanismo, todos os scripts ClickFix inevitavelmente começarão a usá-la, o que efetivamente anulará o valor do aplicativo. Não encontrando análises técnicas detalhadas do funcionamento interno do Privileges na rede, clonei o repositório e comecei a investigar por conta própria. Espero que este material economize seu tempo.

Para começar, vamos definir os principais componentes do Privileges. Para interagir com o usuário, o aplicativo precisa de uma interface. Neste caso, existem até várias: uma interface gráfica (GUI), um utilitário de linha de comando (CLI) e um ícone na barra de menu. Todos eles permitem que o usuário solicite a elevação ou revogação de direitos, interagindo com o "backend" – um agente (PrivilegesAgent). Suas tarefas incluem:

  • Processamento de solicitações: Receber comandos do aplicativo gráfico e do CLI.
  • Autenticação: Organizar a verificação de identidade (biometria, senha, smart cards).
  • Gerenciamento de timer: Monitorar o tempo de validade dos privilégios e sua revogação automática ao final da sessão.
  • Notificações: Informar o usuário sobre o término iminente do tempo de trabalho com direitos de administrador.
  • Logging: Enviar eventos para sistemas locais e remotos de coleta de logs (se configurado).
  • Atualização de status: Exibição visual do tempo restante na barra de menu.

A interação com o agente é construída com base em XPC. No entanto, há uma nuance arquitetural importante: se o aplicativo for executado em modo sandbox, ele não pode executar diretamente um mach_lookup para encontrar o serviço do agente. Para contornar essa limitação, um serviço XPC auxiliar está embutido no bundle. Ele é executado sem o entitlement com.apple.security.app-sandbox, o que permite que ele atue como uma espécie de ponte: o frontend acessa este serviço, e este, por sua vez, se conecta com sucesso ao PrivilegesAgent.

Qual a diferença entre XPC Service e MachService?

Para entender melhor a arquitetura, é preciso compreender os tipos de serviços XPC. De acordo com a documentação da Apple, eles podem ser divididos em três categorias principais:

  • LaunchAgent: Executado no contexto da sessão do usuário. Existe em uma única instância para um usuário específico.
  • LaunchDaemon: Um serviço do sistema, executado em nome do root. É iniciado uma vez para todo o sistema.
  • XPC Service (Bundled): Um tipo especializado de serviço que reside dentro do bundle do aplicativo (no diretório Contents/XPCServices).

A principal diferença do Bundled XPC Service é que ele é iniciado individualmente para cada cliente no momento em que a conexão é criada, e seu ciclo de vida é estritamente limitado ao tempo de vida do processo que o chamou.

A principal diferença para o desenvolvedor reside na forma de inicialização da conexão.

Exemplo de conexão a um serviço via mach_lookup (para Agent ou Daemon):

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

Aqui, procuramos o serviço no namespace global pelo seu identificador. É este passo que o sandbox bloqueia, se o aplicativo não tiver os entitlements apropriados.

Exemplo de conexão a um Bundled XPC Service:

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

Aqui, é usado initWithServiceName. O sistema sabe que o serviço está dentro do mesmo bundle, portanto, tal conexão é permitida mesmo para aplicativos em sandbox.

Assim, o PrivilegesXPC atua como um intermediário confiável: o aplicativo do sandbox acessa-o, e ele, por sua vez, tem permissão para acessar o agente principal.

Você pode ler mais sobre como trabalhar com serviços XPC neste artigo.

Mecanismo de Endpoint: Como encaminhar a conexão para o sandbox

O principal problema do sandbox é que ele proíbe a execução de mach_lookup para encontrar o serviço corp.sap.privileges.agent. No entanto, existe uma brecha legal no mecanismo XPC: se um processo já possui uma conexão pronta, ele pode transferi-la para outro processo. Para isso, é usado o NSXPCListenerEndpoint – um objeto serializável que encapsula informações sobre a conexão. Ele pode ser enviado via XPC como um parâmetro comum, e o destinatário poderá criar um canal de comunicação direto, mesmo estando dentro do sandbox.

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

  • No agente (PrivilegesAgent): O serviço simplesmente retorna o endpoint de seu listener em resposta a uma solicitação:

    objc
    // Objective-C
    - (void)connectWithEndpointReply:(void (^)(NSXPCListenerEndpoint *endpoint))reply {
        if (reply) {
            reply([_listener endpoint]);
        }
    }
  • No aplicativo (Privileges.app): Após receber o endpoint do serviço XPC intermediário, o aplicativo cria uma conexão direta com o agente, contornando as restrições do sandbox:

    objc
    // Objective-C
    // Recebemos o endpoint do serviço XPC intermediário
    NSXPCListenerEndpoint *endpoint = ...;
    
    // Criamos uma conexão direta com o Agent, ignorando a impossibilidade de mach_lookup
    _connection = [[NSXPCConnection alloc] initWithListenerEndpoint:endpoint];
    [_connection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(PrivilegesAgentProtocol)]];
    [_connection resume];

Resultado: O aplicativo em sandbox obtém um canal de comunicação bidirecional completo com o agente, embora não pudesse encontrá-lo no sistema por conta própria.

PrivilegesDaemon: Execução de comandos como root

Embora o agente gerencie a lógica e os timers, ele próprio não tem permissão para alterar a composição dos grupos de usuários, pois é executado no contexto de uma sessão de usuário comum. Para executar operações privilegiadas, ele acessa o PrivilegesDaemon.

O PrivilegesDaemon é um LaunchDaemon clássico, executado em nome do root. É ele quem executa o "trabalho sujo" de gerenciar os direitos de acesso. Quando o agente dá um comando para alterar os direitos, o daemon usa as chamadas de sistema CSIdentityAddMember ou CSIdentityRemoveMember para adicionar o usuário ao grupo Administrators (GID 80) ou removê-lo dele.

É possível elevar privilégios secretamente?

Se o utilitário de linha de comando estiver instalado no sistema, é lógico supor que ele pode ser usado para automação. À primeira vista, parece suficiente executar o comando:

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

Os direitos de administrador realmente aparecerão, mas com uma ressalva importante: se a confirmação por senha ou biometria não estiver ativada nas configurações. Caso contrário, o PrivilegesCLI será interrompido e exigirá que você coloque o dedo no sensor Touch ID, insira a senha ou use um smart card.

Aqui reside uma nuance arquitetural interessante: o PrivilegesCLI em si não implementa a lógica de verificação biométrica. Em vez disso, ele acessa o agente (PrivilegesAgent), chamando o método apropriado. Se a autenticação for bem-sucedida, o CLI, em seguida, chama o método requestAdminRightsWithReason. O fato de a autenticação e a solicitação de direitos serem duas chamadas diferentes no código do CLI, leva à reflexão: não seria possível simplesmente pular a etapa de confirmação de identidade e enviar imediatamente a solicitação de elevação de direitos para o agente ou diretamente para o daemon?

Felizmente, os desenvolvedores previram esse cenário. Os mecanismos de proteção aqui funcionam no nível da conexão XPC:

  • Verificação do chamador: Tanto o agente quanto o daemon, ao receberem uma solicitação, verificam o Audit Token do processo chamador.
  • Code Signing: O sistema verifica não apenas o identificador do aplicativo (Bundle ID), mas também sua assinatura digital.
  • Restrição de acesso: Se a solicitação de requestAdminRights vier de um processo cuja assinatura não corresponde à esperada (por exemplo, de um script de terceiros ou um binário modificado), a conexão será encerrada.

Assim, não é possível simplesmente se passar pelo utilitário de linha de comando Privileges. Para chamar com sucesso o método de elevação de direitos, seu processo deve possuir o mesmo nível de confiança e a mesma assinatura que os componentes oficiais do SAP Privileges.

Como funciona a verificação de assinatura de código?

Cada conexão XPC no macOS é acompanhada por um audit_token – um identificador de processo do sistema que não pode ser falsificado. Quando um cliente (por exemplo, CLI ou aplicativo) se conecta ao agente ou daemon, ocorre uma verificação de segurança em três etapas.

Etapa 1. Obtenção da própria assinatura

Primeiro, o serviço (agente ou daemon) determina os parâmetros de sua própria assinatura usando SecCodeCopySelf e SecCodeCopySigningInformation. Isso é necessário para exigir posteriormente do cliente uma assinatura do mesmo editor.

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

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

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

// Extrai o Common Name (CN) do certificado
CFArrayRef certChain = CFDictionaryGetValue(signingInfo, kSecCodeInfoCertificates);
SecCertificateRef issuerCert = (SecCertificateRef)CFArrayGetValueAtIndex(certChain, 0);
SecCertificateCopyCommonName(issuerCert, &subjectCN);
// No final, obtemos algo como: "Developer ID Application: SAP SE (7R5ZEU67FQ)"

Etapa 2. Formação dos requisitos (Code Requirements)

Com base nos dados obtidos, o serviço forma uma string de requisitos. Este é um conjunto de regras que qualquer processo que tente se conectar via XPC deve atender.

objc
NSString *reqString = [NSString stringWithFormat:
    @"anchor apple generic and "        // Certificado emitido pela Apple
    @"certificate leaf [subject.CN] = \"%@\" and "      // Assinado pelo mesmo desenvolvedor (SAP SE)
    @"info [CFBundleShortVersionString] >= \"%@\" and " // Versão não inferior à atual (proteção contra Rollback)
    @"info [CFBundleIdentifier] = %@",                  // Identificador específico do bundle
    commonName, versionString, bundleIdentifier];

A lógica de confiança aqui é a seguinte:

  • O daemon aceita conexões apenas do agente (corp.sap.privileges.agent).
  • O agente aceita conexões de qualquer componente Privileges (é usada a máscara corp.sap.privileges*).

Essa verificação é praticamente impossível de contornar sem roubar a chave privada do desenvolvedor, pois o próprio kernel do macOS é responsável por garantir o cumprimento das regras.

Mais sobre Code Requirement – na documentação. E também há um exemplo de como contornar o Code Requirement, onde não há anchor trusted.

Etapa 3. Verificação do cliente

No momento da chamada de entrada, o serviço pega o audit_token da conexão, cria um objeto SecTask com base nele e o verifica em relação aos requisitos da etapa 2.

objc
// Obtém o audit_token da conexão XPC de entrada
audit_token_t token = [newConnection auditToken];

// Cria um SecTask para verificar o processo chamador
SecTaskRef taskRef = SecTaskCreateWithAuditToken(NULL, token);

// Verifica se o cliente atende aos nossos requisitos (reqString)
OSStatus result = SecTaskValidateForRequirement(taskRef, (__bridge CFStringRef)reqString);

if (result == errSecSuccess) {
    // Cliente passou na verificação – permitimos a interação
    acceptConnection = YES;
} else {
    // Assinatura não corresponde ou identificador é falso – encerramos a conexão
    os_log_error(OS_LOG_DEFAULT, "SAPCorp: Code signature verification failed");
}

O que essa arquitetura oferece?

A divisão em componentes e a verificação rigorosa da assinatura formam vários níveis de defesa:

  • Proteção contra falsificação (Impersonation). Mesmo que um invasor faça engenharia reversa completa do protocolo de troca e escreva seu próprio cliente XPC, ele não conseguirá passar na verificação SecTaskValidateForRequirement. Para que o agente ou daemon sejam enganados, o processo malicioso deve ser assinado com o mesmo certificado de desenvolvedor (SAP SE), o que em condições normais é impossível.
  • Proteção contra ataques de downgrade. A verificação da versão (CFBundleShortVersionString >= ...) garante que o atacante não possa inserir no sistema uma versão antiga, oficialmente assinada, mas vulnerável, do componente Privileges para explorar bugs antigos nela.
  • Isolamento rigoroso de componentes. Graças à verificação do Bundle ID no nível do daemon, a operação de gerenciamento de grupos só pode ser iniciada pelo agente (corp.sap.privileges.agent). Nenhum outro processo – nem mesmo o PrivilegesCLI original – pode enviar uma solicitação diretamente ao daemon. Isso exclui a situação em que um usuário local tenta chamar os métodos do daemon contornando a lógica do agente.

O papel do PrivilegesWatcher: feedback e monitoramento

O PrivilegesWatcher é responsável por monitorar o estado do sistema. Sua principal tarefa é garantir que as informações na interface do usuário estejam sempre atualizadas, mesmo que os direitos tenham sido alterados contornando o próprio aplicativo.

Como isso funciona tecnicamente:

  • Observação do sistema de arquivos. O Watcher se inscreve em eventos no diretório /var/db/dslocal/nodes/Default/groups. É aqui que os dados sobre grupos são armazenados no macOS no formato XML (plist).
  • Monitoramento do admin.plist. Assim que o arquivo admin.plist (responsável pela composição do grupo de administradores) é alterado, o Watcher registra esse evento.
  • Envio de notificações. Após registrar as alterações, o Watcher envia uma notificação do sistema através do DistributedNotificationCenter.

Essa notificação é capturada pelo PrivilegesAgent. Ele verifica novamente o status atual dos direitos do usuário e, se eles realmente mudaram, inicia uma cadeia de ações:

  • Logging → registra o fato da alteração dos direitos no log do sistema.
  • Atualização da interface → envia sinais internos para os componentes principais – Privileges (o aplicativo principal) e PrivilegesTile (o widget ou ícone na barra de menu).

Graças a esse esquema, o usuário vê instantaneamente o status atual ("Administrador" ou "Usuário Padrão") e a contagem regressiva correta do timer, independentemente de como os direitos foram concedidos.

Questões de segurança

Posso me conectar com um depurador ao PrivilegesCLI e pular a confirmação do usuário?

Como a verificação (chamada de biometria ou senha) é iniciada no lado do PrivilegesCLI, o pesquisador pode ter o desejo lógico de se conectar ao processo com um depurador (por exemplo, LLDB) e simplesmente pular as instruções responsáveis pela autenticação. No entanto, aqui entra em jogo o Hardened Runtime – um mecanismo de proteção da integridade de processos no macOS. Como o PrivilegesCLI é assinado com Hardened Runtime ativado e não possui o entitlement especial com.apple.security.get-task-allow (que permite a depuração), o sistema impedirá qualquer tentativa de anexar ao processo.

Ao tentar depurar, você receberá um erro clássico:

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.))

O sistema de proteção do macOS no nível do kernel bloqueia ptrace e outros mecanismos de injeção em binários confiáveis. Assim, mesmo tendo acesso ao arquivo executável na sessão do usuário, você não poderá alterar a lógica de sua execução em tempo real sem violar a integridade da assinatura. E se você assinar novamente o binário com sua própria assinatura para remover o Hardened Runtime, o agente e o daemon imediatamente deixarão de confiar em você.

E quanto ao PrivilegesXPC – é possível se conectar a ele?

Se com o CLI principal tudo está claro, o que impede atacarmos o elo intermediário – PrivilegesXPC? Acontece que a proteção aqui é ainda mais séria. A tentativa de depurar ou executar diretamente este serviço falhará por dois motivos:

  • Uso do Hardened Runtime (assim como no caso do CLI).
  • Presença de Launch Constraints (restrições de inicialização).

Se você tentar executar o binário do serviço diretamente do terminal:

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

...o sistema encerrará o processo instantaneamente:

[1] 13757 killed

A razão reside nos metadados da assinatura. Vamos examiná-los usando codesign:

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

Na saída, veremos dois blocos chave: Parent Launch Constraints e Responsible Launch Constraints.

Parent Launch Constraints definem quem tem o direito de ser o "pai" (aquele que executa diretamente o lançamento) para este processo.

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

Aqui é especificado is-init-proc: true. Isso significa que apenas o processo com PID 1, ou seja, o gerenciador de processos do sistema launchd, pode iniciar este serviço. Qualquer tentativa de iniciá-lo a partir do Bash, Zsh ou via fork() de outro aplicativo será impedida pelo kernel do macOS.

Responsible Launch Constraints definem os requisitos para o aplicativo que iniciou o serviço (estabeleceu a conexão).

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

Isso significa que o "responsável" pelo lançamento só pode ser um aplicativo com Team ID 7R5ZEU67FQ e Bundle ID corp.sap.privileges.

A combinação dessas restrições torna o PrivilegesXPC praticamente invulnerável a manipulações externas:

  • Você não pode iniciá-lo sozinho (o Parent Constraint impede).
  • Você não pode forçá-lo a ser iniciado a partir do seu código malicioso (o Responsible Constraint impede).
  • Você não pode interferir em seu funcionamento através de um depurador (o Hardened Runtime impede).

Você pode ler mais sobre os tipos de restrições na documentação Applying launch environment and library constraints, sobre os campos possíveis – em Defining launch environment and library constraints.

Por que não consigo desativar a biometria nas configurações?

Normalmente, aplicativos no macOS armazenam seus parâmetros no sistema User Defaults. Este é um mecanismo padrão, onde as configurações ficam em arquivos plist no caminho ~/Library/Preferences/<bundle-id>.plist. O usuário pode alterá-las através da GUI ou do terminal:

bash
defaults write corp.sap.privileges RequireAuthentication -bool false

No entanto, em um ambiente corporativo, isso muitas vezes não funciona. A razão está na hierarquia rigorosa pela qual o macOS forma o conjunto final de configurações ao iniciar um aplicativo:

  • Managed (MDM / perfis de configuração): a prioridade mais alta. Se uma configuração for definida pelo administrador através de um perfil (Mobile Device Management), ela substituirá todos os outros níveis.
  • App (configurações do usuário): o que você altera com os botões na interface ou com o comando defaults write.
  • Globals: parâmetros globais, comuns a todos os aplicativos no sistema.
  • Registration: configurações padrão que o desenvolvedor embutiu no código (via registerDefaults.).

Assim, se sua empresa, através de um perfil MDM, estabeleceu a confirmação obrigatória por biometria, quaisquer tentativas do usuário de alterar esse parâmetro (mesmo pelo terminal) serão ignoradas. O sistema simplesmente aplicará o valor da camada "gerenciada" (Managed), que tem prioridade sobre a do usuário.

Fluxo completo: Como o Privileges concede direitos de administrador

Para visualizar o caminho que seu clique percorre na interface, vamos acompanhá-lo por toda a cadeia:

  1. Solicitação. O usuário clica no botão no Privileges.app ou digita um comando no PrivilegesCLI.
  2. Verificação de configurações. O aplicativo solicita os User Defaults. Se um perfil com autenticação obrigatória foi enviado via MDM, o aplicativo é obrigado a solicitar a senha ou o Touch ID.
  3. Saída do sandbox. O Privileges.app não consegue encontrar diretamente o serviço principal. Ele se conecta ao Bundled XPC Service, que encaminha o NSXPCListenerEndpoint do agente.
  4. Autenticação. O PrivilegesAgent inicia a janela de confirmação do sistema (biometria/senha).
  5. Verificação de confiança (etapa 1). O agente recebe a solicitação do cliente. Usando o audit_token, ele verifica se o cliente é de fato o componente original do Privileges, assinado pela SAP SE.
  6. Delegação. Se a verificação for bem-sucedida, o agente envia a solicitação para o PrivilegesDaemon.
  7. Verificação de confiança (etapa 2). O daemon (executado como root) verifica a assinatura do agente. Graças ao Launch Constraints, o daemon tem certeza de que a solicitação veio de um processo confiável, iniciado pelo sistema (launchd).
  8. Execução. O daemon executa a chamada de sistema CSIdentityAddMember, adicionando o usuário ao grupo admin (GID 80).
  9. Monitoramento. O PrivilegesWatcher detecta a alteração em /var/db/dslocal/.../admin.plist e envia uma notificação.
  10. Atualização. Todos os componentes alteram o status na interface, e o ícone na barra de menu inicia a contagem regressiva.

Resumo

À primeira vista, esse esquema pode parecer excessivo. Por que tantos elos intermediários, se fosse possível simplesmente criar um único binário SUID? No entanto, é precisamente essa arquitetura que atende às recomendações modernas da Apple no guia Designing Secure Helpers and Daemons:

  • Princípio do menor privilégio: O aplicativo GUI não sabe nada sobre grupos, e o daemon não sabe nada sobre a interface.
  • Uso de sandbox: A interação com o usuário é maximamente isolada.
  • Separação de responsabilidades: Cada componente executa um conjunto estritamente limitado de ações.
  • Verificação: Verificação de metadados, assinatura e uso de audit_token em cada etapa da interação XPC.

Para administradores de sistemas, isso significa que a ferramenta pode ser confiável (especialmente em conjunto com MDM). Para desenvolvedores, este é um bom exemplo de como as utilidades do sistema devem ser escritas hoje.

Obrigado a todos que leram até o final! Explore o código, use os entitlements corretos e lembre-se: a segurança começa com a arquitetura.

🛡️⚡

Pare de pesquisar. Comece a hackear.

O MundiX é seu copiloto de pentest com IA: comandos exatos, análise de outputs e próximo passo na kill chain — em segundos.

Testar grátis por 7 dias →

Sem cartão para começar · Planos a partir de R$49/mês

📤 Compartilhar & Baixar

📩 Newsletter MundiX

Receba novidades de cibersegurança + um checklist de pentest grátis. Sem spam.

Ao assinar você concorda em receber e-mails. Cancele quando quiser.