Uma visão geral do MS-RPC e seus mecanismos de segurança
O que é RPC?
Uma RPC (chamada de procedimentoremoto) é uma forma de comunicação entre processos (IPC). Ela permite que um cliente invoque um procedimento exposto por um servidor RPC. O cliente chama a função como se fosse uma chamada de procedimento normal, quase sem necessidade de codificar os detalhes da interação remota. O servidor pode ser hospedado em um processo diferente, na mesma máquina ou em uma máquina remota.
Neste artigo, analisaremos a implementação de MS-RPC (RPC da Microsoft). O MS-RPC é derivado da implementação de referência (V1.1) do protocolo RPC no núcleo do DCE (Distributed Computing Environment, ambiente de computação distribuída).
O RPC é muito usado pelo Windows para vários serviços diferentes, como agendamento de tarefas, criação de serviços, configurações e compartilhamentos de impressoras e gerenciamento de dados criptografados armazenados remotamente. Devido à natureza do RPC como um vetor remoto, ele recebe muita atenção do ponto de vista de segurança. Esta publicação do blog tentará abordar os fundamentos de como o MS-RPC funciona, com algum foco em seus mecanismos de segurança incorporados.
Como o MS-RPC funciona?
Os procedimentos e respectivos parâmetros são definidos em uma linguagem descritiva chamada IDL (Interface Definition Language). Cada interface recebe um identificador exclusivo, chamado UUID, que é usado pelo servidor e pelo cliente para endereçar a interface exata que o servidor expõe.
O arquivo IDL é então compilado usando o compilador MIDL (Microsoft Interface Definition Language, linguagem de definição de interface da Microsoft) em arquivos de cabeçalho e código-fonte com a funcionalidade de tempo de execução. Tecnicamente falando, esses são stubs são usados pelo servidor e pelo cliente e passam o controle para o tempo de execução RPC, que é implementado em rpcrt4.dll. O tempo de execução RPC no lado do cliente organiza os dados e os transmite para o tempo de execução RPC do outro lado (Figura 1).
Fig. 1: Tempo de execução do RPC
Você deve se perguntar como o servidor e o cliente se comunicam de um ponto de vista de rede (ou local). O servidor detecta as conexões RPC recebidas registrando uma combinação de uma sequência de protocolo e um ponto de extremidade. Uma sequência de protocolo pode ser, por exemplo, ncacn_ip_tcp (TCP), ncacn_np (named pipe) ou ncalrpc (LPC). O ponto de extremidade pode ser uma porta, como TCP 5555 ou "pipe", se um pipe nomeado for usado. Os pipes nomeados são transportados pelo transporte SMB pela porta TCP 445 usando o compartilhamento IPC$ oculto. A lista completa de sequências de protocolo está disponível no website da Microsoft.
Portanto, o servidor estabelece as conexões em algum ponto de extremidade. Como o cliente sabe onde se conectar? A resposta depende do tipo de ponto de extremidade: dinâmico ou bem-conhecido (também chamado de estático).
Um ponto de extremidade dinâmico é aquele que é registrado por meio do mapeador de ponto de extremidade no lado do servidor. O mapeador de ponto de extremidade, também conhecido como epmapper, é um serviço RPC que mapeia um serviço para o ponto de extremidade real. O epmapper usa as portas TCP 135 e 593 para RPC por HTTP. Portanto, um cliente pode enumerar (usando APIs designadas) todos os servidores RPC registrados dinamicamente em uma máquina remota usando o epmapper.
Um ponto de extremidade bem-conhecido é aquele que não registra por meio do epmapper. O cliente deve saber o ponto de extremidade que o servidor registrou com antecedência. Isso pode ser feito se o ponto de extremidade for codificado no código do cliente e do servidor ou se o ponto de extremidade estiver presente no arquivo IDL. Veja um exemplo de IDL do serviço de spooler de impressão.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
O RPC representa uma conexão lógica entre um cliente e um servidor usando um identificador de vinculação. Tanto o servidor quanto o cliente usam funções para manipular os dados de vinculação, por exemplo, ao definir informações de autenticação. Quando o cliente define a autenticação na vinculação, ela é considerada autenticada. Quando o programa cliente chama a função RPC, a vinculação de rede é realizada. Do ponto de vista do tráfego de rede, o cliente RPC inicia a interação RPC enviando uma solicitação de vinculação com informações de autenticação no pacote. O servidor pode responder com bind_ack (acknowledged) ou bind_nak (an error occurred).
Fig. 2: Segmento do Wireshark mostrando uma resolução de ponto de extremidade dinâmico
Na Figura 2, no segmento do Wireshark, podemos ver a resolução do ponto de extremidade dinâmico da interface do Agendador de tarefas. Depois que o cliente tiver as informações de ponto de extremidade para o Agendador de tarefas, ele criará uma nova conexão. Podemos então ver uma nova conexão com outro processo de vinculação, incluindo um pacote AUTH3 como parte da autenticação de vinculação.
Assim como os pontos de extremidade, as vinculações têm tipos: automático, implícito e explícito (Figura 3). Eles diferem na quantidade de controle que o aplicativo tem sobre o processo de vinculação.
- Vinculação automática (obsoleta): os aplicativos cliente e servidor não lidam com o processo de vinculação, em vez disso, permitem que o tempo de execução RPC controle totalmente esse processo.
- Vinculação implícita: o cliente tem a opção de configurar a alça de vinculação antes que a vinculação ocorra. Depois que o cliente estabelece uma vinculação, a biblioteca de tempo de execução RPC lida com o restante.
- Vinculação explícita: o cliente precisa configurar o identificador de vinculação. Em seguida, o tempo de execução RPC simplesmente o transfere para o servidor.
Segurança RPC para solicitações remotas
Agora que nós conhecemos os fundamentos do funcionamento da RPC, queremos entender quais ações, políticas e mecanismos podem impedir que um cliente acesse uma função. Isso é interessante tanto de uma perspectiva ofensiva quanto defensiva.
Autenticação de transporte
Alguns transportes, como pipes nomeados ou HTTP, têm a autenticação como parte de seu protocolo. Por exemplo, os pipes nomeados são transportados pelo SMB, que tem autenticação. Basicamente, isso significa que, quando um servidor registra um ponto de extremidade de pipe nomeado, apenas clientes com as credenciais de um usuário válido podem se conectar a esse ponto de extremidade. Em um ambiente de domínio, ter um usuário de domínio no mesmo domínio é suficiente para passar na verificação de autenticação. Se as máquinas não fizerem parte de um domínio, o cliente precisará ter as credenciais de um usuário local no servidor remoto (Figura 4). Trataremos as exclusões posteriormente nesta publicação.
Fig. 4: um exemplo de erro do sistema que nega o acesso
Autenticação de vinculação
O servidor pode usar a autenticação de vinculação para ter um mecanismo de autenticação. Para que isso aconteça, o servidor precisa registrar as informações de autenticação chamando RpcServerRegisterAuthInfo. O cliente deve usar o nome principal do serviço correto e o método do Provedor de suporte de segurança que o servidor usa, caso contrário, ele receberá a mensagem "RPC_S_UNKNOWN_AUTHN_SERVICE" do servidor.
Um cliente pode se autenticar em um servidor definindo dados de autenticação e autorização na vinculação e usando RpcBindingSetAuthInfo e RpcBindingSetAuthInfoEx. O cliente especifica o método de autenticação (por exemplo, NTLM, Kerberos, Negotiate, SCHANNEL etc.) e o nome principal do serviço.
Duas coisas importantes a saber:
Se um cliente definir a autenticação na vinculação e o servidor não tiver registrado nenhuma informação de autenticação, o servidor retornará "RPC_S_UNKNOWN_AUTHN_SERVICE".
O cliente não precisa usar uma vinculação de autenticação apenas porque o servidor registrou uma vinculação de autenticação. Além disso, o tempo de execução RPC não expedirá a vinculação de autenticação do cliente com credenciais inválidas. Mais adiante nesta publicação, trataremos de como o servidor pode impedir o acesso a ligações não autenticadas.
É importante observar que o tempo de execução RPC salva as informações de autorização globalmente. Isso significa que se dois servidores RPC compartilharem o mesmo processo e um deles registrar informações de autenticação, o outro servidor também terá informações de autenticação. Um cliente agora pode autenticar a vinculação ao acessar cada um dos servidores. Chamamos isso de comportamento de multiplexação SSPI.
Segurança de pontos de extremidade
Um servidor pode definir um descritor de segurança no ponto de extremidade. O descritor de segurança é um mecanismo de segurança de verificação de acesso geral no Windows. Ele permite a criação de "regras" para quem tem permissão para acessar um objeto e quem tem acesso negado. Quando tenta-se obter acesso ao objeto, o sistema operacional compara o token de acesso do chamador com o descritor de segurança para saber se o acesso é permitido. Nesse caso, o objeto é o ponto de extremidade e o token de acesso é o token de acesso do cliente, derivado da autenticação do protocolo de transporte. Essa verificação é aplicável somente a transportes autenticados, como pipes nomeados, ALPC e HTTP (quando a autenticação é usada). O TCP é um protocolo de transporte não autenticado, portanto, não teria essa verificação de acesso.
Semelhante à multiplexação SSPI, os pontos de extremidade também são multiplexados, a interface e o ponto final não são vinculados. Um servidor registra interfaces e pontos de extremidade separadamente. Se um processo tiver vários pontos de extremidade registrados, cada interface registrada nesse processo poderá ser acessada por meio de cada um desses pontos de extremidade.
Imagine, por exemplo, que você registre sua interface e crie um ponto de extremidade que se conecta em \\pipe\mypipe. Outro servidor RPC hospedado no mesmo processo registrou seu próprio ponto de extremidade na porta TCP 7777. Sua interface também estará acessível por meio do TCP. Isso vai contornar as restrições de segurança impostas ao ponto de extremidade, como um descritor de segurança (Figura 5). Portanto, recomenda-se não depender da segurança do ponto de extremidade ou verificar se o cliente passou pelo protocolo de transporte esperado.
Fig. 5: Exemplo de contornar o descritor de segurança do ponto de extremidade devido à multiplexação de ponto de extremidade
Segurança da interface
Ao analisar a segurança da interface, vemos que há três maneiras de protegê-la: configurando um retorno de chamada de segurança, configurando um descritor de segurança na interface, usando sinalizadores de interface.
Configurar um retorno de chamada de segurança
O primeiro mecanismo de segurança de interface é um retorno de chamada de segurança. Um retorno de chamada de segurança é um retorno de chamada personalizado, implementado pelo desenvolvedor do servidor. A lógica no retorno de chamada de segurança depende do desenvolvedor e permite que o servidor restrinja o acesso à interface. Se o retorno de chamada retornar RPC_S_OK, então o acesso do cliente é permitido.
Se um retorno de chamada de segurança for registrado, o acesso dos clientes não autenticados será automaticamente negado pelo tempo de execução RPC, a menos que o sinalizador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH esteja definido.
Definir um retorno de chamada de segurança e configurar o sinalizador RPC_IF_ALLOW_SECURE_ONLY não significa que o cliente tenha privilégios avançados. Portanto, o retorno de chamada de segurança é o lugar para o servidor perguntar sobre o nível de privilégios do cliente. Isso é feito chamando RpcBindingInqAuthClient. Outra verificação comum é a do método de transporte usado pelo cliente para impor a conexão por meio de transporte autenticado seguro, como pipes nomeados. Isso é feito chamando RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse e comparando o parâmetro Protseq (sequência do protocolo). Isso também impede o abuso da multiplexação de pontos de extremidade.
Cache de retorno de chamada de segurança
Se um retorno de chamada de segurança for registrado, ele será chamado durante as verificações de segurança. Se o retorno de chamada de segurança for aprovado, ou seja, retornar RPC_S_OK, o resultado será armazenado em cache e, nesse caso, o armazenamento em cache é usado. Na próxima vez que o mesmo cliente chamar uma função na interface, como o resultado do retorno de chamada de segurança é armazenado em cache, o retorno de chamada de segurança não será chamado novamente e a entrada em cache será usada.
A implementação do cache é simples, mas depende de alguns fatores.
- O armazenamento em cache está vinculado ao contexto de segurança do cliente, que é obtido da vinculação. Portanto, se o servidor (ou qualquer outro servidor no processo) não registrasse nenhuma vinculação de autenticação ou o cliente não definisse a vinculação de autenticação, o armazenamento em cache seria desativado para essa chamada.
- Se o servidor registrar a interface com o sinalizador RPC_IF_SEC_NO_CACHE, o tempo de execução RPC forçará a chamada do retorno de chamada de segurança para cada chamada, desabilitando, assim, o mecanismo de cache.
- O sinalizador de interface não documentado RPC_IF_SEC_CACHE_PER_PROC também afeta o mecanismo de cache. Se o servidor tiver especificado esse sinalizador, o cache estará em uma base de chamada e não em uma base de interface. Isso significa que, se o cache tiver um valor bem-sucedido para a função X no TESTE de interface, uma chamada para a função Y no TESTE de interface acionará o retorno de chamada de segurança novamente.
O mecanismo de cache pode causar vulnerabilidades lógicas para qualquer retorno de chamada de segurança que seja dependente da função que o cliente chama. Como desenvolvedor de RPC ou auditor, você deve estar ciente do mecanismo de cache. Publicamos uma pesquisa completa sobre a implementação de cache, inclusive sobre as vulnerabilidades que encontramos devido a esse mecanismo.
Como configurar um descritor de segurança
O segundo mecanismo para proteger uma interface é configurar um descritor de segurança na interface. Somente a função RpcServerRegisterIf3 tem a opção de registrar um descritor de segurança na interface. Se não for definido um descritor de segurança, será considerado descritor de segurança padrão:
- NT AUTHORITY\ANONYMOUS LOGON
- Todos
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
Como usar os sinalizadores de interface
A terceira maneira de proteger uma interface é usar os sinalizadores de interface que são definidos ao criar a interface por meio das funções RpcServerRegisterIf*. Do ponto de vista da segurança, as sinalizações interessantes são:
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH: o retorno da chamada de segurança será chamado para cada uma, independentemente do método de transporte ou do nível de autenticação usados.
RPC_IF_ALLOW_LOCAL_ONLY: serão permitidas somente solicitações locais.
RPC_IF_ALLOW_SECURE_ONLY: as conexões são limitadas a clientes com um nível de autorização superior a RPC_C_AUTHN_LEVEL_NONE. A especificação desse sinalizador acarreta a negação de clientes que chegam por meio da sessão NULL. Esse sinalizador não garante o nível de privilégio de um usuário chamador, ele apenas garante que o cliente tenha credenciais válidas.
RPC_IF_SEC_NO_CACHE: esse sinalizador desativará completamente o armazenamento em cache para o retorno de chamada de segurança da interface.
RPC_IF_SEC_CACHE_PER_PROC: em vez de desativar todo o mecanismo de cache, esse sinalizador vai alterar o comportamento padrão para que seja baseado em chamada em vez de em interface.
Políticas em todo o sistema
Há várias políticas em todo o sistema que são definidas dependendo do tipo de máquina: cliente, servidor ou DC (Domain Controller, controlador de domínio).
Uma política do sistema interessante, relacionada à segurança de pontos de extremidade, é a "Restrict Unauthenticated RPC Clients policy" (política de restrição de clientes RPC não autenticados). Podem ser definidos três valores.
- "Authenticated" (autenticado): o tempo de execução RPC vai bloquear o acesso a clientes TCP que não foram autenticados antecipadamente se o sinalizador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH não estiver definido na interface.
- "Authenticated without exceptions" (autenticado sem exceções): todas as conexões não autenticadas são bloqueadas.
- "None (nenhum)": todos os clientes RPC têm permissão para se conectar a servidores RPC em execução na máquina.
Assim, uma superfície de ataque interessante para clientes não autenticados seriam as interfaces acessíveis por meio do TCP e que registram o sinalizador RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH. Obviamente, o cliente precisará ser aprovado na verificação de retorno de chamada de segurança para chamar as funções das interfaces.
Como já mencionamos, as conexões por meio de pipes nomeados, transportados pelo SMB (Service Message Block, bloco de mensagens do servidor), têm sua própria autenticação como parte do protocolo SMB. Um cliente pode usar um "login anônimo", também conhecido como sessão NULL, ao se conectar pelo SMB. As políticas de sistema relacionadas "Restrict anonymous access to Named Pipes (restringir acesso anônimo a pipes nomeados) e "Network access: Named Pipes that can be accessed anonymously" (acesso à rede: pipes nomeados que podem ser acessados anonimamente). Se o primeiro estiver habilitado, somente os pipes nomeados definidos no segundo poderão ser vinculados anonimamente. Para estações de trabalho, a segunda política está vazia, o que basicamente significa que você não pode usar uma sessão NULL em estações de trabalho no domínio. Para uma máquina DC, a lista de pipes nomeados na política inclui "\pipe\netlogon", "\pipe\samr" e "\pipe\lsarpc". Do ponto de vista de um invasor, isso é interessante, pois são pontos de extremidade aos quais pode se conectar uma máquina de fora do domínio.
Por fim, no que diz respeito à verificação do descritor de segurança de ponto de extremidade, existe uma política em todo o sistema que está desativada por padrão: "Network access: Let Everyone permissions apply to anonymous users" (acesso à rede: permitir que todas as permissões se apliquem a usuários anônimos). Quando ativado, o identificador de segurança de todos é adicionado ao token que foi criado para conexões anônimas.
Verificações de segurança do procedimento
O programador do servidor pode implementar verificações de segurança como parte das funções expostas na interface e, portanto, tem a opção de alterar ou personalizar a lógica da verificação de acordo com a função chamada. Uma verificação de acesso é uma verificação de segurança comum, como a vista na Figura 6 (obtida de srvsvc):
Fig. 6: Uma verificação de acesso é um exemplo de verificação de segurança comum
Neste exemplo, LocalrSessionGetInfo chama SsAccessCheck. SsAccessCheck basicamente imita o cliente chamando RpcImpersonateClient, seguido de uma chamada para a função NtAccessCheckAndAuditAlarm, que executa a verificação de acesso. Isso é seguido por uma chamada RpcRevertToSelf para reverter para o token do servidor. Lembre-se de que é importante verificar o valor de retorno de RpcImpersonateClient, pois se ele falhar, o servidor continuará a ser executado no token de segurança do processo do servidor e não no processo do cliente.
Resumo
Esta publicação do blog apresentou muitas informações sobre o MS-RPC e seus mecanismos de segurança. Incentivamos outros pesquisadores a examinar diferentes servidores MS-RPC, pois eles apresentam uma grande superfície de ataque com potencial para novas vulnerabilidades. Esperamos que nossa publicação ajude outros pesquisadores a dar seus primeiros passos na pesquisa do MS-RPC.
Continuamos criando e agrupando mais recursos no RPC para o nosso repositório GitHub. Agradecemos a todos que contribuíram com seu conhecimento e experiência para este tópico, principalmente James Forshaw e Carsten Sandker.