O principal pesquisador Tom Court of Context, uma empresa de segurança da informação, fala sobre como ele conseguiu detectar um bug potencialmente perigoso no código do cliente Steam.Os jogadores de PC preocupados com a segurança perceberam que a Valve lançou recentemente uma nova atualização do cliente Steam.
Neste post, eu quero dar
desculpas para jogar jogos no trabalho para contar a história de um bug relacionado que existia no cliente Steam por pelo menos dez anos, que até julho do ano passado poderia levar à execução remota de código (execução remota de código, RCE) em todos os 15 milhões de clientes ativos.
Desde julho, quando a Valve (finalmente) compilou seu código com a moderna proteção de exploração ativada, isso só poderia levar a uma falha do cliente, e o RCE era possível apenas em combinação com uma vulnerabilidade separada de vazamento de informações.
Declaramos a Valve uma vulnerabilidade em 20 de fevereiro de 2018 e, para crédito da empresa, ela foi corrigida na filial beta menos de 12 horas depois. A correção foi movida para a filial estável em 22 de março de 2018.
Breve revisão
A base da vulnerabilidade foi o dano ao heap dentro da biblioteca do cliente Steam, que poderia ser chamado remotamente, na parte do código envolvida na recuperação do datagrama fragmentado de vários pacotes UDP recebidos.
O cliente Steam troca dados por meio de seu próprio protocolo (protocolo Steam), que é implementado sobre o UDP. Existem duas áreas neste protocolo que são particularmente interessantes devido à vulnerabilidade:
- Comprimento do pacote
- O comprimento total do datagrama reconstruído
O erro foi causado pela falta de uma verificação simples. O código não verificou se o comprimento do primeiro datagrama fragmentado é menor ou igual ao comprimento total do datagrama. Parece uma supervisão comum, pois para todos os pacotes subsequentes que transmitem fragmentos do datagrama, a verificação é realizada.
Sem erros adicionais de vazamento de dados, é muito difícil controlar os danos de pilha nos sistemas operacionais modernos, portanto, a execução remota de código é difícil de implementar. No entanto, nesse caso, graças ao alocador de memória do próprio Steam e ao ASLR que estava ausente no arquivo binário steamclient.dll (até julho do ano passado), esse bug poderia ser usado como base para uma exploração muito confiável.
Abaixo está uma descrição técnica da vulnerabilidade e sua exploração associada até
implementações de execução de código.
Detalhes da vulnerabilidade
Informação necessária para a compreensão
Protocolo
Terceiros (por exemplo,
https://imfreedom.org/wiki/Steam_Friends ), com base na análise do tráfego gerado pelo cliente Steam, executaram engenharia reversa e criaram documentação detalhada do protocolo Steam. Inicialmente, o protocolo foi documentado em 2008 e não mudou muito desde então.
O protocolo é implementado como um protocolo de transmissão com o estabelecimento de uma conexão através de um fluxo de datagramas UDP. Os pacotes, de acordo com a documentação no link acima, têm a seguinte estrutura:
Aspectos importantes:
- Todos os pacotes começam com 4 bytes de " VS01 "
- packet_len descreve o tamanho da informação útil (para datagramas não fragmentados, o valor é igual ao comprimento dos dados)
- type descreve o tipo de pacote, que pode ter os seguintes valores:
- Autenticação de chamada 0x2
- 0x4 Aceitar Conexão
- Conexão de redefinição 0x5
- 0x6 Um pacote é um fragmento de um datagrama
- O pacote 0x7 é um datagrama separado
- Os campos de origem e destino são identificadores atribuídos para rotear pacotes corretamente em várias conexões dentro do cliente Steam
- Caso o pacote seja um fragmento de um datagrama:
- split_count indica o número de fragmentos nos quais o datagrama é dividido
- data_len indica o comprimento total do datagrama recuperado
- O processamento inicial desses pacotes UDP ocorre na função CUDPConnection :: UDPRecvPkt dentro do steamclient.dll
Criptografia
Informações úteis do pacote de datagrama são criptografadas pelo AES-256 usando uma chave, que é negociada entre o cliente e o servidor em cada sessão. A negociação principal é realizada da seguinte maneira:
- O cliente gera uma chave aleatória AES de 32 bytes e o RSA a criptografa com a chave pública Valve antes de enviá-la ao servidor.
- O servidor, com uma chave privada, pode descriptografar esse valor e aceitá-lo como uma chave AES-256, que será usada na sessão
- Depois que a chave é acordada, todas as informações úteis na sessão atual são criptografadas com essa chave.
Vulnerabilidade
A vulnerabilidade está presente dentro do método
RecvFragment da classe
CUDPConnection . Não há símbolos na versão de lançamento da biblioteca steamclient, no entanto, ao pesquisar por linhas binárias em uma função de nosso interesse, um link para "
CUDPConnection :: RecvFragment " é encontrado. A entrada dessa função é realizada quando o cliente recebe um pacote UDP contendo um datagrama Steam do tipo 0x6 (um "fragmento de um datagrama").
1. A função inicia verificando o status da conexão para garantir que esteja no estado “
Conectado ”.
2. Em seguida, o campo
data_len no datagrama Steam é verificado para garantir que contenha menos de
0x20000060 bytes (parece que esse valor foi escolhido arbitrariamente).
3. Se o teste for aprovado, a função verificará se a conexão está coletando fragmentos de algum datagrama ou se é o primeiro pacote do fluxo.
4. Se este for o primeiro pacote no fluxo, o campo
split_count será
verificado para ver quantos pacotes esse fluxo estenderá
5. Se o fluxo estiver dividido em vários pacotes, o campo
seq_no_of_first_pkt será
verificado para garantir que ele corresponda ao número de série do pacote atual. Isso garante que o pacote seja o primeiro no fluxo.
6. O campo
data_len é
verificado novamente contra o limite de
0x20000060 bytes. Além disso, verifica-se que
split_count é menor que
0x709b pacotes.
7. Se essas condições forem atendidas, um valor booleano é definido para indicar que agora estamos coletando fragmentos. Ele também verifica se ainda não temos um buffer alocado para armazenar fragmentos.
8. Se o ponteiro para o buffer de coleção de fragmentos não for zero, o buffer de coleção de fragmentos atual será liberado e um novo buffer será alocado (consulte o retângulo amarelo na figura abaixo). É aqui que o erro aparece. Espera-se que o buffer de coleção de fragmentos seja alocado no tamanho de
data_len bytes. Se tudo for bem-sucedido (e o código não verificar - um pequeno erro), os dados úteis do datagrama serão copiados para esse buffer usando o
memmove , confiando que o número de bytes para cópia está indicado em
packet_len .
A supervisão mais importante do desenvolvedor foi que a verificação " packet_len é menor ou igual a data_len " não é executada. Isso significa que é possível transferir data_len menos que packet_len e ter até 64 KB de dados (devido ao fato de o campo packet_len ter 2 bytes de largura) copiado para um buffer muito pequeno, o que possibilita explorar a corrupção de heap.Exploração de vulnerabilidade
Esta seção supõe que haja uma solução alternativa para o ASLR. Isso leva ao fato de que antes de iniciar a operação, o endereço inicial do steamclient.dll é conhecido.
Falsificação de pacotes
Para que os pacotes UDP atacantes sejam recebidos pelo cliente, ele deve examinar o datagrama de saída (cliente -> servidor), enviado para descobrir os identificadores da conexão cliente / servidor, bem como o número de série. Em seguida, o invasor deve falsificar os endereços IP e as portas de origem / destino junto com os identificadores de cliente / servidor e aumentar o número de série aprendido em um.
Gerenciamento de memória
Para alocar memória com mais de 1024 (0x400) bytes, é usado um alocador de sistema padrão. Para alocar memória menor ou igual a 1024 bytes, o Steam usa seu próprio alocador que funciona da mesma maneira em todas as plataformas suportadas. Este artigo não discutirá em detalhes esse distribuidor, com exceção dos seguintes aspectos principais:
- Grandes blocos de memória são solicitados ao alocador do sistema, que são então divididos em fragmentos de tamanho fixo para uso nas solicitações de alocação de memória do cliente Steam.
- A seleção é realizada sequencialmente, entre os fragmentos utilizados não há metadados que os separam.
- Cada bloco grande armazena sua própria lista de memória livre, implementada como uma lista vinculada individualmente.
- A parte superior da lista de memória livre indica o primeiro fragmento livre na memória e os primeiros 4 bytes desse fragmento indicam o próximo fragmento livre (se houver).
Alocação de memória
Quando a memória é alocada, o primeiro bloco livre é desconectado da parte superior da lista de memória livre e os primeiros 4 bytes desse bloco correspondentes ao
next_free_block são copiados para a
variável de membro
freelist_head dentro da classe do
alocador .
Memória livre
Quando um bloco é liberado, o campo
freelist_head é copiado para os primeiros 4 bytes do bloco liberado (
next_free_block ) e o endereço do bloco liberado é copiado para a variável de membro
freelist_head da classe do distribuidor.
Como obter uma primitiva de gravação
Um estouro de buffer ocorre no heap e, dependendo do tamanho dos pacotes que causaram a corrupção, a alocação de memória pode ser controlada pelo alocador padrão do Windows (ao alocar memória com mais de 0x400 bytes) ou pelo próprio alocador do Steam (ao alocar memória com menos de 0x400 bytes). Devido à falta de medidas de segurança em meu próprio distribuidor Steam, decidi que era mais fácil usá-lo para uma exploração.
Vamos voltar à seção sobre gerenciamento de memória: sabe-se que a parte superior da lista de blocos de memória livre de um determinado tamanho é armazenada como uma variável membro da classe distribuidora e o ponteiro para o próximo bloco livre da lista é armazenado como os primeiros 4 bytes de cada bloco livre da lista.
Se houver um bloco livre próximo ao bloco em que ocorreu o estouro, os danos no heap nos permitem substituir o ponteiro
next_free_block . Se você considerar que um monte pode ser preparado para isso, o ponteiro reescrito
next_free_block pode ser definido como um endereço para gravação, após o qual a alocação subsequente de memória será gravada nesse local.
O que usar: datagramas ou fragmentos
Ocorre um erro com corrupção de memória no código responsável pelo processamento de fragmentos de datagramas (pacotes do tipo 6). Após a ocorrência de danos, a função
RecvFragment () está no estado em que espera receber mais fragmentos. No entanto, se eles chegarem, uma verificação é realizada:
fragment_size + num_bytes_already_received < sizeof(collection_buffer)
Mas, obviamente, esse não é o caso, porque nosso primeiro pacote já violou essa regra (é possível ignorar essa verificação) e um erro ocorrerá. Para evitar isso, você precisa evitar o
método CUDPConnection :: RecvFragment () após
corrupção de memória.
Felizmente,
CUDPConnection :: RecvDatagram () ainda pode receber e processar pacotes enviados do tipo 7 (datagramas) até que
RecvFragment () seja válido, e isso pode ser usado para iniciar a primitiva de gravação.
Problemas de criptografia
Os pacotes recebidos por
RecvDatagram () e
RecvFragment () devem ser criptografados. No caso de
RecvDatagram (), a descriptografia é realizada quase imediatamente após o recebimento. No caso de
RecvFragment (), ocorre após o recebimento do último fragmento na sessão.
O problema de explorar a vulnerabilidade surge porque não conhecemos a chave de criptografia criada em cada sessão. Isso significa que qualquer código OP / código de shell que enviarmos será "descriptografado" usando o AES256, que transformará nossos dados em lixo. Portanto, é necessário encontrar um método de operação, que é possível quase imediatamente após o recebimento do pacote, antes que os procedimentos de descriptografia possam processar as informações úteis contidas no buffer de pacotes.
Como conseguir a execução de código
Dada a restrição de descriptografia descrita acima, a operação deve ser executada antes da descriptografia dos dados recebidos. Isso impõe restrições adicionais, mas a tarefa ainda é viável: você pode reescrever o ponteiro para que aponte para o objeto
CWorkThreadPool armazenado em um local previsível dentro da seção de dados do arquivo binário. Embora os detalhes e a funcionalidade interna dessa classe sejam desconhecidos, pode-se assumir pelo nome que ele suporta um pool de threads que você pode usar quando precisar "trabalhar". Depois de estudar várias linhas de depuração em um arquivo binário, você pode entender que, entre esses trabalhos, há criptografia e descriptografia (
CWorkItemNetFilterEncrypt ,
CWorkItemNetFilterDecrypt ), portanto, quando essas tarefas são enfileiradas, a classe
CWorkThreadPool é
usada . Sobrescrevendo esse ponteiro e escrevendo o local desejado, podemos simular o ponteiro da vtable e a vtable associada a ele, o que nos permite executar o código, por exemplo, quando
CWorkThreadPool :: AddWorkItem () é chamado, o que deve ocorrer antes de qualquer processo de descriptografia.
A figura abaixo mostra a exploração bem-sucedida da vulnerabilidade até o estágio de obtenção de controle sobre o registro EIP.
A partir de agora, você pode criar uma cadeia ROP que leve à execução de código arbitrário. O vídeo abaixo mostra como um invasor inicia remotamente uma calculadora do Windows em uma versão totalmente corrigida do Windows 10.
Resumir
Se você chegar a esta parte do artigo, obrigado por sua persistência! Espero que você entenda que esse é um bug muito simples e fácil de explorar devido à falta de meios modernos de proteção contra explorações. O código vulnerável provavelmente era muito antigo, mas, caso contrário, funcionou bem; portanto, os desenvolvedores não viram a necessidade de examiná-lo ou atualizar seus scripts de construção. A lição aqui é que é importante que os desenvolvedores revisem periodicamente o código antigo e construam sistemas para garantir que estejam em conformidade com os padrões de segurança modernos, mesmo que a funcionalidade do próprio código permaneça inalterada. Foi incrível encontrar em 2018 um bug tão simples com consequências tão sérias em uma plataforma de software muito popular. Isso deve ser um incentivo para procurar essas vulnerabilidades para todos os pesquisadores!
Por fim, vale a pena discutir o processo de divulgação responsável de informações. Relatamos esse bug à Valve em uma carta para sua
equipe de segurança (
security@valvesoftware.com ) por volta das 16:00 GMT e apenas 8 horas depois, uma correção foi criada e lançada no cliente beta do Steam. Graças a isso, a Valve agora está em primeiro lugar em nossa tabela (imaginária) do concurso “Quem corrigirá a vulnerabilidade mais rapidamente” - uma exceção agradável em comparação com a divulgação de erros para outras empresas, que geralmente resulta em um longo processo de aprovação.
Uma página que descreve os detalhes de todas as atualizações do cliente