Explorando três vulnerabilidades de execução remota de código no tempo de execução RPC
Resumo executivo
Ben Barnea, pesquisador da Akamai, encontrou três vulnerabilidades importantes no tempo de execução RPC do Microsoft Windows que foram atribuídas ao CVE-2023-24869, CVE-2023-24908e CVE-2023-23405, todas com uma pontuação básica de 8,1.
As vulnerabilidades podem levar à execução remota de código. Como a biblioteca de tempo de execução RPC é carregada em todos os servidores RPC e eles são comumente usados pelos serviços do Windows, todas as versões do Windows (Desktop e Servidor) são afetadas.
As vulnerabilidades são estouros de números inteiros em três estruturas de dados usadas pelo tempo de execução RPC.
As vulnerabilidades foram divulgadas de forma responsável à Microsoft e abordadas na March Patch Tuesday de 2023.
Introdução
O MS-RPC é um protocolo altamente usado em redes Windows por muitos serviços e aplicações. Dessa forma, vulnerabilidades no MS-RPC podem levar a consequências adversas. O Grupo de Inteligência de Segurança da Akamai está envolvido em pesquisas do MS-RPC há um ano. Encontramos e exploramos vulnerabilidades, criamos ferramentas de pesquisa e escrevemos alguns dos componentes internos não documentados do protocolo.
Embora as publicações anteriores do blog se concentrem em vulnerabilidades nos serviços, este post examinará vulnerabilidades no tempo de execução RPC – o "mecanismo" do MS-RPC. Essas vulnerabilidades são semelhantes a uma vulnerabilidade que descobrimos em maio de 2022.
Um padrão de estouro de número inteiro
As três novas vulnerabilidades têm um tema comum: elas existem por causa de um estouro de número inteiro na inserção em três estruturas de dados:
SIMPLE_DICT (um dicionário que salva apenas valores)
SIMPLE_DICT2 (um dicionário que salva chaves e valores)
Fila
Todas essas estruturas de dados são implementadas usando uma matriz dinâmica que cresce cada vez que a matriz fica cheia. Esse crescimento acontece alocando duas vezes a memória alocada para a matriz atual. Essa alocação é suscetível a estouro de número inteiro.
A Figura 1 apresenta o código descompactado do tempo de execução RPC. Ela mostra o processo de inserção na estrutura SIMPLE_DICT e a linha vulnerável de código (destacada) onde o estouro de número inteiro pode ser acionado.
Explorando uma vulnerabilidade
Para acionar uma vulnerabilidade, precisamos entender sua causa subjacente, descobrir se existe um fluxo para a função vulnerável e quanto tempo leva para ser acionado.
Para sermos breves, descreveremos uma das três vulnerabilidades: a da estrutura de dados Fila. Como os demais estouros de números inteiros são de natureza semelhante, a análise feita nas seções a seguir pode ser realizada de forma intercambiável.
Entendendo o estouro de número inteiro
Uma fila é uma estrutura de dados FIFO (first in, first out; primeiro a entrar, primeiro a sair) simples. Uma fila no tempo de execução do RPC é implementada usando uma estrutura que contém uma matriz de entradas de fila, capacidade atual e a posição do último item na fila.
Quando uma nova entrada é adicionada à fila (desde que haja um slot disponível), todos os itens são movidos para frente na matriz e o novo item é adicionado ao início da matriz. A posição do último item na fila é incrementada.
Quando ocorre um desenfileiramento, o último item é retirado e a posição do último item é diminuída (Figura 2).
Como mencionado anteriormente, a vulnerabilidade ocorre na inserção de uma nova entrada. Se a matriz dinâmica estiver cheia, o código faz o seguinte:
Aloca uma nova matriz com o seguinte tamanho:
CurrentCapacity * 2 * sizeof(QueueEntry)Copia itens antigos para a nova matriz
Libera itens antigos da matriz
Dobra a capacidade
Para um sistema de 32 bits, o estouro ocorrerá no cálculo do novo tamanho da matriz:
Preenchemos a fila com 0x10000000 (!) itens.
Ocorre uma expansão. O tamanho da nova alocação é calculado: 0x10000000 * 16. À medida que ocorre o transbordamento, o novo tamanho da alocação é 0.
Uma matriz de comprimento zero é alocada.
O código copia a matriz de itens antigos para a nova matriz pequena. Isso levará a uma cópia curinga (uma cópia linear grande).
Em um sistema de 64 bits, essa vulnerabilidade não é explorável porque há uma enorme alocação que falha. Isso faz com que o código saia sem acionar gravações fora dos limites. Apesar de os sistemas de 64 bits serem invulneráveis para esse problema, eles são vulneráveis a outros estouros de números inteiros (em SIMPLE_DICT e SIMPLE_DICT2).
Fluxo de código
Uma conexão RPC é representada usando a classe OSF_SCONNECTION. Cada conexão pode lidar com várias chamadas de cliente (OSF_SCALL), mas em cada momento, apenas uma chamada é permitida para executar na conexão, enquanto as outras estão na fila.
Assim, uma função interessante que usa uma fila é OSF_SCONNECTION::MaybeQueueThisCall. Essa função é chamada como parte do envio de uma nova chamada que chegou na conexão. Nesse caso, a fila é usada para "colocar em espera" as chamadas recebidas enquanto outra chamada está sendo processada.
Portanto, temos uma maneira controlada pelo usuário de preencher uma fila (enviando chamadas de cliente uma após a outra), mas essa função impõe um requisito: Uma chamada está sendo processada atualmente pela conexão. Significa que, se quisermos preencher a fila, precisamos ter uma chamada que leve tempo para ser concluída. Enquanto a chamada é processada, enviaremos várias novas chamadas que preencherão a fila de despacho.
Que tipo de chamada de função leva mais tempo para ser concluída?
O melhor candidato é uma função na qual podemos causar um loop infinito.
A segunda melhor opção é uma vulnerabilidade de coerção de autenticação porque, em seguida, o servidor se conecta a nós, portanto, temos controle sobre o tempo de resposta.
Um último recurso seria uma função complexa com lógica complicada ou uma função que processa muitos dados e, portanto, leva muito tempo para ser concluída.
Decidimos usar nossa própria vulnerabilidade de coerção de autenticação.
O tempo necessário para acionar
Até agora, entendemos o que é necessário para preencher a fila e como isso pode ser feito. Mas surge uma pergunta importante: será que é prático?
Temos controle mínimo sobre a variável na qual o estouro de número inteiro acontece – só podemos aumentá-la uma de cada vez – semelhante aos estouros de refcount (contagem de referência). Esse tipo de estouro de número inteiro é marginalmente pior que os estouros de número inteiro, em que duas variáveis que controlamos totalmente são adicionadas ou multiplicadas, ou quando o tamanho adicionado pode ser um pouco controlado (por exemplo, tamanho do pacote).
Conforme mencionado anteriormente, devemos alocar 0x10000000 (aprox. 268M) itens. Isso é muito.
Tentar acionar a vulnerabilidade no meu computador gerou uma taxa de aproximadamente 15 a 20 chamadas em fila por segundo. Significa que levaria cerca de 155 dias para acioná-la em uma máquina comum! Esperávamos que causasse um número maior de chamadas em fila por segundo. Há um motivo pelo qual o tempo de execução RPC é tão lento? Não é um processo de multithreading?
Nossa suposição era que vários threads processavam e enfileiravam chamadas diferentes para a mesma conexão simultaneamente. Depois de algumas inversões, descobrimos que, na prática, o fluxo é um pouco diferente.
Gerenciamento de pacotes do MS-RPC
Pouco antes de uma chamada ser enviada, o código gira um novo thread (se necessário) e chama OSF_SCONNECTION::TransAsyncReceive. TransAsyncReceive tenta receber uma solicitação na mesma conexão. Em seguida, ele envia a solicitação para o novo thread (chamando CO_SubmitRead).
O outro thread seleciona a solicitação de TppWorkerThread e, por fim, leva a ProcessReceiveComplete, que chama MaybeQueueThisCall para colocar o SCRALL na fila de despacho. Em seguida, ele se propaga e tenta receber uma nova solicitação para essa conexão.
Portanto, embora possamos ter vários threads em execução, na prática, apenas um está sendo usado para a conexão. Significa que não podemos adicionar simultaneamente chamadas à fila a partir de vários threads.
Pacote "sobras"
Tentamos encontrar maneiras de fazer mais chamadas por segundo para minimizar o tempo necessário para acionar a vulnerabilidade. Ao reverter o código de recebimento, percebemos que, se o tamanho de um pacote for maior do que a solicitação RPC real no pacote, o tempo de execução RPC salvará o restante. Mais tarde, quando verifica novas solicitações, ele não usa imediatamente o soquete. Primeiro, ele verifica se tem "sobras" de pacotes e, em caso afirmativo, atende a uma nova solicitação dos sobras.
Isso nos permitiu enviar muito menos pacotes, cada um contendo o número máximo de solicitações. O número de chamadas em fila por segundo permaneceu relativamente inalterado quando tentamos fazer exatamente isso, de modo que não parecia ajudar.
Resumo
Apesar da baixa probabilidade esperada de explorar essas vulnerabilidades, nós as adicionamos à lista de vulnerabilidades importantes que encontramos em nosso último ano de pesquisa sobre o MS-RPC. É importante lembrar que até mesmo vulnerabilidades difíceis de explorar são uma oportunidade para um invasor competente (e paciente).
Embora o MS-RPC exista há décadas, ele ainda tem vulnerabilidades aguardando para serem descobertas.
Esperamos que esta pesquisa incentive outros pesquisadores a analisar o MS-RPC e a superfície de ataque que ele apresenta. Gostaríamos de agradecer à Microsoft por responder rapidamente e corrigir os problemas.
Nossos Repositório do GitHub está cheio de ferramentas e técnicas para ajudá-lo a começar.