O pequeno bug que conseguiu: CVE-2024-30089 é uma vulnerabilidade sutil no kernel que utilizei para explorar uma máquina Windows 11 totalmente atualizada (com todas as mitigações de segurança de Virtualization Based Security e de hardware ativadas) e garantir minha primeira vitória no Pwn2Own deste ano.
Neste artigo, descrevo minha abordagem direta para caça de bugs: escolher um ponto de partida e seguir intuitivamente um caminho até que algo chame minha atenção. Esse bug é interessante porque pode ser acionado de forma confiável devido a um erro de lógica. O erro ocorre em um estado específico dentro de um sistema de comunicação entre processos (inter-process communication, IPC), o que leva a um use-after-free. Encontrar o bug exigiu comparar os caminhos de execução do programa ao longo de seus vários estados possíveis, processo que detalho ao longo do texto. Igualmente intrigantes são a origem do bug e a abordagem da Microsoft para corrigi-lo. Esses tópicos também são abordados neste post.
Uma pergunta comum que recebo sobre pesquisa de vulnerabilidades é como começar. Na verdade, escolher um alvo e cumpri-lo pode ser uma das etapas mais difíceis do processo de pesquisa. A vulnerabilidade discutida aqui está no Microsoft Kernel Streaming Service (mskssrv.sys). Leia este post do blog para ter uma visão geral do subsistema. Nele, destaquei algumas características do subsistema MSKSSRV que podem torná-lo uma boa superfície de ataque, especialmente seu mecanismo de IPC.
A base de código do MSKSSRV é relativamente pequena, e a última vulnerabilidade nesse subsistema que encontrei acabou sendo explorada de forma independente em estado selvagem como um 0-day. Também ouvi sobre esforços adicionais de outros pesquisadores e empresas revisando esse driver. Por isso, inicialmente caí na armadilha comum de assumir que não havia mais bugs a serem descobertos nessa superfície de ataque. Mas, como eu mesmo havia sugerido no post de blog anterior, escolhi confiar em meus instintos e continuar investigando.
Uma ótima forma de obter novas ideias de pesquisa é acompanhar pesquisas recentes. Li um excelente post de blog de k0shl que me inspirou a buscar um determinado tipo de bug. Na vulnerabilidade encontrada por k0shl, o reference count de um objeto é inicializado e incrementado sem bloqueio adequado, criando uma janela de use-after-free. Apesar do bug de k0shl estar em uma área do usuário e não no kernel, o estilo de codificação da biblioteca vulnerável me lembrou de quando auditei anteriormente o driver MSKSSRV.
O servidor MS KS (MSKSSRV) interage com um processo do espaço do usuário via objetosFSStreamReg. e FSContextReg são ambos derivados da classe de baseFSRegObject não implementa uma função de tabela virtual de bloqueio e a implementação da classe de base ( é simplesmente uma instrução nop . Isso significa que nenhum mecanismo de bloqueio foi implementado para os objetos . Por outro lado,FSStreamReg:
Bloco de código 1: FSRendezvousServer::Fechar, bloqueio e desbloqueio para FSRegObjects
Há dois objetos derivados da classe base
Como mencionei no meu último blog post, o aspecto de compartilhamento de objetos entre processos (inter-process object sharing) do MSKSSRV é um caminho interessante para vulnerabilidades, então decidi focar ainda mais nele. O mecanismo IPC do subsistema é ilustrado no diagrama a seguir:
Diagrama 1: Comunicação entre processos no servidor MS KS
Ao abrir um identificador de arquivo para o dispositivo MSKSSRV via CreateFile cria um FILE_OBJECT que corresponde a esse identificador. Utilizando esse identificador, um processo pode inicializar um novo fluxo ou um objeto de contexto enviando um IOCTL ao dispositivo por meio da função DeviceIoControl . O processo de inicialização designa qual processo remoto pode registrar o objeto especificando a ID do processo por meio do argumentolpInBufferFSRegObject é armazenado em
Irp->CurrentStackLocation->FileObject->FsContext2.
O mesmo ponteiro para o objeto FSRegObject é armazenado duas vezes em FsContext2, uma vez no usado pelo processo de inicialização e uma vez no usado no processo de registro. Dessa forma, as referências ao objeto podem ser compartilhadas entre os processos. Por exemplo, usando um objeto FSStreamReg, vários processos podem ter acesso ao buffer de quadro de fluxo, conforme mostrado no Diagrama 1. A contagem de referência do é inicializada como 1 e, em seguida, incrementada novamente após a conclusão da inicialização. Registrar o aumenta novamente sua contagem de referências, totalizando três referências por objeto.
Diagrama 2: Inicialização e registro de objetos FSContextReg
Decidi reexaminar os locais que auditei anteriormente, tentando encontrar vulnerabilidades de bloqueio. Notei que a função
Bloco de código 2: rotinas DispatchCleanup e DipatchCleanup para o MS KS Server Driver
Uma rotinade despacho lida com um ou mais tipos de IRPs, que são solicitações de E/S agrupadas. No Windows, quando todas as referências de identificadores a um arquivo são fechadas, o driver correspondente do sistema de arquivos para o arquivo recebe uma solicitação
No MSKSSRV,
Bloco de código 3: FSRendezvousServer::Close, verificações de processo em FSRegObjects
Informações específicas do processo são armazenadas no
Irp->CurrentStackLocation->FileObject->FsContext2
no momento da inicialização ou registro. O driver determina quais recursos específicos de processo (objetos
, objetos de evento e outros) precisam ser liberados verificando o ID do processo que está efetuando a chamada. Se o processo for de inicialização ou registro, será feita uma limpeza adicional para esses recursos específicos do processo.
Isso chamou minha atenção porque, geralmente, todas as rotinas de despacho são executadas em um contextode processo arbitrário, com algumas exceções. Em outras palavras, o sistema escolhe um encadeamento para realizar o trabalho de despacho; o encadeamento escolhido é arbitrário. Descobri que
Além disso, como uma funcionalidade do sistema operacional Windows, os identificadores podem ser compartilhados com outros processos (por meio de herança de processos filhos ou usando a função de API DuplicateHandle ). Por meio do identificador de arquivo compartilhado, o outro processo também pode interagir com o mesmo
Diagrama 3: Compartilhamento do Identificador do Dispositivo do MS KS Server
Devido a isso, também é possível que o contexto do processo durante
Diagrama 4: Processo externo fechando o último identificador para um FILE_OBJECT
Percebi que a função
Bloco de código 4: FSRegObject::Release pode ser chamado duas vezes a partir de FSRendezvousServer::Close
Em geral, o fato de haver mais desreferências do que referências em um objeto não é a única maneira de ocorrer um uso após liberação. No entanto, neste caso, podemos ter certeza de que se um foi liberado, sua contagem de referência caiu para zero. A última vez que um válido é acessado durante as solicitações IRP é em uma chamada para . Portanto, se um uso após liberação for possível, uma chamada para sempre ocorrerá depois que o objeto já tiver sido liberado. Durante a chamada, o objeto será mais uma vez desreferenciado. Por essa razão, contar o número de desreferências é uma boa heurística para encontrar usos após liberações neste caso específico.
A única coisa que restava fazer era rastrear os possíveis estados do programa, anotando quando o objeto era liberado e acessado. Fiz isso emulando mentalmente a lógica do programa durante as solicitações de , cada uma começando com as funções de despacho correspondentes (bloco de código 2), para cada um dos casos possíveis.
A seguir, são apresentados os estados com base em qual processo fecha a referência final a um identificador. Cada entrada representa o número de desreferências do objeto FSContextReg que ocorrem se o processo correspondente fechar o identificador final. Nota: não há diferença funcional entre o IDENTIFICADOR Nº 1 (identificador de inicialização) e o IDENTIFICADOR Nº 2 (identificador de registro), pois o campo aponta para a mesma memória em ambos s representados pelos respectivos identificadores.
Desreferenciações de FSContextReg para cada um dos possíveis estados do IPC do MSKSSRV
Sucesso! O último estado resulta em quatro desreferências: duas pelo processo externo e duas pelo processo de inicialização (e também de registro), embora inicialmente haja apenas três referências, o que significa que um uso após a liberação é possível! Também repeti o mesmo exercício com os objetos do
Durante esse exercício mental de simulação da máquina virtual, encontrei o problema. Se o processo for o processo de inicialização ou o processo de registro, a limpeza apropriada ocorre e o ponteiro armazenado em
Bloco de código 5: FSRegObject::CloseInitProcess define o ponteiro FSContext como NULL
Isso significa
No entanto, se o processo de chamada for um processo externo, nenhuma limpeza ocorrerá e
Agora, se o segundo identificador for fechado por um processo que inicializou e registrou o objeto FSContextReg, o objeto limpará todos os seus recursos do processo armazenados, ficando vazio. Isso faz com que
Isso resulta em um total de quatro desreferências em um único objeto
Diagrama 5: CVE-2024-30089 representado
O leitor que acompanha o raciocínio pode se perguntar por que, então, um processo externo não pode ser o último a fechar ambas as identificadores, já que isso aparentemente também levaria a quatro desreferências. Antes que o objeto seja destruído e liberado, ele é desvinculado de uma lista armazenada no objeto global FSRendezvousServer , é verificado se o ponteiro no é um membro válido da lista. Nesse caso, o objeto é liberado na segunda chamada para ao final da função. Durante a quarta chamada para o objeto já foi desvinculado da lista, tornando-o um objeto inválido e, portanto, não podendo ser utilizado. Para que ocorra um uso após a liberação, ele deve ocorrer depois que o objeto for recuperado e validado em . O trecho de código abaixo mostra a primitiva de uso após liberação que pode ser obtida pela vulnerabilidade:
Bloco de código 6: caminho da primitiva de UAF
No guia de atualização de segurança desta vulnerabilidade, a pontuação CVSS indica que a “Invasão [é] Mais Provável” e a complexidade do ataque para esta vulnerabilidade é “Baixa”. Embora a Microsoft não forneça explicações detalhadas para sua pontuação, notei alguns padrões enquanto corrija outras vulnerabilidades. A vulnerabilidade provavelmente recebeu essa pontuação porque decorre de um erro lógico, tornando-a acionável de forma confiável. Seguindo as etapas descritas no Diagrama 5, um invasor pode acionar consistentemente o cenário de uso após a liberação retratado no Bloco de código 6. No entanto, isso não significa que a exploração dela na prática seja simples. Um passo a passo detalhado das etapas de invasão será abordado na próxima parte desta série.
Entender como um bug ocorreu é importante para cultivar uma abordagem proativa em relação às práticas de desenvolvimento seguro. Para identificar como a vulnerabilidade foi introduzida, analisei versões anteriores do driver obtidas do Winbindex e procurei por quaisquer diferenças na lógica da função
Na seção de vulnerabilidade, mencionei que a causa principal desse bug não estava definindo
Irp->CurrentStackLocation->FileObject->FsContext2
para
se o processo de chamada for um processo externo. Para minha surpresa, vi essa linha exata de código em uma versão inicial do mskssrv.sys:
Bloco de código 7: Versão inicial do FSrendezvousServer::Close, FsContext2 está definido como NULL
No bloco de código mostrado acima,
Bloco de código 8: FsContext definido como NULL em uma verificação de sinalizador de funcionalidade
É mostrado no bloco de código acima uma verificação para o sinalizador de funcionalidade
Feature_Servicing_TeamsUsingMediaFoundationCrashes
.
Os sinalizadores de funcionalidade são um componente do Windows que alterna várias funcionalidades e experimentos, embora não haja muitas informações públicas sobre eles. Neste post de blog anterior, discutimos como os sinalizadores de funcionalidade foram usados para patches de vulnerabilidade. Às vezes, os sinalizadores de funcionalidade são usados para testar uma funcionalidade antes de ser adotada oficialmente. Nesse caso, se o recurso
Observou-se que essa funcionalidade estava habilitada por padrão nas instalações do Windows 10. No Windows 11, e conforme mostrado no Bloco de Código 1, essa funcionalidade de sinalização condicional não está presente e o ponteiro não está definido como
Feature_Servicing_TeamsUsingMediaFoundationCrashes
a funcionalidade está habilitada, não está definido como NULL, o que introduz a vulnerabilidade., tornando-a também vulnerável.
Devido ao nome da funcionalidade, analisei a funcionalidade do Microsoft Teams, o software de videoconferência. Confirmei que a aplicação pode usar a funcionalidade MSKSSRV para compartilhar streamings de mídia entre processos. É possível que o compartilhamento do identificador de streaming estivesse causando o travamento do Teams. Um tópico interessante para pesquisa adicional seria examinar como o Teams compartilha o dispositivo MSKSSRV entre processos e por que realizar a limpeza adequada de ponteiros poderia causar a falha da aplicação.
Esta parte da série se concentra na vulnerabilidade em si, que inclui sua correção. Eu estava particularmente interessado em examinar a correção para esse bug, já que minha correção proposta parecia desencadear falhas no Microsoft Teams. É importante mencionar que, neste momento, ainda preciso examinar como uma aplicação real usa o driver MSKSSRV na prática. A falta desse contexto introduz pontos cegos na compreensão de por que um sistema é projetado da maneira como é. Uma correção completa para esse bug exigiria alguma reestruturação do código-base e poderia revelar mais detalhes sobre como o sistema IPC deveria funcionar. Eu também esperava obter alguns insights sobre práticas seguras de codificação dos desenvolvedores da Microsoft.
Para minha decepção, o erro lógico que causou a vulnerabilidade, que foi corrigido nas atualizações de segurança de junho de 2024, não foi abordado diretamente.Em vez disso, foi adicionada uma verificação de token de acesso antes dos caminhos de código vulneráveis. Veja o código abaixo para o IOCTL de inicialização de contexto, tratado pela função
Bloco de código 9: a função IOCTL começa com uma verificação de sinalizador de funcionalidade e verifica se o processo de chamada é um servidor de frame
A função acima começa verificando se uma funcionalidade está habilitada. Este é provavelmente o sinalizador de funcionalidade correspondente à correção. Se a funcionalidade estiver habilitada,
Vamos dar uma olhada em
Bloco de código 10: KsIsCurrentProcessFrameServer realizando uma verificação SID no token de acesso do thread de chamada
Esta função verifica o token do encadeamento de chamada em relação a dois identificadores de segurança (SIDs) específicos. Os SIDs correspondem a um token no grupo . Se qualquer um dos SIDs estiver habilitado no token de acesso do encadeamento de chamada, o código da função vulnerável poderá ser executado.
Depois de ver isso, suspeitei que provavelmente ainda havia um bug de administrador para kernel presente. Em última análise, os problemas de corrupção de memória não foram resolvidos. Confirmei isso fazendo uma pequena modificação na minha exploração original: um usuário administrador pode iniciar o serviço , abra um identificador para o serviço e crie o processo de exploração usando o identificador. Consegui obter uma primitiva completa de R/W do kernel em um sistema totalmente corrigido.
Embora a Microsoft não considere o Administrador para o Kernel um limite de boundary, bugs semelhantes foram usados por agentes da ameaça para obter uma primitiva de R/W do kernel e usá-la para operações de blinding e rootkit de EDR. Se você tiver interesse em saber quais tipos de coisas podem ser feitas com esse primitivo, confira minha palestra no BlackHat com FuzziSec.
Este post se concentrou na parte de pesquisa de vulnerabilidades do meu empreendimento Pwn2Own, que consistia em encontrar uma vulnerabilidade de kernel de 0-day que pudesse ser explorada para escalonamento de privilégios. Este post descreve a jornada: inspirar-se em outras pesquisas, não conseguir encontrar um bug, escolher um novo ângulo, encontrar algo suspeito e, finalmente, identificar onde a vulnerabilidade reside. Agora que um bug foi identificado e há uma primitiva de uso após a liberação, o resto deve ser simples, certo? A Microsoft parece pensar assim, eles classificaram esse bug como "invasão mais provável" com complexidade de ataque "baixa". Eles estão certos? Vou falar sobre isso, a estratégia de invasão, e revelar o significado do título da série na próxima parte!
Mantenha-se atualizado sobre as tendências mais importantes (e intrigantes) do setor em IA, automação, dados e muito mais com o boletim informativo Think. Consulte a Declaração de privacidade da IBM.
Andréa Piazza, pelos diagramas incríveis
Anna Erikpatrick, por explicar pacientemente o modelo de segurança do Windows para mim